fuck: IDK, man. Lots of good stuff. Mostly styling. Also Subs!
This commit is contained in:
parent
c8aae69ffb
commit
0e3902d16b
14
.env.example
14
.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
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
];
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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)];
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
|
|
@ -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!')
|
||||
</script>";
|
||||
$random_article->save();
|
||||
|
||||
Subscriber::factory()->count(10)->create();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}*/
|
||||
|
|
|
|||
|
|
@ -10,5 +10,7 @@
|
|||
@empty
|
||||
<h4>No articles found, unlucky.</h4>
|
||||
@endforelse
|
||||
|
||||
{{ $all_articles->links() }}
|
||||
</div>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
<p><i>{{ config('blog.subheading') }}</i></p>
|
||||
</section>
|
||||
<section>
|
||||
@forelse($articles as $article)
|
||||
@if(count($articles) > 0)
|
||||
@foreach($articles as $article)
|
||||
<article>
|
||||
<h3>
|
||||
<a href="{{ route('article', ['article_slug' => $article->slug]) }}">
|
||||
|
|
@ -19,8 +20,16 @@
|
|||
{{ $article->subtitle }}
|
||||
</h4>
|
||||
</article>
|
||||
@empty
|
||||
@endforeach
|
||||
{{--
|
||||
<section>
|
||||
<a href="{{ route('all-articles') }}">
|
||||
See All Articles
|
||||
</a>
|
||||
</section>
|
||||
--}}
|
||||
@else
|
||||
<div>No articles published yet. Check back soon!</div>
|
||||
@endforelse
|
||||
@endif
|
||||
</section>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -9,8 +9,12 @@
|
|||
<title>{{ config('blog.publishing_name') }} | @yield('title')</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
@include('guest.top-bar')
|
||||
|
||||
@include('guest.main-nav')
|
||||
</header>
|
||||
|
||||
<section id="error-bag">
|
||||
@foreach($errors->all() as $error)
|
||||
{{ $error }}
|
||||
|
|
@ -21,4 +25,9 @@
|
|||
@yield('content', 'Nothing to see here.')
|
||||
</main>
|
||||
</body>
|
||||
@if(! in_array(Route::currentRouteName(), ['subscribe-success', 'subscribe-fail']))
|
||||
<footer>
|
||||
@include('shared.subscribe-form')
|
||||
</footer>
|
||||
@endif
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,22 @@
|
|||
<article>
|
||||
<a href="{{ url()->previous() }}"> ← Go Back</a>
|
||||
@if($article['header_image_url'])
|
||||
<img src="{{ $article['header_image_url'] }}" />
|
||||
@endif
|
||||
<h1>{{ $article['title'] }}</h1>
|
||||
<h2>{{ $article['subtitle'] }}</h2>
|
||||
<p>{{ $article['author']['display_name'] }}</p>
|
||||
<p>Published At: {{ $article['published_at'] }}</p>
|
||||
<p>Written By: {{ $article['author']['display_name'] }}</p>
|
||||
<p>Published: {{ $article['published_at'] }}</p>
|
||||
{{--
|
||||
<ul>
|
||||
<li><a href="{{ $article['primary_link'] }}">Link 1</a></li>
|
||||
<li><a href="{{ $article['secondary_link'] }}">Link 2</a></li>
|
||||
<li><a href="{{ $article['mastodon_handle'] }}">Mastodon</a></li>
|
||||
</ul>
|
||||
<main>
|
||||
--}}
|
||||
|
||||
<hr id="article-header-separator" />
|
||||
|
||||
<section>
|
||||
{!! $article['body'] !!}
|
||||
</main>
|
||||
</section>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\ReaderController;
|
||||
use App\Http\Controllers\WriterController;
|
||||
use App\Models\Article;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
|
|
@ -24,10 +26,10 @@ Route::get('/test-insert', function () {
|
|||
return view('test-insert');
|
||||
})->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 () {
|
||||
|
|
|
|||
Loading…
Reference in New Issue