fuck: IDK, man. Lots of good stuff. Mostly styling. Also Subs!

This commit is contained in:
Jonathan Nielson 2025-10-06 22:10:25 -05:00
parent c8aae69ffb
commit 0e3902d16b
19 changed files with 366 additions and 48 deletions

View File

@ -25,17 +25,3 @@ MAIL_PASSWORD=null
MAIL_ENCRYPTION=null MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}" 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

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers;
use App\Models\Subscriber;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
class ReaderController extends Controller
{
public function subscribe(Request $request): RedirectResponse
{
// Validate email
$validated = $request->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'));
}
}
}

View File

@ -2,10 +2,12 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Carbon;
class Article extends Model class Article extends Model
{ {
@ -27,12 +29,34 @@ class Article extends Model
'processed_at', '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. */ /** The person that wrote the article. */
public function author(): BelongsTo public function author(): BelongsTo
{ {
return $this->belongsTo(Author::class); return $this->belongsTo(Author::class);
} }
/** ---------------------------------------
* MODEL METHODS
* ---------------------------------------
*/
public function convertMarkdownToMarkup() public function convertMarkdownToMarkup()
{ {
if (is_null($this->markdown_body)) return null; if (is_null($this->markdown_body)) return null;

26
app/Models/Subscriber.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class Subscriber extends Model
{
use HasFactory;
/** The name of the database table storing this Model's records. */
protected $table = 'subscribers';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
/** The attributes on the Model that can be mass-assigned */
protected $fillable = [
'email',
];
}

View File

@ -16,7 +16,7 @@ return [
| |
*/ */
'name' => env('APP_NAME', 'Laravel'), 'name' => env('APP_NAME', 'Word Salad'),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -70,7 +70,7 @@ return [
| |
*/ */
'timezone' => 'UTC', 'timezone' => 'America/Denver',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -42,13 +42,13 @@ class ArticleFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'title' => fake()->words(3, true), 'title' => $this->getRandomTitle(),
'slug' => fake()->unique()->slug(), 'slug' => fake()->unique()->slug(),
'author_id' => static::$author->id ??= Author::factory()->create()->id, '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 'body' => '', // We'll update it after making using the on-Model method
'published_at' => $this->generateRandomPublishDate(), 'published_at' => $this->generateRandomPublishDate(),
'header_image_url' => fake()->imageUrl(), 'header_image_url' => null,
'markdown_body' => $this->generateMarkdown(), 'markdown_body' => $this->generateMarkdown(),
'processed_at' => fake()->date('Y-m-d H:m:s'), 'processed_at' => fake()->date('Y-m-d H:m:s'),
]; ];
@ -98,4 +98,60 @@ class ArticleFactory extends Factory
return now()->addDays(rand(0,50)); 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)];
}
} }

View File

@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Subscriber>
*/
class SubscriberFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'email' => fake()->safeEmail(),
];
}
}

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('subscribers', function (Blueprint $table) {
$table->id();
$table->string('email', 512)->unique();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('subscribers');
}
};

View File

@ -5,6 +5,7 @@ namespace Database\Seeders;
use App\Models\User; use App\Models\User;
use App\Models\Author; use App\Models\Author;
use App\Models\Article; use App\Models\Article;
use App\Models\Subscriber;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
@ -23,7 +24,7 @@ class DatabaseSeeder extends Seeder
$test_author = Author::factory()->user($test_user)->create(); $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(); $random_article = Article::where('id', '<', 10)->inRandomOrder()->first();
@ -31,5 +32,7 @@ class DatabaseSeeder extends Seeder
alert('oh shit. XSS might be possible here!') alert('oh shit. XSS might be possible here!')
</script>"; </script>";
$random_article->save(); $random_article->save();
Subscriber::factory()->count(10)->create();
} }
} }

View File

@ -3,9 +3,26 @@
} }
body { body {
background-color: #813d9c; /* Primary Purple */ /* background-color: #813d9c; /* Old Purple */
background-color: #2d033e; /* New Purple */
color: white; color: white;
text-shadow: 1px 1px 2px blueviolet; 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 { a {
@ -14,19 +31,71 @@ a {
} }
#top-bar { #top-bar {
padding: 0.5em; padding: 0.5em 0em;
background-color: rgba(red, green, blue, 0.2); background-color: #ffffff3d;
border-bottom: 1px solid white; border-bottom: 1px solid white;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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; border-radius: 50px;
background: #813d9c; background: #813d9c;
box-shadow: -33px 33px 65px #66307b, box-shadow: -33px 33px 65px #66307b,
33px -33px 65px #9c4abd; 33px -33px 65px #9c4abd;
padding: 2em; padding: 2em;
} }*/

View File

@ -10,5 +10,7 @@
@empty @empty
<h4>No articles found, unlucky.</h4> <h4>No articles found, unlucky.</h4>
@endforelse @endforelse
{{ $all_articles->links() }}
</div> </div>
@endsection @endsection

View File

@ -8,19 +8,28 @@
<p><i>{{ config('blog.subheading') }}</i></p> <p><i>{{ config('blog.subheading') }}</i></p>
</section> </section>
<section> <section>
@forelse($articles as $article) @if(count($articles) > 0)
<article> @foreach($articles as $article)
<h3> <article>
<a href="{{ route('article', ['article_slug' => $article->slug]) }}"> <h3>
{{ $article->title }} <a href="{{ route('article', ['article_slug' => $article->slug]) }}">
</a> {{ $article->title }}
</h3> </a>
<h4> </h3>
{{ $article->subtitle }} <h4>
</h4> {{ $article->subtitle }}
</article> </h4>
@empty </article>
@endforeach
{{--
<section>
<a href="{{ route('all-articles') }}">
See All Articles
</a>
</section>
--}}
@else
<div>No articles published yet. Check back soon!</div> <div>No articles published yet. Check back soon!</div>
@endforelse @endif
</section> </section>
@endsection @endsection

View File

@ -0,0 +1,11 @@
<nav id="main-nav">
<div id="nav-logo"><a href="/">{{ config('app.name') }}</a></div>
<div>
{{--
<form action="{{route('search')}}">
<input type="search" id="site-search" name="q" />
<button>Search</button>
</form>
--}}
</div>
</nav>

View File

@ -0,0 +1,10 @@
@extends('layouts.guest')
@section('title', 'Subscription Failed')
@section('content')
<section>
<h1>Subscription Error</h1>
<p>Something went wrong while marking your interest. Sorry.</p>
</section>
@endsection

View File

@ -0,0 +1,10 @@
@extends('layouts.guest')
@section('title', 'Subscription Confirmed')
@section('content')
<section>
<h1>Subscription Confirmed</h1>
<p>You'll be the first to know about future posts. Thank you.</p>
</section>
@endsection

View File

@ -9,7 +9,11 @@
<title>{{ config('blog.publishing_name') }} | @yield('title')</title> <title>{{ config('blog.publishing_name') }} | @yield('title')</title>
</head> </head>
<body> <body>
@include('guest.top-bar') <header>
@include('guest.top-bar')
@include('guest.main-nav')
</header>
<section id="error-bag"> <section id="error-bag">
@foreach($errors->all() as $error) @foreach($errors->all() as $error)
@ -21,4 +25,9 @@
@yield('content', 'Nothing to see here.') @yield('content', 'Nothing to see here.')
</main> </main>
</body> </body>
@if(! in_array(Route::currentRouteName(), ['subscribe-success', 'subscribe-fail']))
<footer>
@include('shared.subscribe-form')
</footer>
@endif
</html> </html>

View File

@ -1,16 +1,22 @@
<article> <article>
<a href="{{ url()->previous() }}">&nbsp;&nbsp;&larr;&nbsp;Go Back</a> @if($article['header_image_url'])
<img src="{{ $article['header_image_url'] }}" /> <img src="{{ $article['header_image_url'] }}" />
@endif
<h1>{{ $article['title'] }}</h1> <h1>{{ $article['title'] }}</h1>
<h2>{{ $article['subtitle'] }}</h2> <h2>{{ $article['subtitle'] }}</h2>
<p>{{ $article['author']['display_name'] }}</p> <p>Written By: {{ $article['author']['display_name'] }}</p>
<p>Published At: {{ $article['published_at'] }}</p> <p>Published: {{ $article['published_at'] }}</p>
{{--
<ul> <ul>
<li><a href="{{ $article['primary_link'] }}">Link 1</a></li> <li><a href="{{ $article['primary_link'] }}">Link 1</a></li>
<li><a href="{{ $article['secondary_link'] }}">Link 2</a></li> <li><a href="{{ $article['secondary_link'] }}">Link 2</a></li>
<li><a href="{{ $article['mastodon_handle'] }}">Mastodon</a></li> <li><a href="{{ $article['mastodon_handle'] }}">Mastodon</a></li>
</ul> </ul>
<main> --}}
<hr id="article-header-separator" />
<section>
{!! $article['body'] !!} {!! $article['body'] !!}
</main> </section>
</article> </article>

View File

@ -0,0 +1,10 @@
<div id="subscribe-form">
<form action="{{ route('subscribe') }}" method="POST">
@csrf
<div class="email-input">
<label for="email">Email Address</label>
<input type="email" name="email" id="email" required placeholder="coolperson@example.com" />
</div>
<button id="subscribe-button" type="submit">Subscribe</button>
</form>
</div>

View File

@ -1,7 +1,9 @@
<?php <?php
use App\Http\Controllers\ReaderController;
use App\Http\Controllers\WriterController; use App\Http\Controllers\WriterController;
use App\Models\Article; use App\Models\Article;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
/* /*
@ -24,10 +26,10 @@ Route::get('/test-insert', function () {
return view('test-insert'); return view('test-insert');
})->name('test-insert'); })->name('test-insert');
Route::get('/recent-articles', function () { Route::get('/all-articles', function () {
$all_articles = Article::where('published_at', '<', now())->limit(10)->get(); $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]);
})->name('recent-articles'); })->name('all-articles');
Route::get('/post/{article_slug}', function ($article_slug) { Route::get('/post/{article_slug}', function ($article_slug) {
$active_article = Article::with('author') $active_article = Article::with('author')
@ -45,6 +47,16 @@ Route::get('/writer/login', function () {
return view('writer.login'); return view('writer.login');
})->name('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::post('/writer/authenticate', [WriterController::class, 'authenticate'])->name('authenticate');
Route::middleware('auth')->group(function () { Route::middleware('auth')->group(function () {