From 0ad6f8d833fe13e1db7554326c896eba9225fbe2 Mon Sep 17 00:00:00 2001 From: Jonathan Nielson Date: Sat, 11 Oct 2025 23:36:10 -0500 Subject: [PATCH] feat: email sub/unsub/queue end-to-end rough draft --- .env.example | 12 +- .../QueueNewArticleAlertForSubscribers.php | 62 ++++ app/Console/Kernel.php | 2 +- app/Http/Controllers/ReaderController.php | 15 + app/Http/Controllers/WriterController.php | 4 +- app/Mail/NewArticlePublished.php | 57 ++++ app/Models/Article.php | 1 + composer.json | 4 + ...024_02_05_014414_create_articles_table.php | 4 + .../2025_10_11_220917_create_jobs_table.php | 32 ++ public/word-salad-writer.css | 8 + public/word-salad.css | 35 +-- .../views/guest/unsubscribe-fail.blade.php | 11 + .../views/guest/unsubscribe-success.blade.php | 26 ++ resources/views/guest/unsubscribe.blade.php | 15 + resources/views/layouts/guest.blade.php | 4 +- .../views/mail/articles/published.blade.php | 26 ++ .../views/vendor/mail/html/button.blade.php | 24 ++ .../views/vendor/mail/html/footer.blade.php | 11 + .../views/vendor/mail/html/header.blade.php | 12 + .../views/vendor/mail/html/layout.blade.php | 57 ++++ .../views/vendor/mail/html/message.blade.php | 27 ++ .../views/vendor/mail/html/panel.blade.php | 14 + .../views/vendor/mail/html/subcopy.blade.php | 7 + .../views/vendor/mail/html/table.blade.php | 3 + .../views/vendor/mail/html/themes/default.css | 297 ++++++++++++++++++ .../views/vendor/mail/text/button.blade.php | 1 + .../views/vendor/mail/text/footer.blade.php | 1 + .../views/vendor/mail/text/header.blade.php | 1 + .../views/vendor/mail/text/layout.blade.php | 9 + .../views/vendor/mail/text/message.blade.php | 27 ++ .../views/vendor/mail/text/panel.blade.php | 1 + .../views/vendor/mail/text/subcopy.blade.php | 1 + .../views/vendor/mail/text/table.blade.php | 1 + resources/views/writer/dashboard.blade.php | 50 ++- resources/views/writer/login.blade.php | 2 +- routes/web.php | 27 +- 37 files changed, 845 insertions(+), 46 deletions(-) create mode 100644 app/Console/Commands/QueueNewArticleAlertForSubscribers.php create mode 100644 app/Mail/NewArticlePublished.php create mode 100644 database/migrations/2025_10_11_220917_create_jobs_table.php create mode 100644 resources/views/guest/unsubscribe-fail.blade.php create mode 100644 resources/views/guest/unsubscribe-success.blade.php create mode 100644 resources/views/guest/unsubscribe.blade.php create mode 100644 resources/views/mail/articles/published.blade.php create mode 100644 resources/views/vendor/mail/html/button.blade.php create mode 100644 resources/views/vendor/mail/html/footer.blade.php create mode 100644 resources/views/vendor/mail/html/header.blade.php create mode 100644 resources/views/vendor/mail/html/layout.blade.php create mode 100644 resources/views/vendor/mail/html/message.blade.php create mode 100644 resources/views/vendor/mail/html/panel.blade.php create mode 100644 resources/views/vendor/mail/html/subcopy.blade.php create mode 100644 resources/views/vendor/mail/html/table.blade.php create mode 100644 resources/views/vendor/mail/html/themes/default.css create mode 100644 resources/views/vendor/mail/text/button.blade.php create mode 100644 resources/views/vendor/mail/text/footer.blade.php create mode 100644 resources/views/vendor/mail/text/header.blade.php create mode 100644 resources/views/vendor/mail/text/layout.blade.php create mode 100644 resources/views/vendor/mail/text/message.blade.php create mode 100644 resources/views/vendor/mail/text/panel.blade.php create mode 100644 resources/views/vendor/mail/text/subcopy.blade.php create mode 100644 resources/views/vendor/mail/text/table.blade.php diff --git a/.env.example b/.env.example index feb3053..a76ac74 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,7 @@ +PUBLISHING_NAME="Word Salad" +SUBHEAD="A blog with an as yet unclear direction." +CONTACT_EMAIL="test@example.org" + APP_NAME=Laravel APP_ENV=local APP_KEY= @@ -13,7 +17,7 @@ DB_CONNECTION=sqlite BROADCAST_DRIVER=log CACHE_DRIVER=file FILESYSTEM_DISK=local -QUEUE_CONNECTION=sync +QUEUE_CONNECTION=database SESSION_DRIVER=file SESSION_LIFETIME=120 @@ -24,8 +28,4 @@ MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS="hello@example.com" -MAIL_FROM_NAME="${APP_NAME}" - -PUBLISHING_NAME="Word Salad" -SUBHEAD="A blog with an as yet unclear direction." -CONTACT_EMAIL="test@example.org" +MAIL_FROM_NAME="${PUBLISHING_NAME}" diff --git a/app/Console/Commands/QueueNewArticleAlertForSubscribers.php b/app/Console/Commands/QueueNewArticleAlertForSubscribers.php new file mode 100644 index 0000000..116039b --- /dev/null +++ b/app/Console/Commands/QueueNewArticleAlertForSubscribers.php @@ -0,0 +1,62 @@ +where('published_at', '<', now()) + ->whereNull('subscribers_notified_at') + ->get(); + + logger(count($articlesForNotification) . ' article(s) ready for notifying.'); + + $subscribersToNotify = Subscriber::all(); + + logger(count($subscribersToNotify) . ' subscribers(s) to notify.'); + + logger((count($articlesForNotification) * count($subscribersToNotify)) . ' emails being queued for send.'); + + foreach($articlesForNotification as $article) { + + $article->update(['subscribers_notified_at' => now()]); + + foreach($subscribersToNotify as $subscriber) { + Mail::to($subscriber)->queue(new NewArticlePublished($article, $subscriber)); + } + } + + $endTime = microtime(true); + + logger('Command Succeeded in ' . ($endTime - $startTime) . ' second(s).'); + + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e6b9960..82629b6 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -12,7 +12,7 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule): void { - // $schedule->command('inspire')->hourly(); + $schedule->command('app:queue-new-article-alert-for-subscribers')->everyFiveMinutes()->withoutOverlapping(); } /** diff --git a/app/Http/Controllers/ReaderController.php b/app/Http/Controllers/ReaderController.php index a468ffa..0d6f8c0 100644 --- a/app/Http/Controllers/ReaderController.php +++ b/app/Http/Controllers/ReaderController.php @@ -22,4 +22,19 @@ class ReaderController extends Controller return redirect(route('subscribe-fail')); } } + + public function unsubscribe(Request $request): RedirectResponse + { + // Validate email + $validated = $request->validate([ + 'email' => 'required|email|max:512', + ]); + + if($validated['email']) { + Subscriber::where('email', $validated['email'])->delete(); + return redirect(route('unsubscribe-success')); + } else { + return redirect(route('unsubscribe-fail')); + } + } } diff --git a/app/Http/Controllers/WriterController.php b/app/Http/Controllers/WriterController.php index c277a8f..02c9152 100644 --- a/app/Http/Controllers/WriterController.php +++ b/app/Http/Controllers/WriterController.php @@ -42,7 +42,9 @@ class WriterController extends Controller public function dashboard(Request $request): View { - return view('writer.dashboard'); + $allArticles = Article::with(['author'])->orderBy('last_updated')->get(); + + return view('writer.dashboard', ['articles' => $allArticles]); } public function newArticle(Request $request): View diff --git a/app/Mail/NewArticlePublished.php b/app/Mail/NewArticlePublished.php new file mode 100644 index 0000000..d863628 --- /dev/null +++ b/app/Mail/NewArticlePublished.php @@ -0,0 +1,57 @@ +article->title, + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'mail.articles.published', + with: [ + 'article' => $this->article, + 'email' => $this?->subscriber?->email, + ], + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/app/Models/Article.php b/app/Models/Article.php index a140fdf..6d4198f 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -27,6 +27,7 @@ class Article extends Model 'header_image_url', 'markdown_body', 'processed_at', + 'subscribers_notified_at', ]; /** --------------------------------------- diff --git a/composer.json b/composer.json index 8a3d72d..e9c79ad 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,10 @@ } }, "scripts": { + "mailpit": [ + "Composer\\Config::disableProcessTimeout", + "mailpit" + ], "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" diff --git a/database/migrations/2024_02_05_014414_create_articles_table.php b/database/migrations/2024_02_05_014414_create_articles_table.php index ca55f4b..7d35d72 100644 --- a/database/migrations/2024_02_05_014414_create_articles_table.php +++ b/database/migrations/2024_02_05_014414_create_articles_table.php @@ -44,6 +44,10 @@ return new class extends Migration // HTML body was generated (among other things) $table->dateTime('processed_at')->nullable(); + // The dateTime when the emails were queued for sending for all subscribers + // after first publishing + $table->dateTime('subscribers_notified_at')->nullable(); + $table->timestamps(); }); } diff --git a/database/migrations/2025_10_11_220917_create_jobs_table.php b/database/migrations/2025_10_11_220917_create_jobs_table.php new file mode 100644 index 0000000..6098d9b --- /dev/null +++ b/database/migrations/2025_10_11_220917_create_jobs_table.php @@ -0,0 +1,32 @@ +bigIncrements('id'); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + } +}; diff --git a/public/word-salad-writer.css b/public/word-salad-writer.css index 5f35fa0..b948c20 100644 --- a/public/word-salad-writer.css +++ b/public/word-salad-writer.css @@ -1,3 +1,11 @@ .is-invalid { border: red 4px solid; } + +main { + padding: 0 1em; +} + +#article-list-table { + text-overflow: ellipsis; +} diff --git a/public/word-salad.css b/public/word-salad.css index ed33e1e..e5a1e9a 100644 --- a/public/word-salad.css +++ b/public/word-salad.css @@ -179,6 +179,10 @@ footer { margin-bottom: 2.5em; } +#writer-login { + min-height: 60vh; +} + /* .bubble-panel { border-radius: 50px; background: #813d9c; @@ -186,34 +190,3 @@ footer { 33px -33px 65px #9c4abd; padding: 2em; }*/ - - -/* Create a custom animation */ -@keyframes move-out { - from { - transform: translateY(0%); - } - - to { - transform: translateY(-100%); - } -} - -@keyframes move-in { - from { - transform: translateY(100%); - } - - to { - transform: translateY(0%); - } -} - -/* Apply the custom animation to the old and new page states */ -::view-transition-old(root) { - animation: 0.4s ease-in both move-out; -} - -::view-transition-new(root) { - animation: 0.4s ease-in both move-in; -} \ No newline at end of file diff --git a/resources/views/guest/unsubscribe-fail.blade.php b/resources/views/guest/unsubscribe-fail.blade.php new file mode 100644 index 0000000..d8e1d8d --- /dev/null +++ b/resources/views/guest/unsubscribe-fail.blade.php @@ -0,0 +1,11 @@ +@extends('layouts.guest') + +@section('title', 'Un-Subscription Failed') + +@section('content') +
+

Error Attempting to Unsubscribe

+ +

Something went wrong while removing your interest from my list. Please try again, or contact me at {{ config('blog.contact_email') }} if the problem persists.

+
+@endsection diff --git a/resources/views/guest/unsubscribe-success.blade.php b/resources/views/guest/unsubscribe-success.blade.php new file mode 100644 index 0000000..a8e92f2 --- /dev/null +++ b/resources/views/guest/unsubscribe-success.blade.php @@ -0,0 +1,26 @@ +@extends('layouts.guest') + +@section('title', 'Subscription Removed') + +@section('content') +
+

Subscription Removed

+

Please allow up to five minutes for this to take effect*. You will no longer receive emails from me. Thank you for your interest.

+ +
+ + +
+@endsection diff --git a/resources/views/guest/unsubscribe.blade.php b/resources/views/guest/unsubscribe.blade.php new file mode 100644 index 0000000..bf6b58b --- /dev/null +++ b/resources/views/guest/unsubscribe.blade.php @@ -0,0 +1,15 @@ +@extends('layouts.guest') + +@section('title', 'Unsubscribe') + +@section('content') +
+

Unsubscribe from All Emails

+
+ @csrf + + + +
+
+@endsection diff --git a/resources/views/layouts/guest.blade.php b/resources/views/layouts/guest.blade.php index 6c63ee9..0a84d32 100644 --- a/resources/views/layouts/guest.blade.php +++ b/resources/views/layouts/guest.blade.php @@ -26,11 +26,11 @@
- @if(! in_array(Route::currentRouteName(), ['subscribe-success', 'subscribe-fail'])) + @if($showSubscribeForm ?? false) @include('shared.subscribe-form') @endif
diff --git a/resources/views/mail/articles/published.blade.php b/resources/views/mail/articles/published.blade.php new file mode 100644 index 0000000..56d55f9 --- /dev/null +++ b/resources/views/mail/articles/published.blade.php @@ -0,0 +1,26 @@ + +# {{ $article->title }} + +## {{ $article->subtitle }} + +Author: {{ $article?->author?->display_name }} + + +Read in Browser + + +
+ + +{!! $article->body !!} + + +
+ +{{ config('blog.publishing_name') }} + + +Unsubscribe + + +
diff --git a/resources/views/vendor/mail/html/button.blade.php b/resources/views/vendor/mail/html/button.blade.php new file mode 100644 index 0000000..4a9bf7d --- /dev/null +++ b/resources/views/vendor/mail/html/button.blade.php @@ -0,0 +1,24 @@ +@props([ + 'url', + 'color' => 'primary', + 'align' => 'center', +]) + + + + + diff --git a/resources/views/vendor/mail/html/footer.blade.php b/resources/views/vendor/mail/html/footer.blade.php new file mode 100644 index 0000000..3ff41f8 --- /dev/null +++ b/resources/views/vendor/mail/html/footer.blade.php @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/resources/views/vendor/mail/html/header.blade.php b/resources/views/vendor/mail/html/header.blade.php new file mode 100644 index 0000000..56197f8 --- /dev/null +++ b/resources/views/vendor/mail/html/header.blade.php @@ -0,0 +1,12 @@ +@props(['url']) + + + +@if (trim($slot) === 'Laravel') + +@else +{{ $slot }} +@endif + + + diff --git a/resources/views/vendor/mail/html/layout.blade.php b/resources/views/vendor/mail/html/layout.blade.php new file mode 100644 index 0000000..4240d51 --- /dev/null +++ b/resources/views/vendor/mail/html/layout.blade.php @@ -0,0 +1,57 @@ + + + +{{ config('blog.publishing_name') }} + + + + + + + + + + + + + + + diff --git a/resources/views/vendor/mail/html/message.blade.php b/resources/views/vendor/mail/html/message.blade.php new file mode 100644 index 0000000..67cce6a --- /dev/null +++ b/resources/views/vendor/mail/html/message.blade.php @@ -0,0 +1,27 @@ + +{{-- Header --}} + + +{{ config('blog.publishing_name') }} + + + +{{-- Body --}} +{{ $slot }} + +{{-- Subcopy --}} +@isset($subcopy) + + +{{ $subcopy }} + + +@endisset + +{{-- Footer --}} + + +© {{ date('Y') }} {{ config('blog.publishing_name') }}. @lang('All rights reserved.') + + + diff --git a/resources/views/vendor/mail/html/panel.blade.php b/resources/views/vendor/mail/html/panel.blade.php new file mode 100644 index 0000000..2975a60 --- /dev/null +++ b/resources/views/vendor/mail/html/panel.blade.php @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/views/vendor/mail/html/subcopy.blade.php b/resources/views/vendor/mail/html/subcopy.blade.php new file mode 100644 index 0000000..790ce6c --- /dev/null +++ b/resources/views/vendor/mail/html/subcopy.blade.php @@ -0,0 +1,7 @@ + + + + + diff --git a/resources/views/vendor/mail/html/table.blade.php b/resources/views/vendor/mail/html/table.blade.php new file mode 100644 index 0000000..a5f3348 --- /dev/null +++ b/resources/views/vendor/mail/html/table.blade.php @@ -0,0 +1,3 @@ +
+{{ Illuminate\Mail\Markdown::parse($slot) }} +
diff --git a/resources/views/vendor/mail/html/themes/default.css b/resources/views/vendor/mail/html/themes/default.css new file mode 100644 index 0000000..377721c --- /dev/null +++ b/resources/views/vendor/mail/html/themes/default.css @@ -0,0 +1,297 @@ +/* Base */ + +body, +body *:not(html):not(style):not(br):not(tr):not(code) { + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + position: relative; +} + +body { + -webkit-text-size-adjust: none; + background-color: #ffffff; + color: #718096; + height: 100%; + line-height: 1.4; + margin: 0; + padding: 0; + width: 100% !important; +} + +p, +ul, +ol, +blockquote { + line-height: 1.4; + text-align: left; +} + +a { + color: #3869d4; +} + +a img { + border: none; +} + +/* Typography */ + +h1 { + color: #3d4852; + font-size: 18px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h2 { + font-size: 16px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h3 { + font-size: 14px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +p { + font-size: 16px; + line-height: 1.5em; + margin-top: 0; + text-align: left; +} + +p.sub { + font-size: 12px; +} + +img { + max-width: 100%; +} + +/* Layout */ + +.wrapper { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #edf2f7; + margin: 0; + padding: 0; + width: 100%; +} + +.content { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +/* Header */ + +.header { + padding: 25px 0; + text-align: center; +} + +.header a { + color: #550B73; + font-size: 19px; + font-weight: bold; + text-decoration: none; +} + +/* Logo */ + +.logo { + height: 75px; + max-height: 75px; + width: 75px; +} + +/* Body */ + +.body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #edf2f7; + border-bottom: 1px solid #edf2f7; + border-top: 1px solid #edf2f7; + margin: 0; + padding: 0; + width: 100%; +} + +.inner-body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + background-color: #ffffff; + border-color: #e8e5ef; + border-radius: 2px; + border-width: 1px; + box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015); + margin: 0 auto; + padding: 0; + width: 570px; +} + +/* Subcopy */ + +.subcopy { + border-top: 1px solid #e8e5ef; + margin-top: 25px; + padding-top: 25px; +} + +.subcopy p { + font-size: 14px; +} + +/* Footer */ + +.footer { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + margin: 0 auto; + padding: 0; + text-align: center; + width: 570px; +} + +.footer p { + color: #b0adc5; + font-size: 12px; + text-align: center; +} + +.footer a { + color: #b0adc5; + text-decoration: underline; +} + +/* Tables */ + +.table table { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + width: 100%; +} + +.table th { + border-bottom: 1px solid #edeff2; + margin: 0; + padding-bottom: 8px; +} + +.table td { + color: #74787e; + font-size: 15px; + line-height: 18px; + margin: 0; + padding: 10px 0; +} + +.content-cell { + max-width: 100vw; + padding: 32px; +} + +/* Buttons */ + +.action { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + padding: 0; + text-align: center; + width: 100%; +} + +.button { + -webkit-text-size-adjust: none; + border-radius: 4px; + color: #fff; + display: inline-block; + overflow: hidden; + text-decoration: none; +} + +.button-primary { + background-color: #550B73; + border-bottom: 8px solid #550B73; + border-left: 18px solid #550B73; + border-right: 18px solid #550B73; + border-top: 8px solid #550B73; +} + +.button-blue { + background-color: #2d3748; + border-bottom: 8px solid #2d3748; + border-left: 18px solid #2d3748; + border-right: 18px solid #2d3748; + border-top: 8px solid #2d3748; +} + +.button-green, +.button-success { + background-color: #48bb78; + border-bottom: 8px solid #48bb78; + border-left: 18px solid #48bb78; + border-right: 18px solid #48bb78; + border-top: 8px solid #48bb78; +} + +.button-red, +.button-error { + background-color: #e53e3e; + border-bottom: 8px solid #e53e3e; + border-left: 18px solid #e53e3e; + border-right: 18px solid #e53e3e; + border-top: 8px solid #e53e3e; +} + +/* Panels */ + +.panel { + border-left: #2d3748 solid 4px; + margin: 21px 0; +} + +.panel-content { + background-color: #edf2f7; + color: #718096; + padding: 16px; +} + +.panel-content p { + color: #718096; +} + +.panel-item { + padding: 0; +} + +.panel-item p:last-of-type { + margin-bottom: 0; + padding-bottom: 0; +} + +/* Utilities */ + +.break-all { + word-break: break-all; +} diff --git a/resources/views/vendor/mail/text/button.blade.php b/resources/views/vendor/mail/text/button.blade.php new file mode 100644 index 0000000..97444eb --- /dev/null +++ b/resources/views/vendor/mail/text/button.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/resources/views/vendor/mail/text/footer.blade.php b/resources/views/vendor/mail/text/footer.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/footer.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/header.blade.php b/resources/views/vendor/mail/text/header.blade.php new file mode 100644 index 0000000..aaa3e57 --- /dev/null +++ b/resources/views/vendor/mail/text/header.blade.php @@ -0,0 +1 @@ +[{{ $slot }}]({{ $url }}) diff --git a/resources/views/vendor/mail/text/layout.blade.php b/resources/views/vendor/mail/text/layout.blade.php new file mode 100644 index 0000000..ec58e83 --- /dev/null +++ b/resources/views/vendor/mail/text/layout.blade.php @@ -0,0 +1,9 @@ +{!! strip_tags($header ?? '') !!} + +{!! strip_tags($slot) !!} +@isset($subcopy) + +{!! strip_tags($subcopy) !!} +@endisset + +{!! strip_tags($footer ?? '') !!} diff --git a/resources/views/vendor/mail/text/message.blade.php b/resources/views/vendor/mail/text/message.blade.php new file mode 100644 index 0000000..78f82ed --- /dev/null +++ b/resources/views/vendor/mail/text/message.blade.php @@ -0,0 +1,27 @@ + + {{-- Header --}} + + + {{ config('blog.publishing_name') }} + + + + {{-- Body --}} + {{ $slot }} + + {{-- Subcopy --}} + @isset($subcopy) + + + {{ $subcopy }} + + + @endisset + + {{-- Footer --}} + + + © {{ date('Y') }} {{ config('blog.publishing_name') }}. @lang('All rights reserved.') + + + diff --git a/resources/views/vendor/mail/text/panel.blade.php b/resources/views/vendor/mail/text/panel.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/panel.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/subcopy.blade.php b/resources/views/vendor/mail/text/subcopy.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/subcopy.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/table.blade.php b/resources/views/vendor/mail/text/table.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/table.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/writer/dashboard.blade.php b/resources/views/writer/dashboard.blade.php index 6b282a8..bddfa80 100644 --- a/resources/views/writer/dashboard.blade.php +++ b/resources/views/writer/dashboard.blade.php @@ -5,6 +5,54 @@ @section('content')

Writer's Dashboard

- + +
+ +

All Articles

+ + + + + + + + + + + + + + + + + @forelse($articles as $article) + + + + + + + + + + + + @empty + + @endforelse + +
IDSlugTitleSubheadAuthorPublished AtLast UpdatedFirst CreatedActions
{{ $article->id }}{{ $article?->slug ?? '-' }}{{ $article?->title ?? '-' }}{{ $article?->subtitle ?? '-' }}{{ $article?->author?->display_name ?? '-' }}{{ $article?->published_at ?? '-' }}{{ $article?->updated_at ?? '-' }}{{ $article?->created_at ?? '-' }} + {{-- + + + + --}} + + + +
No Articles
+
@endsection diff --git a/resources/views/writer/login.blade.php b/resources/views/writer/login.blade.php index 0e6bd44..cbcbb6f 100644 --- a/resources/views/writer/login.blade.php +++ b/resources/views/writer/login.blade.php @@ -3,7 +3,7 @@ @section('title', 'Writer Login') @section('content') -
+

Writer Login

diff --git a/routes/web.php b/routes/web.php index 4a8c49e..0a2ac7f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -19,7 +19,7 @@ use Illuminate\Support\Facades\Route; Route::get('/', function () { $articles = Article::with(['author'])->where('published_at', '<', now())->latest()->limit(5)->get(); - return view('guest.home', ['articles' => $articles]); + return view('guest.home', ['articles' => $articles, 'showSubscribeForm' => true]); }); Route::get('/test-insert', function () { @@ -28,7 +28,7 @@ Route::get('/test-insert', function () { Route::get('/all-articles', function () { $all_articles = Article::where('published_at', '<', now())->paginate(25); - return view('guest.all-articles', ['all_articles' => $all_articles]); + return view('guest.all-articles', ['all_articles' => $all_articles, 'showSubscribeForm' => true]); })->name('all-articles'); Route::get('/post/{article_slug}', function ($article_slug) { @@ -40,7 +40,7 @@ Route::get('/post/{article_slug}', function ($article_slug) { if(empty($active_article)) { return abort(404); } - return view('guest.article', ['article' => $active_article]); + return view('guest.article', ['article' => $active_article, 'showSubscribeForm' => true]); })->name('article'); Route::get('/writer/login', function () { @@ -57,6 +57,27 @@ Route::get('/subscribe-fail', function() { return view('guest.subscribe-fail'); })->name('subscribe-fail'); +Route::get('/unsubscribe', function(Request $request) { + return view('guest.unsubscribe', ['email' => $request->query('email')]); +})->name('unsubscribe'); + +Route::post('/unsubscribe', [ReaderController::class, 'unsubscribe'])->name('unsubscribe-save'); + +Route::get('/unsubscribe-success', function() { + return view('guest.unsubscribe-success'); +})->name('unsubscribe-success'); + +Route::get('/unsubscribe-fail', function() { + return view('guest.unsubscribe-fail'); +})->name('unsubscribe-fail'); + + +/** + * ---------------- + * WRITER ENDPOINTS + * ---------------- + */ + Route::post('/writer/authenticate', [WriterController::class, 'authenticate'])->name('authenticate'); Route::middleware('auth')->group(function () {