diff --git a/.env.example b/.env.example index fa2dcc0..638b883 100644 --- a/.env.example +++ b/.env.example @@ -25,17 +25,3 @@ MAIL_PASSWORD=null MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="${APP_NAME}" - -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 -AWS_BUCKET= -AWS_USE_PATH_STYLE_ENDPOINT=false - -PUSHER_APP_ID= -PUSHER_APP_KEY= -PUSHER_APP_SECRET= -PUSHER_HOST= -PUSHER_PORT=443 -PUSHER_SCHEME=https -PUSHER_APP_CLUSTER=mt1 diff --git a/app/Http/Controllers/ReaderController.php b/app/Http/Controllers/ReaderController.php new file mode 100644 index 0000000..a468ffa --- /dev/null +++ b/app/Http/Controllers/ReaderController.php @@ -0,0 +1,25 @@ +validate([ + 'email' => 'required|email|max:512', + ]); + + if($validated['email']) { + Subscriber::firstOrCreate(['email' => $validated['email']]); + return redirect(route('subscribe-success')); + } else { + return redirect(route('subscribe-fail')); + } + } +} diff --git a/app/Models/Article.php b/app/Models/Article.php index c1d2786..a140fdf 100644 --- a/app/Models/Article.php +++ b/app/Models/Article.php @@ -2,10 +2,12 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Support\Carbon; class Article extends Model { @@ -27,12 +29,34 @@ class Article extends Model 'processed_at', ]; + /** --------------------------------------- + * ACCESSORS + * --------------------------------------- + */ + + protected function publishedAt(): Attribute + { + return Attribute::make( + get: fn (string $value) => Carbon::parse($value)->format('D, M jS o, g:i A'), + ); + } + + /** --------------------------------------- + * RELATIONSHIPS + * --------------------------------------- + */ + /** The person that wrote the article. */ public function author(): BelongsTo { return $this->belongsTo(Author::class); } + /** --------------------------------------- + * MODEL METHODS + * --------------------------------------- + */ + public function convertMarkdownToMarkup() { if (is_null($this->markdown_body)) return null; diff --git a/app/Models/Subscriber.php b/app/Models/Subscriber.php new file mode 100644 index 0000000..bb7200a --- /dev/null +++ b/app/Models/Subscriber.php @@ -0,0 +1,26 @@ + env('APP_NAME', 'Laravel'), + 'name' => env('APP_NAME', 'Word Salad'), /* |-------------------------------------------------------------------------- @@ -70,7 +70,7 @@ return [ | */ - 'timezone' => 'UTC', + 'timezone' => 'America/Denver', /* |-------------------------------------------------------------------------- diff --git a/database/factories/ArticleFactory.php b/database/factories/ArticleFactory.php index 0bac576..00dc89b 100644 --- a/database/factories/ArticleFactory.php +++ b/database/factories/ArticleFactory.php @@ -42,13 +42,13 @@ class ArticleFactory extends Factory public function definition(): array { return [ - 'title' => fake()->words(3, true), + 'title' => $this->getRandomTitle(), 'slug' => fake()->unique()->slug(), 'author_id' => static::$author->id ??= Author::factory()->create()->id, - 'subtitle' => fake()->sentence(), + 'subtitle' => $this->getRandomSentence(), 'body' => '', // We'll update it after making using the on-Model method 'published_at' => $this->generateRandomPublishDate(), - 'header_image_url' => fake()->imageUrl(), + 'header_image_url' => null, 'markdown_body' => $this->generateMarkdown(), 'processed_at' => fake()->date('Y-m-d H:m:s'), ]; @@ -98,4 +98,60 @@ class ArticleFactory extends Factory return now()->addDays(rand(0,50)); } } + + private function getRandomTitle(): string + { + $possibilities = [ + 'What\'s in a Title? Hopefully More Than This', + 'This One Will Leave You Speechless', + 'Finally, The Pizza of Your Dreams', + 'We Came, We Saw, and Then...', + 'The Incomprehensibility of Aluminum', + 'You Merely Adopted this Format, I Was Born In It, Molded by It', + 'Vector Math is for Cowards', + 'When Two Packets Love Each Other Very Much', + 'I\'m Slipping, I\'m Sliding (I\'m on a Slip \'n Slide)', + 'Tips On Giving Up (Maximum Efficiency)', + 'Top 10 Reasons to Give Up Completely', + 'Even the Monsters Are Disappointed', + ]; + + return $possibilities[random_int(0, count($possibilities) - 1)]; + } + + private function getRandomSentence(): string + { + $possibilities = [ + 'That page would not bring the reward that was promised.', + 'However, his tendency toward love was endearing, I miss that most of all.', + 'The jellyfish doesn\'t care what you think, that\'s hubris.', + 'And yet.', + 'I find it distasteful, the smell lingering in the air for weeks and months while officials dragged their feet.', + 'I think it was down this hill, maybe.', + 'Cold eyes, I never liked them.', + 'You can call me fit, or the love of your life, if you want.', + 'The kei truck proves its superiority yet again in the quarter-mile.', + 'He knocked, but heard nothing.', + 'First base!', + 'But these histories are still lost to us, despite our vast technical advances.', + 'In the sincerest way possible, I am begging you to start the fire.', + 'They drifted up high over head and then sank reluctantly.', + 'I\'m so proud of you, no matter how belated!', + 'She gripped the handle, but didn\'t have the heart to turn it.', + 'If this, then that, so to speak.', + 'Mapping the source column onto the target column should be strictly validated to ensure no missing fields.', + 'Would there be something in there, though?', + 'The wood splintering echoed down the hall.', + 'I simply do not understand.', + 'The radio tower nearly scraped the belly of the bomber as it strained against the immense pressure.', + 'What\'s actually left after that?', + 'Yes, but it needs more spice.', + 'Backtracking is good, actually.', + 'Scan has always been the problem.', + 'Smell first, then you can think about licking.', + ]; + + return $possibilities[random_int(0, count($possibilities) - 1)]; + + } } diff --git a/database/factories/SubscriberFactory.php b/database/factories/SubscriberFactory.php new file mode 100644 index 0000000..7eb7bc4 --- /dev/null +++ b/database/factories/SubscriberFactory.php @@ -0,0 +1,23 @@ + + */ +class SubscriberFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'email' => fake()->safeEmail(), + ]; + } +} diff --git a/database/migrations/2025_10_06_194310_make_subscribers_table.php b/database/migrations/2025_10_06_194310_make_subscribers_table.php new file mode 100644 index 0000000..0e03e12 --- /dev/null +++ b/database/migrations/2025_10_06_194310_make_subscribers_table.php @@ -0,0 +1,27 @@ +id(); + $table->string('email', 512)->unique(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('subscribers'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 2ec62de..66eb989 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -5,6 +5,7 @@ namespace Database\Seeders; use App\Models\User; use App\Models\Author; use App\Models\Article; +use App\Models\Subscriber; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Hash; @@ -23,7 +24,7 @@ class DatabaseSeeder extends Seeder $test_author = Author::factory()->user($test_user)->create(); - Article::factory()->author($test_author)->count(30)->create(); + Article::factory()->author($test_author)->count(50)->create(); $random_article = Article::where('id', '<', 10)->inRandomOrder()->first(); @@ -31,5 +32,7 @@ class DatabaseSeeder extends Seeder alert('oh shit. XSS might be possible here!') "; $random_article->save(); + + Subscriber::factory()->count(10)->create(); } } diff --git a/public/word-salad.css b/public/word-salad.css index 1a4fe85..f176d8d 100644 --- a/public/word-salad.css +++ b/public/word-salad.css @@ -3,9 +3,26 @@ } body { - background-color: #813d9c; /* Primary Purple */ + /* background-color: #813d9c; /* Old Purple */ + background-color: #2d033e; /* New Purple */ color: white; text-shadow: 1px 1px 2px blueviolet; + margin: 0; +} + +main { + display: flex; + justify-content: center; +} + +article { + max-width: 60vw; +} + +@media screen and (max-width: 1250px) { + article { + max-width: 90vw; + } } a { @@ -14,19 +31,71 @@ a { } #top-bar { - padding: 0.5em; - background-color: rgba(red, green, blue, 0.2); + padding: 0.5em 0em; + background-color: #ffffff3d; border-bottom: 1px solid white; display: flex; align-items: center; justify-content: center; + font-size: smaller; +} + +nav { + display: flex; + justify-content: space-between; +} + +#nav-logo > a { + text-decoration: none; + color: #ff9344; + font-size: x-large; + font-weight: bold; + margin: 0.3em; +} + +#article-header-separator { + width: 60%; + color: white; + margin: 2em auto; } -.bubble-panel { +footer { + min-height: 7em; + display: flex; + justify-content: center; +} + +#subscribe-form { + border-radius: 1.5em; + border-color: white; + border-style: solid; + border-width: 1px; + padding: 1.4em; + margin-top: 1.5em; + margin-bottom: 2em; +} + +#subscribe-form > form { + display: flex; + justify-content: center; + gap: 10px; +} + +#subscribe-form .email-input { + display: flex; + flex-direction: column; +} + +#subscribe-button { + background-color: #ff9344; + color: white; +} + +/* .bubble-panel { border-radius: 50px; background: #813d9c; box-shadow: -33px 33px 65px #66307b, 33px -33px 65px #9c4abd; padding: 2em; -} \ No newline at end of file +}*/ diff --git a/resources/views/guest/all-articles.blade.php b/resources/views/guest/all-articles.blade.php index e5048db..28076f0 100644 --- a/resources/views/guest/all-articles.blade.php +++ b/resources/views/guest/all-articles.blade.php @@ -10,5 +10,7 @@ @empty

No articles found, unlucky.

@endforelse + + {{ $all_articles->links() }} @endsection diff --git a/resources/views/guest/home.blade.php b/resources/views/guest/home.blade.php index bf1ad70..0bb78b9 100644 --- a/resources/views/guest/home.blade.php +++ b/resources/views/guest/home.blade.php @@ -8,19 +8,28 @@

{{ config('blog.subheading') }}

- @forelse($articles as $article) - - @empty + @if(count($articles) > 0) + @foreach($articles as $article) + + @endforeach + {{-- +
+ + See All Articles + +
+ --}} + @else
No articles published yet. Check back soon!
- @endforelse + @endif
@endsection diff --git a/resources/views/guest/main-nav.blade.php b/resources/views/guest/main-nav.blade.php new file mode 100644 index 0000000..cdc1a8b --- /dev/null +++ b/resources/views/guest/main-nav.blade.php @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/resources/views/guest/subscribe-fail.blade.php b/resources/views/guest/subscribe-fail.blade.php new file mode 100644 index 0000000..23f0ddc --- /dev/null +++ b/resources/views/guest/subscribe-fail.blade.php @@ -0,0 +1,10 @@ +@extends('layouts.guest') + +@section('title', 'Subscription Failed') + +@section('content') +
+

Subscription Error

+

Something went wrong while marking your interest. Sorry.

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

Subscription Confirmed

+

You'll be the first to know about future posts. Thank you.

+
+@endsection diff --git a/resources/views/layouts/guest.blade.php b/resources/views/layouts/guest.blade.php index 562f2c0..2422b17 100644 --- a/resources/views/layouts/guest.blade.php +++ b/resources/views/layouts/guest.blade.php @@ -9,7 +9,11 @@ {{ config('blog.publishing_name') }} | @yield('title') - @include('guest.top-bar') +
+ @include('guest.top-bar') + + @include('guest.main-nav') +
@foreach($errors->all() as $error) @@ -21,4 +25,9 @@ @yield('content', 'Nothing to see here.') + @if(! in_array(Route::currentRouteName(), ['subscribe-success', 'subscribe-fail'])) +
+ @include('shared.subscribe-form') +
+ @endif diff --git a/resources/views/shared/single-article.blade.php b/resources/views/shared/single-article.blade.php index 53832c2..bf0d8c8 100644 --- a/resources/views/shared/single-article.blade.php +++ b/resources/views/shared/single-article.blade.php @@ -1,16 +1,22 @@
-   ← Go Back - + @if($article['header_image_url']) + + @endif

{{ $article['title'] }}

{{ $article['subtitle'] }}

-

{{ $article['author']['display_name'] }}

-

Published At: {{ $article['published_at'] }}

+

Written By: {{ $article['author']['display_name'] }}

+

Published: {{ $article['published_at'] }}

+ {{-- -
+ --}} + +
+ +
{!! $article['body'] !!} -
+
diff --git a/resources/views/shared/subscribe-form.blade.php b/resources/views/shared/subscribe-form.blade.php new file mode 100644 index 0000000..3337f0e --- /dev/null +++ b/resources/views/shared/subscribe-form.blade.php @@ -0,0 +1,10 @@ +
+
+ @csrf + + +
+
diff --git a/routes/web.php b/routes/web.php index 79baef6..701fe26 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,9 @@ name('test-insert'); -Route::get('/recent-articles', function () { - $all_articles = Article::where('published_at', '<', now())->limit(10)->get(); +Route::get('/all-articles', function () { + $all_articles = Article::where('published_at', '<', now())->paginate(25); return view('guest.all-articles', ['all_articles' => $all_articles]); -})->name('recent-articles'); +})->name('all-articles'); Route::get('/post/{article_slug}', function ($article_slug) { $active_article = Article::with('author') @@ -45,6 +47,16 @@ Route::get('/writer/login', function () { return view('writer.login'); })->name('login'); +Route::post('/subscribe', [ReaderController::class, 'subscribe'])->name('subscribe'); + +Route::get('/subscribe-success', function() { + return view('guest.subscribe-success'); +})->name('subscribe-success'); + +Route::get('/subscribe-fail', function() { + return view('guest.subscribe-fail'); +})->name('subscribe-fail'); + Route::post('/writer/authenticate', [WriterController::class, 'authenticate'])->name('authenticate'); Route::middleware('auth')->group(function () {