From a34940d22cfe51a35abc991375a3980e036ccf0b Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 18 Dec 2025 14:44:05 -0600 Subject: [PATCH 1/4] Add NewsStory model and migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/Models/NewsStory.php | 10 ++++++ ...12_18_203202_create_news_stories_table.php | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 app/Models/NewsStory.php create mode 100644 database/migrations/2025_12_18_203202_create_news_stories_table.php diff --git a/app/Models/NewsStory.php b/app/Models/NewsStory.php new file mode 100644 index 0000000..b0a949d --- /dev/null +++ b/app/Models/NewsStory.php @@ -0,0 +1,10 @@ +id(); + $table->string('headline'); + $table->text('body'); + $table->date('start_publication_date')->nullable(); + $table->date('stop_publication_date')->nullable(); + $table->string('active'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('news_stories'); + } +}; From 18a48f5463fbd0ee55f1137f8598d57305e3d4f3 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 19 Dec 2025 09:27:13 -0600 Subject: [PATCH 2/4] Create news story form --- .../Controllers/Admin/NewsStoryController.php | 67 +++++++++++++++++++ app/View/Components/Layout/Admin.php | 4 ++ resources/css/app.css | 1 + resources/views/admin/news/create.blade.php | 48 +++++++++++++ resources/views/admin/news/index.blade.php | 10 +++ .../views/components/form/checkbox.blade.php | 2 +- .../form/radio-group-item.blade.php | 8 +++ .../components/form/radio-group.blade.php | 12 ++++ routes/web.php | 2 + 9 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/Admin/NewsStoryController.php create mode 100644 resources/views/admin/news/create.blade.php create mode 100644 resources/views/admin/news/index.blade.php create mode 100644 resources/views/components/form/radio-group-item.blade.php create mode 100644 resources/views/components/form/radio-group.blade.php diff --git a/app/Http/Controllers/Admin/NewsStoryController.php b/app/Http/Controllers/Admin/NewsStoryController.php new file mode 100644 index 0000000..6dc3ca0 --- /dev/null +++ b/app/Http/Controllers/Admin/NewsStoryController.php @@ -0,0 +1,67 @@ +paginate(15); + return view('admin.news.index', compact('stories')); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + return view('admin.news.create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + // + } + + /** + * Display the specified resource. + */ + public function show(string $id) + { + // + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(string $id) + { + // + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, string $id) + { + // + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(string $id) + { + // + } +} diff --git a/app/View/Components/Layout/Admin.php b/app/View/Components/Layout/Admin.php index b6a8707..0623231 100644 --- a/app/View/Components/Layout/Admin.php +++ b/app/View/Components/Layout/Admin.php @@ -36,6 +36,10 @@ class Admin extends Component 'name' => 'Audition Etudes', 'link' => route('admin.etudes.index'), ], + [ + 'name' => 'News Stories', + 'link' => route('admin.news.index'), + ] ]; } diff --git a/resources/css/app.css b/resources/css/app.css index f5f339c..0a01846 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,6 +1,7 @@ @import 'tailwindcss'; @layer base { + dl dt { @apply font-semibold; } diff --git a/resources/views/admin/news/create.blade.php b/resources/views/admin/news/create.blade.php new file mode 100644 index 0000000..f8d6f72 --- /dev/null +++ b/resources/views/admin/news/create.blade.php @@ -0,0 +1,48 @@ + + + Create News Story + + +
+ +
+
+ +
+
+ + Status + Active + Draft + +
+ +
+ + +
+
+ + +
+
+ Save Story +
+
+
+
+
diff --git a/resources/views/admin/news/index.blade.php b/resources/views/admin/news/index.blade.php new file mode 100644 index 0000000..506dab9 --- /dev/null +++ b/resources/views/admin/news/index.blade.php @@ -0,0 +1,10 @@ + + + News Stories + +
+ New Story +
+
+
+
diff --git a/resources/views/components/form/checkbox.blade.php b/resources/views/components/form/checkbox.blade.php index d8fce9c..327fd6d 100644 --- a/resources/views/components/form/checkbox.blade.php +++ b/resources/views/components/form/checkbox.blade.php @@ -6,7 +6,7 @@
+ {{ $attributes->merge(['class' => 'col-start-1 row-start-1 appearance-none rounded-sm border border-gray-300 bg-white checked:border-brand-600 checked:bg-brand-600 indeterminate:border-brand-600 indeterminate:bg-brand-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 dark:border-white/10 dark:bg-white/5 dark:checked:border-brand-500 dark:checked:bg-brand-500 dark:indeterminate:border-brand-500 dark:indeterminate:bg-brand-500 dark:focus-visible:outline-brand-500 dark:disabled:border-white/5 dark:disabled:bg-white/10 dark:disabled:checked:bg-white/10 forced-colors:appearance-auto']) }}/> + + +
diff --git a/resources/views/components/form/radio-group.blade.php b/resources/views/components/form/radio-group.blade.php new file mode 100644 index 0000000..fef4dd3 --- /dev/null +++ b/resources/views/components/form/radio-group.blade.php @@ -0,0 +1,12 @@ +@props(['label' => null, 'sublabel' => null, 'name']) +
+ @if($label) + {{ $label }} + @endif + @if($sublabel) +

{{ $sublabel }}

+ @endif +
+ {{ $slot }} +
+
diff --git a/routes/web.php b/routes/web.php index bb98881..193840e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,6 +2,7 @@ use App\Http\Controllers\Admin\AuditionEtudeController; use App\Http\Controllers\Admin\DashboardController; +use App\Http\Controllers\Admin\NewsStoryController; use App\Http\Controllers\Admin\SiteDataController; use App\Http\Controllers\Admin\UsersController; use App\Http\Controllers\AuditionInformationPageController; @@ -25,4 +26,5 @@ Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () Route::get('/', [UsersController::class, 'index'])->name('index'); }); Route::resource('/etudes', AuditionEtudeController::class)->names('etudes'); + Route::resource('/news', NewsStoryController::class)->names('news'); }); From b57a2a515a8c150ce293cc14aac287835aebd996 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 19 Dec 2025 12:41:47 -0600 Subject: [PATCH 3/4] Alert component --- resources/views/components/alert.blade.php | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 resources/views/components/alert.blade.php diff --git a/resources/views/components/alert.blade.php b/resources/views/components/alert.blade.php new file mode 100644 index 0000000..e6ab0b5 --- /dev/null +++ b/resources/views/components/alert.blade.php @@ -0,0 +1,38 @@ +@props(['color' => 'blue']) +
+
$color === 'yellow', + 'border-red-400 bg-red-50 dark:border-red-500 dark:bg-red-500/15' => $color === 'red', + 'border-green-400 bg-green-50 dark:border-green-500 dark:bg-green-500/10' => $color === 'green', + 'border-blue-400 bg-blue-50 dark:border-blue-500 dark:bg-blue-500/10' => $color === 'blue', + ])> +
+
+ +
+
+

$color === 'yellow', + 'text-blue-700 dar:text-blue-300' => $color === 'blue', + 'text-red-700 dar:text-red-300' => $color === 'red', + 'text-green-700 dar:text-green-300' => $color === 'green', + ])> + You have no credits left. +

+
+
+
From ecd16e5d41409a1de330068b6d45758ce1860326 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Mon, 22 Dec 2025 22:54:36 -0600 Subject: [PATCH 4/4] Working News Section --- .ai/mcp/mcp.json | 20 ++++++ app/Enums/StoryStatusEnum.php | 31 ++++++++++ .../Controllers/Admin/NewsStoryController.php | 36 ++++++----- app/Http/Controllers/WelcomeController.php | 6 +- app/Http/Requests/NewsStoryRequest.php | 49 +++++++++++++++ app/Models/NewsStory.php | 43 ++++++++++++- database/factories/NewsStoryFactory.php | 25 ++++++++ resources/views/admin/news/create.blade.php | 7 +++ resources/views/admin/news/edit.blade.php | 62 +++++++++++++++++++ resources/views/admin/news/index.blade.php | 39 +++++++++++- resources/views/components/alert.blade.php | 45 +++++++------- .../views/components/badge-pill.blade.php | 18 ++++++ .../views/components/modal-danger.blade.php | 49 +++++++++++++++ resources/views/welcome.blade.php | 23 ++++++- routes/web.php | 2 +- 15 files changed, 410 insertions(+), 45 deletions(-) create mode 100644 .ai/mcp/mcp.json create mode 100644 app/Enums/StoryStatusEnum.php create mode 100644 app/Http/Requests/NewsStoryRequest.php create mode 100644 database/factories/NewsStoryFactory.php create mode 100644 resources/views/admin/news/edit.blade.php create mode 100644 resources/views/components/badge-pill.blade.php create mode 100644 resources/views/components/modal-danger.blade.php diff --git a/.ai/mcp/mcp.json b/.ai/mcp/mcp.json new file mode 100644 index 0000000..592e550 --- /dev/null +++ b/.ai/mcp/mcp.json @@ -0,0 +1,20 @@ +{ + "mcpServers": { + "laravel-boost": { + "command": "/Users/mayoung/Library/Application Support/Herd/bin/php82", + "args": [ + "/Users/mayoung/Herd/meobda-website/artisan", + "boost:mcp" + ] + }, + "herd": { + "command": "/Users/mayoung/Library/Application Support/Herd/bin/php82", + "args": [ + "/Applications/Herd.app/Contents/Resources/herd-mcp.phar" + ], + "env": { + "SITE_PATH": "/Users/mayoung/Herd/meobda-website" + } + } + } +} \ No newline at end of file diff --git a/app/Enums/StoryStatusEnum.php b/app/Enums/StoryStatusEnum.php new file mode 100644 index 0000000..41bffc9 --- /dev/null +++ b/app/Enums/StoryStatusEnum.php @@ -0,0 +1,31 @@ + 'Draft', + self::SCHEDULED => 'Scheduled', + self::PUBLISHED => 'Published', + self::EXPIRED => 'Expired', + }; + } + + public function color(): string + { + return match ($this) { + self::DRAFT => 'yellow', + self::SCHEDULED => 'blue', + self::PUBLISHED => 'green', + self::EXPIRED => 'red', + }; + } +} diff --git a/app/Http/Controllers/Admin/NewsStoryController.php b/app/Http/Controllers/Admin/NewsStoryController.php index 6dc3ca0..ceedf1d 100644 --- a/app/Http/Controllers/Admin/NewsStoryController.php +++ b/app/Http/Controllers/Admin/NewsStoryController.php @@ -3,8 +3,8 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Http\Requests\NewsStoryRequest; use App\Models\NewsStory; -use Illuminate\Http\Request; class NewsStoryController extends Controller { @@ -14,6 +14,7 @@ class NewsStoryController extends Controller public function index() { $stories = NewsStory::orderBy('id', 'desc')->paginate(15); + return view('admin.news.index', compact('stories')); } @@ -28,33 +29,35 @@ class NewsStoryController extends Controller /** * Store a newly created resource in storage. */ - public function store(Request $request) + public function store(NewsStoryRequest $request) { - // + NewsStory::create( + $request->validated() + ); + + return redirect()->route('admin.news.index')->with('success', 'Story Added Successfully'); } - /** - * Display the specified resource. - */ - public function show(string $id) - { - // - } /** * Show the form for editing the specified resource. */ - public function edit(string $id) + public function edit(int $newsStoryID) { - // + $newsStory = NewsStory::findOrFail($newsStoryID); + + return view('admin.news.edit', compact('newsStory')); } /** * Update the specified resource in storage. */ - public function update(Request $request, string $id) + public function update(NewsStoryRequest $request, string $id) { - // + $newsStory = NewsStory::findOrFail($id); + $newsStory->update($request->validated()); + + return redirect()->route('admin.news.index')->with('success', 'Story Updated Successfully'); } /** @@ -62,6 +65,9 @@ class NewsStoryController extends Controller */ public function destroy(string $id) { - // + $story = NewsStory::findOrFail($id); + $story->delete(); + + return redirect()->route('admin.news.index')->with('success', 'Story Deleted Successfully'); } } diff --git a/app/Http/Controllers/WelcomeController.php b/app/Http/Controllers/WelcomeController.php index 096c00e..49e30ee 100644 --- a/app/Http/Controllers/WelcomeController.php +++ b/app/Http/Controllers/WelcomeController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Models\NewsStory; + use function siteData; class WelcomeController extends Controller @@ -17,6 +19,7 @@ class WelcomeController extends Controller $beginnerClinicDates = siteData('beginnerClinicDates'); $beginnerClinicLocation = siteData('beginnerClinicLocation'); $officers = siteData('officers'); + $newsStories = NewsStory::published()->orderBy('updated_at', 'desc')->paginate(3); return view('welcome', compact( 'officers', @@ -27,6 +30,7 @@ class WelcomeController extends Controller 'concertClinicDates', 'concertClinicLocation', 'beginnerClinicDates', - 'beginnerClinicLocation')); + 'beginnerClinicLocation', + 'newsStories')); } } diff --git a/app/Http/Requests/NewsStoryRequest.php b/app/Http/Requests/NewsStoryRequest.php new file mode 100644 index 0000000..e8d259c --- /dev/null +++ b/app/Http/Requests/NewsStoryRequest.php @@ -0,0 +1,49 @@ +merge([ + 'active' => $this->boolean('active'), + 'scheduleStart' => $this->boolean('scheduleStart'), + 'scheduleEnd' => $this->boolean('scheduleEnd'), + 'start_publication_date' => $this->boolean('scheduleStart') + ? $this->input('start_publication_date') + : now()->format('Y-m-d'), + 'stop_publication_date' => $this->boolean('scheduleEnd') + ? $this->input('stop_publication_date') + : '2100-01-01', + ]); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'headline' => ['required', 'string'], + 'body' => ['required', 'string'], + 'active' => ['required', 'boolean'], + 'scheduleStart' => ['sometimes', 'boolean'], + 'start_publication_date' => ['nullable', 'date'], + 'scheduleEnd' => ['sometimes', 'boolean'], + 'stop_publication_date' => ['nullable', 'date', 'after_or_equal:start_publication_date'], + ]; + } +} diff --git a/app/Models/NewsStory.php b/app/Models/NewsStory.php index b0a949d..b8dce9d 100644 --- a/app/Models/NewsStory.php +++ b/app/Models/NewsStory.php @@ -2,9 +2,50 @@ namespace App\Models; +use App\Enums\StoryStatusEnum; +use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class NewsStory extends Model { - // + use HasFactory; + + protected $fillable = [ + 'headline', + 'body', + 'active', + 'start_publication_date', + 'stop_publication_date', + ]; + + protected function casts(): array + { + return [ + 'active' => 'boolean', + 'start_publication_date' => 'date', + 'stop_publication_date' => 'date', + ]; + } + + protected function status(): Attribute + { + return Attribute::make( + get: fn () => match (true) { + ! $this->active => StoryStatusEnum::DRAFT, + $this->start_publication_date && now() < $this->start_publication_date => StoryStatusEnum::SCHEDULED, + $this->stop_publication_date && now() >= $this->stop_publication_date => StoryStatusEnum::EXPIRED, + default => StoryStatusEnum::PUBLISHED, + } + ); + } + + public function scopePublished($query) + { + return $query->where('active', true) + ->where(fn ($q) => $q->whereNull('start_publication_date') + ->orWhereDate('start_publication_date', '<=', now())) + ->where(fn ($q) => $q->whereNull('stop_publication_date') + ->orWhereDate('stop_publication_date', '>=', now())); + } } diff --git a/database/factories/NewsStoryFactory.php b/database/factories/NewsStoryFactory.php new file mode 100644 index 0000000..f3f1089 --- /dev/null +++ b/database/factories/NewsStoryFactory.php @@ -0,0 +1,25 @@ + $this->faker->sentence(), + 'body' => $this->faker->realText(), + 'start_publication_date' => Carbon::now(), + 'stop_publication_date' => Carbon::now()->addDays(7), + 'active' => '1', + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + ]; + } +} diff --git a/resources/views/admin/news/create.blade.php b/resources/views/admin/news/create.blade.php index f8d6f72..3582da3 100644 --- a/resources/views/admin/news/create.blade.php +++ b/resources/views/admin/news/create.blade.php @@ -1,8 +1,15 @@ Create News Story + + @if($errors->any()) + @foreach($errors->all() as $error) + {{ $error }} + @endforeach + @endif +
diff --git a/resources/views/admin/news/edit.blade.php b/resources/views/admin/news/edit.blade.php new file mode 100644 index 0000000..5bc899a --- /dev/null +++ b/resources/views/admin/news/edit.blade.php @@ -0,0 +1,62 @@ + + + Create News Story + + + @if($errors->any()) + @foreach($errors->all() as $error) + {{ $error }} + @endforeach + @endif + +
+ +
+
+ + {{ $newsStory->body }} + +
+
+ + Status + @if($newsStory->active) + Active + Draft + @else + Active + Draft + @endif + +
+ +
+ + +
+
+ + +
+
+ Save Story +
+
+
+
+
diff --git a/resources/views/admin/news/index.blade.php b/resources/views/admin/news/index.blade.php index 506dab9..9b58065 100644 --- a/resources/views/admin/news/index.blade.php +++ b/resources/views/admin/news/index.blade.php @@ -1,10 +1,47 @@ - + News Stories
New Story
+
+ + + + Headline + Status + Submission Date + Start Publication + Stop Publication + + @foreach($stories as $story) + + + + + + + Confirm Story Deletion + Delete + + Do you really want to delete the story with the headline
{{ $story->headline }}? +
+
+ {{ $story->headline }} + + + {{ $story->status->label() }} + + + {{ $story->created_at->format('m/d/Y') }} + {{ $story->start_publication_date->format('m/d/Y') }} + {{ $story->stop_publication_date?->format('m/d/Y') }} + + + @endforeach +
+
diff --git a/resources/views/components/alert.blade.php b/resources/views/components/alert.blade.php index e6ab0b5..8acb1dd 100644 --- a/resources/views/components/alert.blade.php +++ b/resources/views/components/alert.blade.php @@ -1,38 +1,37 @@ @props(['color' => 'blue']) -
-
$color === 'yellow', 'border-red-400 bg-red-50 dark:border-red-500 dark:bg-red-500/15' => $color === 'red', 'border-green-400 bg-green-50 dark:border-green-500 dark:bg-green-500/10' => $color === 'green', 'border-blue-400 bg-blue-50 dark:border-blue-500 dark:bg-blue-500/10' => $color === 'blue', ])> -
-
-
+
+ -
-
-

+ + +

+
+

$color === 'yellow', 'text-blue-700 dar:text-blue-300' => $color === 'blue', 'text-red-700 dar:text-red-300' => $color === 'red', 'text-green-700 dar:text-green-300' => $color === 'green', ])> - You have no credits left. -

-
+ {{ $slot }} +

+
diff --git a/resources/views/components/badge-pill.blade.php b/resources/views/components/badge-pill.blade.php new file mode 100644 index 0000000..3b4fed8 --- /dev/null +++ b/resources/views/components/badge-pill.blade.php @@ -0,0 +1,18 @@ +@props(['color' => 'blue']) + + $color === 'red', + 'bg-gray-50 text-gray-600 inset-ring inset-ring-gray-500/10 dark:bg-gray-400/10 dark:text-gray-400 dark:inset-ring-gray-400/20' => $color === 'gray', + 'bg-yellow-50 text-yellow-800 inset-ring inset-ring-yellow-600/20 dark:bg-yellow-400/10 dark:text-yellow-500 dark:inset-ring-yellow-400/20' => $color === 'yellow', + 'bg-green-50 text-green-700 inset-ring inset-ring-green-600/20 dark:bg-green-400/10 dark:text-green-400 dark:inset-ring-green-500/20' => $color === 'green', + 'bg-blue-50 text-blue-700 inset-ring inset-ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:inset-ring-blue-400/30' => $color === 'blue', + 'bg-indigo-50 text-indigo-700 inset-ring inset-ring-indigo-700/10 dark:bg-indigo-400/10 dark:text-indigo-400 dark:inset-ring-indigo-400/30' => $color === 'indigo', + 'bg-purple-50 text-purple-700 inset-ring inset-ring-purple-700/10 dark:bg-purple-400/10 dark:text-purple-400 dark:inset-ring-purple-400/30' => $color === 'purple', + 'bg-pink-50 text-pink-700 inset-ring inset-ring-pink-700/10 dark:bg-pink-400/10 dark:text-pink-400 dark:inset-ring-pink-400/20' => $color === 'pink', +])> + {{ $slot }} + + + + diff --git a/resources/views/components/modal-danger.blade.php b/resources/views/components/modal-danger.blade.php new file mode 100644 index 0000000..89b1dfa --- /dev/null +++ b/resources/views/components/modal-danger.blade.php @@ -0,0 +1,49 @@ +@props(['id', 'openButton', 'heading', 'affirmativeButton', 'formMethod', 'formAction']) + + + + + +
+ + +
+
+ +
+
+

{{ $heading }}

+
+

merge(['class'=>'text-sm text-gray-500 dark:text-gray-400']) }}>{{ $slot }}

+
+
+
+
+ + +
+
+
+
+
+
diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php index a2aaca5..7827459 100644 --- a/resources/views/welcome.blade.php +++ b/resources/views/welcome.blade.php @@ -52,9 +52,26 @@
- - News Story - + @foreach($newsStories as $story) + + {{ $story->headline }} + +

+ Published: {{ $story->start_publication_date->format('m/d/Y') ?? $story->created_at->format('m/d/Y') }}

+ @if($story->created_at->format('m/d/Y') != $story->updated_at->format('m/d/Y')) +

Last Updated: {{ $story->updated_at->format('m/d/Y') }}

+ @endif +
+ {{ $story->body }} +
+
+ @endforeach + @if($newsStories->hasPages()) + + See More Stories + {{ $newsStories->links() }} + + @endif
diff --git a/routes/web.php b/routes/web.php index 193840e..758be45 100644 --- a/routes/web.php +++ b/routes/web.php @@ -26,5 +26,5 @@ Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () Route::get('/', [UsersController::class, 'index'])->name('index'); }); Route::resource('/etudes', AuditionEtudeController::class)->names('etudes'); - Route::resource('/news', NewsStoryController::class)->names('news'); + Route::resource('/news', NewsStoryController::class)->except(['show'])->names('news'); });