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 new file mode 100644 index 0000000..ceedf1d --- /dev/null +++ b/app/Http/Controllers/Admin/NewsStoryController.php @@ -0,0 +1,73 @@ +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(NewsStoryRequest $request) + { + NewsStory::create( + $request->validated() + ); + + return redirect()->route('admin.news.index')->with('success', 'Story Added Successfully'); + } + + + /** + * Show the form for editing the specified resource. + */ + 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(NewsStoryRequest $request, string $id) + { + $newsStory = NewsStory::findOrFail($id); + $newsStory->update($request->validated()); + + return redirect()->route('admin.news.index')->with('success', 'Story Updated Successfully'); + } + + /** + * Remove the specified resource from storage. + */ + 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 new file mode 100644 index 0000000..b8dce9d --- /dev/null +++ b/app/Models/NewsStory.php @@ -0,0 +1,51 @@ + '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/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/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/database/migrations/2025_12_18_203202_create_news_stories_table.php b/database/migrations/2025_12_18_203202_create_news_stories_table.php new file mode 100644 index 0000000..4b9f267 --- /dev/null +++ b/database/migrations/2025_12_18_203202_create_news_stories_table.php @@ -0,0 +1,32 @@ +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'); + } +}; 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..3582da3 --- /dev/null +++ b/resources/views/admin/news/create.blade.php @@ -0,0 +1,55 @@ + + + Create News Story + + + @if($errors->any()) + @foreach($errors->all() as $error) + {{ $error }} + @endforeach + @endif + + +
+ +
+
+ +
+
+ + Status + Active + Draft + +
+ +
+ + +
+
+ + +
+
+ Save Story +
+
+
+
+
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 new file mode 100644 index 0000000..9b58065 --- /dev/null +++ b/resources/views/admin/news/index.blade.php @@ -0,0 +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 new file mode 100644 index 0000000..8acb1dd --- /dev/null +++ b/resources/views/components/alert.blade.php @@ -0,0 +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', + ])> + {{ $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/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/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 bb98881..758be45 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)->except(['show'])->names('news'); });