added email subscriptions

This commit is contained in:
2024-09-27 22:16:29 +10:00
parent db018e9120
commit b10b6b712e
10 changed files with 203 additions and 5 deletions

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers;
use App\Models\EmailSubscriptions;
use App\Models\SentEmail;
use Illuminate\Http\Request;
class UnsubscribeController extends Controller
{
/**
* Display a listing of the resource.
*/
public function destroy($email)
{
$emailModel = SentEmail::where('id', $email)->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');
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\SentEmail;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@@ -48,6 +49,18 @@ class SendEmail implements ShouldQueue
*/ */
public function handle(): void 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); Mail::to($this->to)->send($this->mailable);
} }
} }

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Mail;
use App\Models\Workshop;
use App\Traits\HasUnsubscribeLink;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
class UpcomingWorkshops extends Mailable
{
use Queueable, SerializesModels, HasUnsubscribeLink;
public $subject;
public $email;
public $workshops;
public function __construct($email, $subject = 'Upcoming Workshops 🌟')
{
$this->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
]);
}
}

View File

@@ -7,8 +7,6 @@ use Illuminate\Database\Eloquent\Model;
class EmailSubscriptions extends Model class EmailSubscriptions extends Model
{ {
use HasFactory;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
* *

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Traits;
trait HasUnsubscribeLink
{
protected $unsubscribeLink;
public function withUnsubscribeLink($link)
{
$this->unsubscribeLink = $link;
return $this;
}
}

View File

@@ -6,6 +6,8 @@ use Illuminate\Support\Str;
trait Slug trait Slug
{ {
protected $appendsSlug = ['slug'];
/** /**
* Boot function from Laravel. * 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. * Get the value indicating whether the IDs are incrementing.
* *
@@ -47,7 +59,7 @@ trait Slug
*/ */
public function getRouteKey() public function getRouteKey()
{ {
return $this->slug(); return $this->slug;
} }
/** /**
@@ -68,7 +80,7 @@ trait Slug
* *
* @return string * @return string
*/ */
public function slug() public function getSlugAttribute()
{ {
return Str::slug($this->title) . '-' . $this->id; return Str::slug($this->title) . '-' . $this->id;
} }

View File

@@ -0,0 +1,29 @@
@component('mail::message', ['email' => $email])
<p>Hey there!</p>
<p>Check out our exciting workshops coming up in the next few weeks:</p>
<p class="center">
@php
$currentLocation = null;
@endphp
@foreach($workshops as $workshop)
@if($workshop->location->name !== $currentLocation)
<h2 style="margin-top: 32px; margin-bottom: 6px">{{ $workshop->location->name }}</h2>
@php
$currentLocation = $workshop->location->name;
@endphp
@endif
<p style="margin-bottom: 6px">{{ $workshop->starts_at->format('D, j M, g:i A') . ' - ' }}<a href="{{ route('workshop.show', $workshop->slug) }}">{{ $workshop->title }}</a> ({{ ($workshop->price && is_numeric($workshop->price) && $workshop->price != '0' ? '$' . number_format((float)$workshop->price, 2) : 'Free') . ( $workshop->status === 'scheduled' ? ' / Opens soon' : '') }})</p>
@endforeach
<p class="tall center" style="margin-top: 32px">
@component('mail::button', ['url' => 'https://stemmechanics.com.au/workshops'])
View All Workshops
@endcomponent
</p>
<p>We hope to see you at one of our upcoming workshops!</p>
<p>Warm regards,</p>
<p>—James 😁</p>
@slot('subcopy')
<h4>Why did I get this email?</h4>
<p class="sub">You received this email as you are subscribed to our upcoming workshop email list. If you wish no longer receive this email, you can <a href="{{ $unsubscribeLink }}">unsubscribe here</a>.</p>
@endslot
@endcomponent

View File

@@ -43,7 +43,6 @@ h1 {
margin: 0; margin: 0;
padding: 0; padding: 0;
display: inline-block; display: inline-block;
border: 1px solid red;
} }
h2 { h2 {

View File

@@ -1,10 +1,39 @@
<?php <?php
use App\Jobs\SendEmail;
use App\Mail\UpcomingWorkshops;
use App\Mail\UserWelcome;
use App\Models\Media; use App\Models\Media;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
Artisan::command('email:send', function() {
$subjects = [
'🚀 Your STEM Adventure Awaits!',
'⚡ Spark Your STEM Skills in a Workshop',
'🔬 Unleash Your Curiosity in a Workshop',
'🧠 Boost Your Brain with STEM Workshops',
'🌟 Become a STEM Star: Join Our Workshops',
'🔧 Tinker, Create, Learn in a Workshop',
'🎨 Where Science Meets Creativity',
'🏆 Level Up Your STEM Skills',
'🌈 Discover the STEM Spectrum',
'🔮 Future Innovators: Workshops Unveiled',
];
$subject = $subjects[array_rand($subjects)];
$subscribers = DB::table('email_subscriptions')
->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() { Artisan::command('cleanup', function() {
// Clean up expired tokens // Clean up expired tokens

View File

@@ -7,6 +7,7 @@ use App\Http\Controllers\LocationController;
use App\Http\Controllers\MediaController; use App\Http\Controllers\MediaController;
use App\Http\Controllers\PostController; use App\Http\Controllers\PostController;
use App\Http\Controllers\SearchController; use App\Http\Controllers\SearchController;
use App\Http\Controllers\UnsubscribeController;
use App\Http\Controllers\UserController; use App\Http\Controllers\UserController;
use App\Http\Controllers\WorkshopController; use App\Http\Controllers\WorkshopController;
use Illuminate\Support\Facades\Route; 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('workshops/{workshop}', [WorkshopController::class, 'show'])->name('workshop.show');
Route::get('search', [SearchController::class, 'index'])->name('search.index'); Route::get('search', [SearchController::class, 'index'])->name('search.index');
Route::get('unsubscribe/{email}', [UnsubscribeController::class, 'destroy'])->name('unsubscribe');
Route::middleware('auth')->group(function () { Route::middleware('auth')->group(function () {
Route::get('/account', [AccountController::class, 'show'])->name('account.show'); Route::get('/account', [AccountController::class, 'show'])->name('account.show');