From b10b6b712ef818628542c2d9daa11ffdf2b00582 Mon Sep 17 00:00:00 2001 From: James Collins Date: Fri, 27 Sep 2024 22:16:29 +1000 Subject: [PATCH] added email subscriptions --- .../Controllers/UnsubscribeController.php | 44 ++++++++++++++ app/Jobs/SendEmail.php | 13 +++++ app/Mail/UpcomingWorkshops.php | 58 +++++++++++++++++++ app/Models/EmailSubscriptions.php | 2 - app/Traits/HasUnsubscribeLink.php | 14 +++++ app/Traits/Slug.php | 16 ++++- .../views/emails/upcoming-workshops.blade.php | 29 ++++++++++ .../views/vendor/mail/html/themes/default.css | 1 - routes/console.php | 29 ++++++++++ routes/web.php | 2 + 10 files changed, 203 insertions(+), 5 deletions(-) create mode 100644 app/Http/Controllers/UnsubscribeController.php create mode 100644 app/Mail/UpcomingWorkshops.php create mode 100644 app/Traits/HasUnsubscribeLink.php create mode 100644 resources/views/emails/upcoming-workshops.blade.php diff --git a/app/Http/Controllers/UnsubscribeController.php b/app/Http/Controllers/UnsubscribeController.php new file mode 100644 index 0000000..9423bc2 --- /dev/null +++ b/app/Http/Controllers/UnsubscribeController.php @@ -0,0 +1,44 @@ +first(); + + if (!$emailModel) { + // Email not found, redirect to home page with a message + return redirect()->route('index')->with([ + 'message' => 'The unsubscribe link is invalid or has expired.', + 'message-title' => 'Invalid Unsubscribe Link', + 'message-type' => 'warning' + ]); + } + + // Existing unsubscribe logic + $subscriptions = EmailSubscriptions::where('email', $emailModel->recipient)->get(); + + if ($subscriptions->isEmpty()) { + session()->flash('message', 'You are already unsubscribed.'); + session()->flash('message-title', 'Already Unsubscribed'); + session()->flash('message-type', 'info'); + } else { + EmailSubscriptions::where('email', $emailModel->recipient)->delete(); + + session()->flash('message', 'You have been successfully unsubscribed.'); + session()->flash('message-title', 'Unsubscribed'); + session()->flash('message-type', 'success'); + } + + return redirect()->route('index'); + } +} diff --git a/app/Jobs/SendEmail.php b/app/Jobs/SendEmail.php index 0044d95..53a8c60 100644 --- a/app/Jobs/SendEmail.php +++ b/app/Jobs/SendEmail.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Models\SentEmail; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -48,6 +49,18 @@ class SendEmail implements ShouldQueue */ public function handle(): void { + // Record sent email + $sentEmail = SentEmail::create([ + 'recipient' => $this->to, + 'mailable_class' => get_class($this->mailable) + ]); + + // Add unsubscribe link if mailable supports it + if (method_exists($this->mailable, 'withUnsubscribeLink')) { + $unsubscribeLink = route('unsubscribe', ['email' => $sentEmail->id]); + $this->mailable->withUnsubscribeLink($unsubscribeLink); + } + Mail::to($this->to)->send($this->mailable); } } diff --git a/app/Mail/UpcomingWorkshops.php b/app/Mail/UpcomingWorkshops.php new file mode 100644 index 0000000..f34fed0 --- /dev/null +++ b/app/Mail/UpcomingWorkshops.php @@ -0,0 +1,58 @@ +subject = $subject; + $this->email = $email; + $this->workshops = $this->getUpcomingWorkshops(); + } + + private function getUpcomingWorkshops() + { + $startDate = Carbon::now()->addDays(3); + $endDate = Carbon::now()->addDays(42); + + return Workshop::select('workshops.*', 'locations.name as location_name') + ->join('locations', 'workshops.location_id', '=', 'locations.id') + ->whereIn('workshops.status', ['open','scheduled']) + ->whereBetween('workshops.starts_at', [$startDate, $endDate]) + ->where('locations.name', 'not like', '%private%') + ->orderBy('locations.name') + ->orderBy('workshops.starts_at') + ->get(); + } + + public function build() + { + // Bail if there are no upcoming workshops + if ($this->workshops->isEmpty()) { + return false; + } + + return $this + ->subject($this->subject) + ->markdown('emails.upcoming-workshops') + ->with([ + 'email' => $this->email, + 'workshops' => $this->workshops, + 'unsubscribeLink' => $this->unsubscribeLink + ]); + } +} diff --git a/app/Models/EmailSubscriptions.php b/app/Models/EmailSubscriptions.php index 66bcdaf..8d6b5f3 100644 --- a/app/Models/EmailSubscriptions.php +++ b/app/Models/EmailSubscriptions.php @@ -7,8 +7,6 @@ use Illuminate\Database\Eloquent\Model; class EmailSubscriptions extends Model { - use HasFactory; - /** * The attributes that are mass assignable. * diff --git a/app/Traits/HasUnsubscribeLink.php b/app/Traits/HasUnsubscribeLink.php new file mode 100644 index 0000000..f37f12f --- /dev/null +++ b/app/Traits/HasUnsubscribeLink.php @@ -0,0 +1,14 @@ +unsubscribeLink = $link; + return $this; + } +} diff --git a/app/Traits/Slug.php b/app/Traits/Slug.php index da3f9f0..3bd67a0 100644 --- a/app/Traits/Slug.php +++ b/app/Traits/Slug.php @@ -6,6 +6,8 @@ use Illuminate\Support\Str; trait Slug { + protected $appendsSlug = ['slug']; + /** * Boot function from Laravel. * @@ -20,6 +22,16 @@ trait Slug }); } + /** + * Initialize the trait. + * + * @return void + */ + public function initializeSlug(): void + { + $this->appends = array_merge($this->appends ?? [], $this->appendsSlug); + } + /** * Get the value indicating whether the IDs are incrementing. * @@ -47,7 +59,7 @@ trait Slug */ public function getRouteKey() { - return $this->slug(); + return $this->slug; } /** @@ -68,7 +80,7 @@ trait Slug * * @return string */ - public function slug() + public function getSlugAttribute() { return Str::slug($this->title) . '-' . $this->id; } diff --git a/resources/views/emails/upcoming-workshops.blade.php b/resources/views/emails/upcoming-workshops.blade.php new file mode 100644 index 0000000..39c5135 --- /dev/null +++ b/resources/views/emails/upcoming-workshops.blade.php @@ -0,0 +1,29 @@ +@component('mail::message', ['email' => $email]) +

Hey there!

+

Check out our exciting workshops coming up in the next few weeks:

+

+ @php + $currentLocation = null; + @endphp + @foreach($workshops as $workshop) + @if($workshop->location->name !== $currentLocation) +

{{ $workshop->location->name }}

+ @php + $currentLocation = $workshop->location->name; + @endphp + @endif +

{{ $workshop->starts_at->format('D, j M, g:i A') . ' - ' }}{{ $workshop->title }} ({{ ($workshop->price && is_numeric($workshop->price) && $workshop->price != '0' ? '$' . number_format((float)$workshop->price, 2) : 'Free') . ( $workshop->status === 'scheduled' ? ' / Opens soon' : '') }})

+ @endforeach +

+ @component('mail::button', ['url' => 'https://stemmechanics.com.au/workshops']) + View All Workshops + @endcomponent +

+

We hope to see you at one of our upcoming workshops!

+

Warm regards,

+

โ€”James ๐Ÿ˜

+ @slot('subcopy') +

Why did I get this email?

+

You received this email as you are subscribed to our upcoming workshop email list. If you wish no longer receive this email, you can unsubscribe here.

+ @endslot +@endcomponent diff --git a/resources/views/vendor/mail/html/themes/default.css b/resources/views/vendor/mail/html/themes/default.css index 5046a45..fdaf330 100644 --- a/resources/views/vendor/mail/html/themes/default.css +++ b/resources/views/vendor/mail/html/themes/default.css @@ -43,7 +43,6 @@ h1 { margin: 0; padding: 0; display: inline-block; - border: 1px solid red; } h2 { diff --git a/routes/console.php b/routes/console.php index 7fb3cf0..3d75655 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,10 +1,39 @@ whereNotNull('confirmed') + ->get(); + + foreach ($subscribers as $subscriber) { + dispatch(new SendEmail($subscriber->email, new UpcomingWorkshops($subscriber->email, $subject)))->onQueue('mail'); + } +})->purpose('Send newsletter to confirmed subscribers')->daily(); + Artisan::command('cleanup', function() { // Clean up expired tokens diff --git a/routes/web.php b/routes/web.php index c4eabce..5cec90b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,7 @@ use App\Http\Controllers\LocationController; use App\Http\Controllers\MediaController; use App\Http\Controllers\PostController; use App\Http\Controllers\SearchController; +use App\Http\Controllers\UnsubscribeController; use App\Http\Controllers\UserController; use App\Http\Controllers\WorkshopController; use Illuminate\Support\Facades\Route; @@ -20,6 +21,7 @@ Route::get('workshops/past', [WorkshopController::class, 'past_index'])->name('w Route::get('workshops/{workshop}', [WorkshopController::class, 'show'])->name('workshop.show'); Route::get('search', [SearchController::class, 'index'])->name('search.index'); +Route::get('unsubscribe/{email}', [UnsubscribeController::class, 'destroy'])->name('unsubscribe'); Route::middleware('auth')->group(function () { Route::get('/account', [AccountController::class, 'show'])->name('account.show');