Compare commits

..

10 Commits

Author SHA1 Message Date
Matt Young bd9a6de959 Merge branch 'refs/heads/news-stories' 2025-12-22 22:55:19 -06:00
Matt Young ecd16e5d41 Working News Section 2025-12-22 22:54:36 -06:00
Matt Young b57a2a515a Alert component 2025-12-19 12:41:47 -06:00
Matt Young 18a48f5463 Create news story form 2025-12-19 09:27:13 -06:00
Matt Young a34940d22c Add NewsStory model and migration
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-18 14:44:05 -06:00
Matt Young cc831c026d Add unique key to audition etudes 2025-12-18 14:24:58 -06:00
Matt Young c676690967 Add unique key to audition etudes 2025-12-18 14:18:37 -06:00
Matt Young b146146b58 Displayin etudes 2025-12-18 13:57:11 -06:00
Matt Young 750c9792be Displayin etudes 2025-12-18 13:56:58 -06:00
Matt Young e9a6379438 Etude index working 2025-12-18 09:20:07 -06:00
42 changed files with 1260 additions and 28 deletions

20
.ai/mcp/mcp.json Normal file
View File

@ -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"
}
}
}
}

View File

@ -17,6 +17,7 @@ This application is a Laravel application and its main Laravel ecosystems packag
- laravel/sail (SAIL) - v1
- pestphp/pest (PEST) - v3
- phpunit/phpunit (PHPUNIT) - v11
- alpinejs (ALPINEJS) - v3
- tailwindcss (TAILWINDCSS) - v4
## Conventions

View File

@ -17,6 +17,7 @@ This application is a Laravel application and its main Laravel ecosystems packag
- laravel/sail (SAIL) - v1
- pestphp/pest (PEST) - v3
- phpunit/phpunit (PHPUNIT) - v11
- alpinejs (ALPINEJS) - v3
- tailwindcss (TAILWINDCSS) - v4
## Conventions

View File

@ -0,0 +1,20 @@
<?php
namespace App\Actions;
use Carbon\Carbon;
class GetCurrentAuditionEtudeSet
{
public function __invoke(): int
{
return $this->getCurrentSet();
}
public function getCurrentSet(?string $date = null): int
{
$date = $date ? Carbon::parse($date) : now();
return $date->month < 3 ? $date->year : $date->year + 1;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Enums;
enum StoryStatusEnum: string
{
case DRAFT = 'draft';
case SCHEDULED = 'scheduled';
case PUBLISHED = 'published';
case EXPIRED = 'expired';
public function label(): string
{
return match ($this) {
self::DRAFT => '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',
};
}
}

View File

@ -1,12 +1,14 @@
<?php
namespace App\Http\Controllers;
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\EtudeUploadRequest;
use App\Models\AuditionedEnsemble;
use App\Models\AuditionEtude;
use App\Models\Instrument;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class AuditionEtudeController extends Controller
{
@ -15,7 +17,9 @@ class AuditionEtudeController extends Controller
*/
public function index()
{
return view('admin.audition_etude.index');
$etudes = AuditionEtude::paginate(10);
return view('admin.audition_etude.index', compact('etudes'));
}
/**
@ -37,7 +41,12 @@ class AuditionEtudeController extends Controller
*/
public function store(EtudeUploadRequest $request)
{
$path = $request->file('file_upload')->store('etudes', 'public');
$instrument = Instrument::find($request->instrument_id);
$ensemble = AuditionedEnsemble::find($request->auditioned_ensemble_id);
$filename = $ensemble->name.' '.$instrument->instrument.' Set '.$request->set.'.pdf';
$filename = str_replace(' ', '_', $filename);
$path = $request->file('file_upload')->storeAs('etudes', $filename, 'public');
$originalFilename = $request->file('file_upload')->getClientOriginalName();
$fileSize = $request->file('file_upload')->getSize();
@ -50,6 +59,12 @@ class AuditionEtudeController extends Controller
'file_size' => $fileSize,
]);
session([
'previous_instrument_id' => $request->instrument_id,
'previous_auditioned_ensemble_id' => $request->auditioned_ensemble_id,
'previous_set' => $request->set,
]);
return redirect()->route('admin.etudes.index')->with('success', 'Etude uploaded successfully.');
}
@ -80,8 +95,11 @@ class AuditionEtudeController extends Controller
/**
* Remove the specified resource from storage.
*/
public function destroy(AuditionEtude $auditionEtude)
public function destroy(AuditionEtude $etude)
{
//
Storage::disk('public')->delete('/'.$etude->file_path);
$etude->delete();
return redirect()->route('admin.etudes.index')->with('success', 'Etude deleted successfully.');
}
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\NewsStoryRequest;
use App\Models\NewsStory;
class NewsStoryController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$stories = NewsStory::orderBy('id', 'desc')->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');
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers;
use App\Models\AuditionedEnsemble;
use App\Models\Instrument;
use App\Services\AuditionEtudeService;
class EtudesController extends Controller
{
public function __invoke(AuditionEtudeService $service)
{
$ensembles = AuditionedEnsemble::all();
$instruments = Instrument::has('etudes')->withCount('etudes')->get();
$schoolYear = $service->getActiveSchoolYear();
$currentSet = [];
$etudes = [];
foreach ($ensembles as $ensemble) {
$thisSet = $service->getSetForEnsemble($ensemble);
$currentSet[$ensemble->id] = $thisSet;
$etudes[$ensemble->id] = $ensemble
->etudes()->where('set', '=', $thisSet)
->get()->keyBy('instrument_id');
}
return view('etudes', compact('ensembles', 'schoolYear', 'currentSet', 'instruments','etudes'));
}
}

View File

@ -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'));
}
}

View File

@ -4,6 +4,7 @@ namespace App\Http\Requests;
use App\Models\AuditionedEnsemble;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class EtudeUploadRequest extends FormRequest
{
@ -30,15 +31,18 @@ class EtudeUploadRequest extends FormRequest
'required',
'numeric',
'min:1',
Rule::unique('audition_etudes')
->where('auditioned_ensemble_id', $this->auditioned_ensemble_id)
->where('instrument_id', $this->instrument_id),
function ($attribute, $value, $fail) {
/** @noinspection PhpUndefinedFieldInspection */
$ensemble = AuditionedEnsemble::find($this->audtioned_ensemble_id);
$ensemble = AuditionedEnsemble::find($this->auditioned_ensemble_id);
if ($ensemble && $value > $ensemble->set_count) {
$fail("The set number cannot exceed {$ensemble->set_count} for this ensemble.");
}
},
],
'file_upload' => ['required', 'file', 'mimes:pdf', 'mimetypes:application/pdf', 'max:10240'],
'file_upload' => ['required', 'file', 'mimes:pdf', 'mimetypes:application/pdf', 'max:51200'],
];
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class NewsStoryRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->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, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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'],
];
}
}

View File

@ -2,6 +2,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -20,4 +21,24 @@ class AuditionEtude extends Model
{
return $this->belongsTo(AuditionedEnsemble::class);
}
protected function humanReadableFileSize(): Attribute
{
return Attribute::make(
get: function () {
$bytes = $this->file_size;
if ($bytes === null) {
return 'N/A';
}
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2).' '.$units[$pow];
}
);
}
}

View File

@ -3,10 +3,16 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class AuditionedEnsemble extends Model
{
protected $fillable = [
'name', 'set_count',
];
public function etudes(): HasMany
{
return $this->hasMany(AuditionEtude::class);
}
}

View File

@ -3,10 +3,16 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Instrument extends Model
{
protected $fillable = [
'instrument', 'score_order',
];
public function etudes(): HasMany
{
return $this->hasMany(AuditionEtude::class);
}
}

51
app/Models/NewsStory.php Normal file
View File

@ -0,0 +1,51 @@
<?php
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()));
}
}

View File

@ -2,6 +2,7 @@
namespace App\Providers;
use App\Services\AuditionEtudeService;
use App\Services\SiteDataService;
use Illuminate\Support\ServiceProvider;
@ -13,6 +14,7 @@ class AppServiceProvider extends ServiceProvider
public function register(): void
{
$this->app->singleton(SiteDataService::class);
$this->app->singleton(AuditionEtudeService::class);
}
/**

View File

@ -0,0 +1,80 @@
<?php
namespace App\Services;
use App\Models\AuditionedEnsemble;
use Carbon\Carbon;
readonly class AuditionEtudeService
{
protected int $startYear;
protected int $changeoverMonth;
public function __construct()
{
$this->startYear = config('siteData.etude_start_year');
$this->changeoverMonth = config('siteData.etude_changeover_month');
}
/**
* Get the audition year for a given date.
*
* The audition year follows a cycle based on the configured changeover month.
* Before the changeover month, returns the current calendar year.
* On or after the changeover month, returns the next calendar year.
*
* @param string|null $date Optional date string in 'YYYY-MM-DD' format. Defaults to the current date.
* @return int The audition year
*
* @example
* getCurrentAuditionYear('2024-01-15') // Returns 2024 (if changeover month is 3)
* getCurrentAuditionYear('2024-03-01') // Returns 2025 (if changeover month is 3)
* getCurrentAuditionYear() // Returns current audition year
*/
public function getCurrentAuditionYear(?string $date = null): int
{
$date = $date ? Carbon::parse($date) : now();
return $date->month < $this->changeoverMonth ? $date->year : $date->year + 1;
}
/**
* Get the active school year as a string for a given date.
*
* @param string|null $date Optional date string in 'YYYY-MM-DD' format. Defaults to the current date.
* @return string The school year in "YYYY - YYYY" format
*
* @example
* getActiveSchoolYear('2024-01-15') // Returns "2023 - 2024"
* getActiveSchoolYear('2024-09-01') // Returns "2024 - 2025"
*/
public function getActiveSchoolYear(?string $date = null): string
{
$auditionYear = $this->getCurrentAuditionYear($date);
return ($auditionYear - 1).' - '.$auditionYear;
}
/**
* Get the current set number for an ensemble in a given year.
*
* Sets rotate annually based on the ensemble's set count.
* The rotation is calculated from the configured start year.
*
* @param AuditionedEnsemble $ensemble The ensemble to get the set for
* @param int|null $year Optional year. Defaults to current audition year.
* @return int The set number (1 to ensemble's set_count)
*
* @example
* getSetForEnsemble($ensemble, 2024) // Returns the set number for 2024
*/
public function getSetForEnsemble(AuditionedEnsemble $ensemble, ?int $year = null): int
{
$year = $year ?? $this->getCurrentAuditionYear();
$setCount = $ensemble->set_count;
$yearDiff = $year - $this->startYear;
return ($yearDiff % $setCount) + 1;
}
}

View File

@ -36,6 +36,10 @@ class Admin extends Component
'name' => 'Audition Etudes',
'link' => route('admin.etudes.index'),
],
[
'name' => 'News Stories',
'link' => route('admin.news.index'),
]
];
}

View File

@ -30,7 +30,7 @@ class App extends Component
],
[
'name' => 'Audition Etudes',
'link' => '#',
'link' => route('etudes'),
],
];
}

View File

@ -10,12 +10,14 @@
"license": "MIT",
"require": {
"php": "^8.2",
"blade-ui-kit/blade-heroicons": "^2.6",
"laravel/fortify": "^1.32",
"laravel/framework": "^12.0",
"laravel/prompts": "^0.3.8",
"laravel/tinker": "^2.10.1"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.16",
"fakerphp/faker": "^1.23",
"laravel/boost": "^1.8",
"laravel/pail": "^1.2.2",

310
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bd377529b45b2db07fbfd360dccb505c",
"content-hash": "b97c7859ce60e3e2bd8fd47d5ed2f249",
"packages": [
{
"name": "bacon/bacon-qr-code",
@ -61,6 +61,156 @@
},
"time": "2025-11-19T17:15:36+00:00"
},
{
"name": "blade-ui-kit/blade-heroicons",
"version": "2.6.0",
"source": {
"type": "git",
"url": "https://github.com/driesvints/blade-heroicons.git",
"reference": "4553b2a1f6c76f0ac7f3bc0de4c0cfa06a097d19"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/driesvints/blade-heroicons/zipball/4553b2a1f6c76f0ac7f3bc0de4c0cfa06a097d19",
"reference": "4553b2a1f6c76f0ac7f3bc0de4c0cfa06a097d19",
"shasum": ""
},
"require": {
"blade-ui-kit/blade-icons": "^1.6",
"illuminate/support": "^9.0|^10.0|^11.0|^12.0",
"php": "^8.0"
},
"require-dev": {
"orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
"phpunit/phpunit": "^9.0|^10.5|^11.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"BladeUI\\Heroicons\\BladeHeroiconsServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"BladeUI\\Heroicons\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dries Vints",
"homepage": "https://driesvints.com"
}
],
"description": "A package to easily make use of Heroicons in your Laravel Blade views.",
"homepage": "https://github.com/blade-ui-kit/blade-heroicons",
"keywords": [
"Heroicons",
"blade",
"laravel"
],
"support": {
"issues": "https://github.com/driesvints/blade-heroicons/issues",
"source": "https://github.com/driesvints/blade-heroicons/tree/2.6.0"
},
"funding": [
{
"url": "https://github.com/sponsors/driesvints",
"type": "github"
},
{
"url": "https://www.paypal.com/paypalme/driesvints",
"type": "paypal"
}
],
"time": "2025-02-13T20:53:33+00:00"
},
{
"name": "blade-ui-kit/blade-icons",
"version": "1.8.0",
"source": {
"type": "git",
"url": "https://github.com/driesvints/blade-icons.git",
"reference": "7b743f27476acb2ed04cb518213d78abe096e814"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/driesvints/blade-icons/zipball/7b743f27476acb2ed04cb518213d78abe096e814",
"reference": "7b743f27476acb2ed04cb518213d78abe096e814",
"shasum": ""
},
"require": {
"illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/filesystem": "^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/view": "^8.0|^9.0|^10.0|^11.0|^12.0",
"php": "^7.4|^8.0",
"symfony/console": "^5.3|^6.0|^7.0",
"symfony/finder": "^5.3|^6.0|^7.0"
},
"require-dev": {
"mockery/mockery": "^1.5.1",
"orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0",
"phpunit/phpunit": "^9.0|^10.5|^11.0"
},
"bin": [
"bin/blade-icons-generate"
],
"type": "library",
"extra": {
"laravel": {
"providers": [
"BladeUI\\Icons\\BladeIconsServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"BladeUI\\Icons\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dries Vints",
"homepage": "https://driesvints.com"
}
],
"description": "A package to easily make use of icons in your Laravel Blade views.",
"homepage": "https://github.com/blade-ui-kit/blade-icons",
"keywords": [
"blade",
"icons",
"laravel",
"svg"
],
"support": {
"issues": "https://github.com/blade-ui-kit/blade-icons/issues",
"source": "https://github.com/blade-ui-kit/blade-icons"
},
"funding": [
{
"url": "https://github.com/sponsors/driesvints",
"type": "github"
},
{
"url": "https://www.paypal.com/paypalme/driesvints",
"type": "paypal"
}
],
"time": "2025-02-13T20:35:06+00:00"
},
{
"name": "brick/math",
"version": "0.14.1",
@ -6295,6 +6445,91 @@
}
],
"packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
"version": "v3.16.2",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
"reference": "730dbf8bf41f5691e026dd771e64dd54ad1b10b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/730dbf8bf41f5691e026dd771e64dd54ad1b10b3",
"reference": "730dbf8bf41f5691e026dd771e64dd54ad1b10b3",
"shasum": ""
},
"require": {
"illuminate/routing": "^10|^11|^12",
"illuminate/session": "^10|^11|^12",
"illuminate/support": "^10|^11|^12",
"php": "^8.1",
"php-debugbar/php-debugbar": "^2.2.4",
"symfony/finder": "^6|^7"
},
"require-dev": {
"mockery/mockery": "^1.3.3",
"orchestra/testbench-dusk": "^7|^8|^9|^10",
"phpunit/phpunit": "^9.5.10|^10|^11",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar"
},
"providers": [
"Barryvdh\\Debugbar\\ServiceProvider"
]
},
"branch-alias": {
"dev-master": "3.16-dev"
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Barryvdh\\Debugbar\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "PHP Debugbar integration for Laravel",
"keywords": [
"debug",
"debugbar",
"dev",
"laravel",
"profiler",
"webprofiler"
],
"support": {
"issues": "https://github.com/barryvdh/laravel-debugbar/issues",
"source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.16.2"
},
"funding": [
{
"url": "https://fruitcake.nl",
"type": "custom"
},
{
"url": "https://github.com/barryvdh",
"type": "github"
}
],
"time": "2025-12-03T14:52:46+00:00"
},
{
"name": "brianium/paratest",
"version": "v7.8.4",
@ -7909,6 +8144,79 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
{
"name": "php-debugbar/php-debugbar",
"version": "v2.2.4",
"source": {
"type": "git",
"url": "https://github.com/php-debugbar/php-debugbar.git",
"reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/3146d04671f51f69ffec2a4207ac3bdcf13a9f35",
"reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35",
"shasum": ""
},
"require": {
"php": "^8",
"psr/log": "^1|^2|^3",
"symfony/var-dumper": "^4|^5|^6|^7"
},
"replace": {
"maximebf/debugbar": "self.version"
},
"require-dev": {
"dbrekelmans/bdi": "^1",
"phpunit/phpunit": "^8|^9",
"symfony/panther": "^1|^2.1",
"twig/twig": "^1.38|^2.7|^3.0"
},
"suggest": {
"kriswallsmith/assetic": "The best way to manage assets",
"monolog/monolog": "Log using Monolog",
"predis/predis": "Redis storage"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
}
},
"autoload": {
"psr-4": {
"DebugBar\\": "src/DebugBar/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maxime Bouroumeau-Fuseau",
"email": "maxime.bouroumeau@gmail.com",
"homepage": "http://maximebf.com"
},
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "Debug bar in the browser for php application",
"homepage": "https://github.com/php-debugbar/php-debugbar",
"keywords": [
"debug",
"debug bar",
"debugbar",
"dev"
],
"support": {
"issues": "https://github.com/php-debugbar/php-debugbar/issues",
"source": "https://github.com/php-debugbar/php-debugbar/tree/v2.2.4"
},
"time": "2025-07-22T14:01:30+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",

View File

@ -2,6 +2,24 @@
return [
/*
|--------------------------------------------------------------------------
| Etude Start Year
|--------------------------------------------------------------------------
|
| A year in the past where all ensembles used set 1
|
*/
'etude_start_year' => env('ETUDE_START_YEAR', 2009),
/*
|--------------------------------------------------------------------------
| Etude Start Year
|--------------------------------------------------------------------------
|
| A year in the past where all ensembles used set 1
|
*/
'etude_changeover_month' => env('ETUDE_CHANGEOVER_MONTH', 3),
/*
|--------------------------------------------------------------------------

View File

@ -0,0 +1,25 @@
<?php
namespace Database\Factories;
use App\Models\NewsStory;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Carbon;
class NewsStoryFactory extends Factory
{
protected $model = NewsStory::class;
public function definition(): array
{
return [
'headline' => $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(),
];
}
}

View File

@ -0,0 +1,28 @@
<?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::table('audition_etudes', function (Blueprint $table) {
$table->unique(['instrument_id', 'auditioned_ensemble_id', 'set']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('audition_etudes', function (Blueprint $table) {
$table->dropUnique(['instrument_id', 'auditioned_ensemble_id', 'set']);
});
}
};

View File

@ -0,0 +1,32 @@
<?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('news_stories', function (Blueprint $table) {
$table->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');
}
};

View File

@ -34,7 +34,12 @@ class InstrumentSeeder extends Seeder
['instrument' => 'Euphonium TC', 'score_order' => 170],
['instrument' => 'Tuba', 'score_order' => 180],
['instrument' => 'Percussion', 'score_order' => 200],
['instrument' => 'Drums', 'score_order' => 210],
['instrument' => 'Vibes', 'score_order' => 220],
['instrument' => 'String Bass', 'score_order' => 300],
['instrument' => 'Bass', 'score_order' => 400],
['instrument' => 'Piano', 'score_order' => 410],
['instrument' => 'Guitar', 'score_order' => 420],
];
Instrument::insert($defaultInstruments);

View File

@ -1,6 +1,7 @@
@import 'tailwindcss';
@layer base {
dl dt {
@apply font-semibold;
}

View File

@ -7,11 +7,14 @@
<div class="flex">
<div class="shrink-0">
<svg class="size-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z"
clip-rule="evenodd"/>
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">There were {{ $errors->count() }} error(s) with your submission</h3>
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">There
were {{ $errors->count() }} error(s) with your submission</h3>
<div class="mt-2 text-sm text-red-700 dark:text-red-300">
<ul class="list-disc space-y-1 pl-5">
@foreach($errors->all() as $error)
@ -24,13 +27,20 @@
</div>
@endif
<x-form.form method="POST" action="{{ route('admin.etudes.store') }}" enctype="multipart/form-data" x-data="{ selectedEnsemble: '', selectedInstrument: '', selectedSetNumber: '' }">
<x-form.form method="POST" action="{{ route('admin.etudes.store') }}" enctype="multipart/form-data"
x-data="{ selectedEnsemble: '', selectedInstrument: '', selectedSetNumber: '' }">
<div>
<x-form.select name="auditioned_ensemble_id" x-model="selectedEnsemble">
<x-slot:label>Ensemble</x-slot:label>
<option value="">Select Ensemble...</option>
@foreach($ensembles as $ensemble)
<option value="{{ $ensemble->id }}" {{ old('auditioned_ensemble_id') === $ensemble->id ? ' SELECTED ':'' }}>{{ $ensemble->name }}</option>
<option
value="{{ $ensemble->id }}"
{{ old('auditioned_ensemble_id') == $ensemble->id ? ' SELECTED ':'' }}
{{ session('previous_auditioned_ensemble_id') == $ensemble->id ? ' SELECTED ':'' }}
>
{{ $ensemble->name }}
</option>
@endforeach
</x-form.select>
</div>
@ -39,18 +49,31 @@
<option value="">Select Instrument...</option>
<x-slot:label>Instrument</x-slot:label>
@foreach($instruments as $instrument)
<option value="{{ $instrument->id }}" {{ old('instrument_id') === $instrument->id ? ' SELECTED ':'' }}>{{ $instrument->instrument }}</option>
<option
value="{{ $instrument->id }}"
{{ old('instrument_id') === $instrument->id ? ' SELECTED ':'' }}
{{ session('previous_instrument_id') === $instrument->id ? ' SELECTED ':'' }}
>
{{ $instrument->instrument }}
</option>
@endforeach
</x-form.select>
</div>
<div class="mt-3">
<x-form.input name="set" label="Set Number" value="{{ old('set') }}" type="number" min="1" x-bind:max="selectedEnsemble ? getSetCount(selectedEnsemble) : ''" x-bind:disabled="!selectedEnsemble" x-model="selectedSetNumber" />
<x-form.input name="set" label="Set Number"
:value="session('previous_set')"
type="number" min="1"
x-bind:max="selectedEnsemble ? getSetCount(selectedEnsemble) : ''"
x-bind:disabled="!selectedEnsemble" x-model="selectedSetNumber"/>
</div>
<div class="mt-3">
<x-form.input name="file_upload" type="file" label="Etude PDF"/>
</div>
<div class="mt-3 text-right">
<x-form.button type="submit" x-bind:disabled="!selectedEnsemble || !selectedInstrument || !selectedSetNumber">Save Etude</x-form.button>
<x-form.button type="submit"
x-bind:disabled="!selectedEnsemble || !selectedInstrument || !selectedSetNumber">Save
Etude
</x-form.button>
</div>
</x-form.form>
</x-slot:body>
@ -65,6 +88,7 @@
},
@endforeach
};
function getSetCount(ensembleId) {
// noinspection JSUnresolvedVariable
return ensembles[ensembleId]?.setCount || '';

View File

@ -1,4 +1,43 @@
<x-layout.admin>
<x-form.button href="{{ route('admin.etudes.create') }}" type="link">Add Etude</x-form.button>
<p>Audition Etude Index</p>
<x-card class="mt-3">
<x-slot:header class="bg-brand-600!">Etudes</x-slot:header>
<x-slot:body class="bg-white border border-brand-600">
<x-table>
<x-slot:header>
<x-table.th>Ensemble</x-table.th>
<x-table.th>Set</x-table.th>
<x-table.th>Instrument</x-table.th>
<x-table.th>Link</x-table.th>
<x-table.th>Original Filename</x-table.th>
<x-table.th>Size</x-table.th>
<x-table.th></x-table.th>
</x-slot:header>
@foreach($etudes as $etude)
<tr>
<x-table.td>{{ $etude->auditionedEnsemble->name }}</x-table.td>
<x-table.td>{{ $etude->set }}</x-table.td>
<x-table.td>{{ $etude->instrument->instrument }}</x-table.td>
<x-table.td>
<a href="/{{ $etude->file_path }}" target="_new">
<x-heroicon-o-arrow-down-tray class="w-5"/>
</a>
</x-table.td>
<x-table.td>{{ $etude->original_filename }}</x-table.td>
<x-table.td>{{ $etude->human_readable_file_size }}</x-table.td>
<x-table.td>
<x-form method="DELETE" action="{{ route('admin.etudes.destroy', $etude) }}">
<button type="submit">
<x-heroicon-o-trash class="w-5"/>
</button>
</x-form>
</x-table.td>
</tr>
@endforeach
</x-table>
<div class="mt-3 px-4">
{{ $etudes->links() }}
</div>
</x-slot:body>
</x-card>
</x-layout.admin>

View File

@ -0,0 +1,55 @@
<x-layout.admin>
<x-card class="max-w-3xl mx-auto">
<x-slot:header class="bg-brand-600!">Create News Story</x-slot:header>
<x-slot:body class="bg-white border border-brand-600">
@if($errors->any())
@foreach($errors->all() as $error)
<x-alert color="red">{{ $error }}</x-alert>
@endforeach
@endif
<x-form method="POST" :action="route('admin.news.store')">
<div>
<x-form.input name="headline" label="Headline"/>
</div>
<div class="mt-3">
<x-form.textarea name="body" label="Body"/>
</div>
<div class="mt-3">
<x-form.radio-group name="active">
<x-slot:label>Status</x-slot:label>
<x-form.radio-group-item id="active" value="true" checked>Active</x-form.radio-group-item>
<x-form.radio-group-item id="draft" value="false">Draft</x-form.radio-group-item>
</x-form.radio-group>
</div>
<div x-data="{ showStartDate: false }" class="mt-3">
<x-form.checkbox name="scheduleStart" x-model="showStartDate" value="true"
label="Schedule Publication"/>
<x-form.input
type="date"
name="start_publication_date"
label=""
:value="now()->toDateString()"
x-show="showStartDate"
x-cloak
/>
</div>
<div x-data="{ showEndDate: false }" class="mt-3">
<x-form.checkbox name="scheduleEnd" x-model="showEndDate" value="true" label="Schedule Removal"/>
<x-form.input
type="date"
name="stop_publication_date"
label=""
x-show="showEndDate"
x-cloak
/>
</div>
<div class="mt-3 text-right">
<x-form.button type="submit">Save Story</x-form.button>
</div>
</x-form>
</x-slot:body>
</x-card>
</x-layout.admin>

View File

@ -0,0 +1,62 @@
<x-layout.admin>
<x-card class="max-w-3xl mx-auto">
<x-slot:header class="bg-brand-600!">Create News Story</x-slot:header>
<x-slot:body class="bg-white border border-brand-600">
@if($errors->any())
@foreach($errors->all() as $error)
<x-alert color="red">{{ $error }}</x-alert>
@endforeach
@endif
<x-form method="PATCH" :action="route('admin.news.update', $newsStory)">
<div>
<x-form.input name="headline" label="Headline" value="{{ $newsStory->headline }}" />
</div>
<div class="mt-3">
<x-form.textarea name="body" label="Body">
{{ $newsStory->body }}
</x-form.textarea>
</div>
<div class="mt-3">
<x-form.radio-group name="active">
<x-slot:label>Status</x-slot:label>
@if($newsStory->active)
<x-form.radio-group-item id="active" value="true" checked>Active</x-form.radio-group-item>
<x-form.radio-group-item id="draft" value="false">Draft</x-form.radio-group-item>
@else
<x-form.radio-group-item id="active" value="true">Active</x-form.radio-group-item>
<x-form.radio-group-item id="draft" value="false" checked>Draft</x-form.radio-group-item>
@endif
</x-form.radio-group>
</div>
<div x-data="{ showStartDate: {{ $newsStory->start_publication_date ? 'true' : 'false' }} }" class="mt-3">
<x-form.checkbox name="scheduleStart" x-model="showStartDate" value="true"
label="Schedule Publication"/>
<x-form.input
type="date"
name="start_publication_date"
label=""
value="{{ $newsStory->start_publication_date?->toDateString() ?? now()->toDateString() }}"
x-show="showStartDate"
x-cloak
/>
</div>
<div x-data="{ showEndDate: {{ $newsStory->stop_publication_date ? 'true' : 'false' }} }" class="mt-3">
<x-form.checkbox name="scheduleEnd" x-model="showEndDate" value="true" label="Schedule Removal"/>
<x-form.input
type="date"
name="stop_publication_date"
label=""
value="{{ $newsStory->stop_publication_date?->toDateString() ?? '' }}"
x-show="showEndDate"
x-cloak
/>
</div>
<div class="mt-3 text-right">
<x-form.button type="submit">Save Story</x-form.button>
</div>
</x-form>
</x-slot:body>
</x-card>
</x-layout.admin>

View File

@ -0,0 +1,47 @@
<x-layout.admin>
<x-card class="">
<x-slot:header class="bg-brand-600!">News Stories</x-slot:header>
<x-slot:body class="bg-white border border-brand-600">
<div class="text-right">
<x-form.button type="link" :href="route('admin.news.create')">New Story</x-form.button>
</div>
<div class="mt-3">
<x-table>
<x-slot:header>
<x-table.th></x-table.th>
<x-table.th>Headline</x-table.th>
<x-table.th>Status</x-table.th>
<x-table.th>Submission Date</x-table.th>
<x-table.th>Start Publication</x-table.th>
<x-table.th>Stop Publication</x-table.th>
</x-slot:header>
@foreach($stories as $story)
<tr>
<x-table.td class="flex gap-3 ">
<a href="{{route('admin.news.edit', $story)}}">
<x-heroicon-s-pencil-square class="size-5"/>
</a>
<x-modal-danger id="delete-{{$story->id}}" form-method="DELETE" form-action="{{ route('admin.news.destroy', $story) }}">
<x-slot:heading>Confirm Story Deletion</x-slot:heading>
<x-slot:affirmativeButton type="submit">Delete</x-slot:affirmativeButton>
<x-slot:openButton><x-heroicon-s-trash class="size-5" /></x-slot:openButton>
Do you really want to delete the story with the headline<br />{{ $story->headline }}?
</x-modal-danger>
</x-table.td>
<x-table.td>{{ $story->headline }}</x-table.td>
<x-table.td>
<x-badge-pill :color="$story->status->color()">
{{ $story->status->label() }}
</x-badge-pill>
</x-table.td>
<x-table.td>{{ $story->created_at->format('m/d/Y') }}</x-table.td>
<x-table.td>{{ $story->start_publication_date->format('m/d/Y') }}</x-table.td>
<x-table.td>{{ $story->stop_publication_date?->format('m/d/Y') }}</x-table.td>
</tr>
@endforeach
</x-table>
</div>
</x-slot:body>
</x-card>
</x-layout.admin>

View File

@ -0,0 +1,37 @@
@props(['color' => 'blue'])
<div @class([
'border-l-4 p-2 mb-3 ',
'border-yellow-400 bg-yellow-50 dark:border-yellow-500 dark:bg-yellow-500/10' => $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',
])>
<div class="flex">
<div class="shrink-0">
<svg viewBox="0 0 20 20" fill="currentColor" data-slot="icon" aria-hidden="true"
@class([
'size-5 ',
'text-yellow-400 dark:text-yellow-500' => $color === 'yellow',
'text-red-400 dark:text-red-500' => $color === 'red',
'text-green-400 dark:text-green-500' => $color === 'green',
'text-blue-400 dark:text-blue-500' => $color === 'blue',
])>
<path
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
clip-rule="evenodd" fill-rule="evenodd"/>
</svg>
</div>
<div class="ml-3">
<p @class([
'text-sm ',
'text-yellow-700 dar:text-yellow-300' => $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 }}
</p>
</div>
</div>
</div>

View File

@ -0,0 +1,18 @@
@props(['color' => 'blue'])
<span @class([
'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium',
'bg-red-50 text-red-700 inset-ring inset-ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:inset-ring-red-400/20' => $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 }}
</span>

View File

@ -6,7 +6,7 @@
<div class="group grid size-4 grid-cols-1">
<input id="{{ $id ?? $name }}" type="checkbox" name="{{ $name }}"
{{ $checked ? 'checked' : '' }}
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"/>
{{ $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']) }}/>
<svg viewBox="0 0 14 14" fill="none"
class="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-disabled:stroke-gray-950/25 dark:group-has-disabled:stroke-white/25">
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"

View File

@ -0,0 +1,8 @@
@aware(['name'])
@props(['value', 'id'])
<div class="flex items-center">
<input type="radio" name="{{ $name }}" id="{{ $id }}"
value="{{ $value }}" {{ $attributes }}
class="relative size-4 appearance-none rounded-full border border-gray-300 bg-white before:absolute before:inset-1 before:rounded-full before:bg-white not-checked:before:hidden checked:border-indigo-600 checked:bg-indigo-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:before:bg-gray-400 dark:border-white/10 dark:bg-white/5 dark:checked:border-indigo-500 dark:checked:bg-indigo-500 dark:focus-visible:outline-indigo-500 dark:disabled:border-white/5 dark:disabled:bg-white/10 dark:disabled:before:bg-white/20 forced-colors:appearance-auto forced-colors:before:hidden"/>
<label for="{{ $id }}" class="ml-3 block text-sm/6 font-medium text-gray-900 dark:text-white">{{ $slot }}</label>
</div>

View File

@ -0,0 +1,12 @@
@props(['label' => null, 'sublabel' => null, 'name'])
<fieldset>
@if($label)
<legend class="text-sm/6 font-semibold text-gray-900 dark:text-white">{{ $label }}</legend>
@endif
@if($sublabel)
<p class="mt-1 text-sm/6 text-gray-600 dark:text-gray-400">{{ $sublabel }}</p>
@endif
<div class="mt-3 space-y-2">
{{ $slot }}
</div>
</fieldset>

View File

@ -0,0 +1,49 @@
@props(['id', 'openButton', 'heading', 'affirmativeButton', 'formMethod', 'formAction'])
<button command="show-modal" commandfor="{{ $id }}"
class="rounded-md bg-gray-950/5 px-2.5 py-1.5 text-sm font-semibold text-gray-900 hover:bg-gray-950/10 dark:bg-white/10 dark:text-white dark:inset-ring dark:inset-ring-white/5 dark:hover:bg-white/20">
{{ $openButton }}
</button>
<el-dialog>
<dialog id="{{ $id }}" aria-labelledby="dialog-title"
class="fixed inset-0 size-auto max-h-none max-w-none overflow-y-auto bg-transparent backdrop:bg-transparent">
<el-dialog-backdrop
class="fixed inset-0 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in dark:bg-gray-900/50"></el-dialog-backdrop>
<div tabindex="0"
class="flex min-h-full items-end justify-center p-4 text-center focus:outline-none sm:items-center sm:p-0">
<el-dialog-panel
class="relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in sm:my-8 sm:w-full sm:max-w-lg sm:p-6 data-closed:sm:translate-y-0 data-closed:sm:scale-95 dark:bg-gray-800 dark:outline dark:-outline-offset-1 dark:outline-white/10">
<x-form method="{{$formMethod}}" action="{{$formAction}}">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex size-12 shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:size-10 dark:bg-red-500/10">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"
data-slot="icon" aria-hidden="true" class="size-6 text-red-600 dark:text-red-400">
<path
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 id="dialog-title"
class="text-base font-semibold text-gray-900 dark:text-white">{{ $heading }}</h3>
<div class="mt-2">
<p {{ $attributes->merge(['class'=>'text-sm text-gray-500 dark:text-gray-400']) }}>{{ $slot }}</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button type="submit"
class="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 sm:ml-3 sm:w-auto dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400">
{{ $affirmativeButton }}
</button>
<button type="button" command="close" commandfor="{{ $id }}"
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20">
Cancel
</button>
</div>
</x-form>
</el-dialog-panel>
</div>
</dialog>
</el-dialog>

View File

@ -0,0 +1,20 @@
<x-layout.app>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-3">
@foreach($ensembles as $ensemble)
<x-card class="mb-8">
<x-slot:header>{{ $ensemble->name }}<br />{{ $schoolYear }}<br />Set {{ $currentSet[$ensemble->id] }}</x-slot:header>
<ul>
@foreach($instruments as $instrument)
@continue(! $etudes[$ensemble->id]->has($instrument->id))
<li class="mb-2">
<a class="text-blue-800 underline" href="/{{ $etudes[$ensemble->id][$instrument->id]->file_path }}">
{{ $instrument->instrument }}
</a>
</li>
@endforeach
</ul>
</x-card>
@endforeach
</div>
</x-layout.app>

View File

@ -52,9 +52,26 @@
</x-card>
<div class="sm:col-span-2 lg:col-start-2 mt-5">
<x-card>
<x-slot:header>News Story</x-slot:header>
@foreach($newsStories as $story)
<x-card class="mb-5 ">
<x-slot:header>{{ $story->headline }}</x-slot:header>
<p class="text-xs italic">
Published: {{ $story->start_publication_date->format('m/d/Y') ?? $story->created_at->format('m/d/Y') }}</p>
@if($story->created_at->format('m/d/Y') != $story->updated_at->format('m/d/Y'))
<p class="mt-1 text-xs italic">Last Updated: {{ $story->updated_at->format('m/d/Y') }}</p>
@endif
<div class="mt-3">
{{ $story->body }}
</div>
</x-card>
@endforeach
@if($newsStories->hasPages())
<x-card>
<x-slot:header>See More Stories</x-slot:header>
{{ $newsStories->links() }}
</x-card>
@endif
</div>
</div>

View File

@ -1,17 +1,20 @@
<?php
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\AuditionEtudeController;
use App\Http\Controllers\AuditionInformationPageController;
use App\Http\Controllers\ClinicInformationPageController;
use App\Http\Controllers\EtudesController;
use App\Http\Controllers\WelcomeController;
use Illuminate\Support\Facades\Route;
Route::get('/', WelcomeController::class)->name('welcome');
Route::get('/audition-information', AuditionInformationPageController::class)->name('audition-information');
Route::get('/clinic-information', ClinicInformationPageController::class)->name('clinic-information');
Route::get('/etudes', EtudesController::class)->name('etudes');
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/', DashboardController::class)->name('dashboard');
@ -23,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');
});

2
storage/debugbar/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore