Add ability to update site data from admin page.

This commit is contained in:
Matt Young 2025-12-16 11:24:54 -06:00
parent d755d53208
commit b5ca5ab177
11 changed files with 284 additions and 18 deletions

View File

@ -0,0 +1,62 @@
<?php
namespace App\Http\Controllers\Admin;
use const PHP_EOL;
use App\Http\Controllers\Controller;
use App\Http\Requests\SiteDataRequest;
use App\Models\SiteDataItem;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class SiteDataController extends Controller
{
public function index()
{
$dataStrings = SiteDataItem::where('type', 'string')->get();
$dataText = SiteDataItem::where('type', 'text')->get();
$officerText = $this->officerText(siteData('officers'));
$concertEnsemblesText = $this->ensemblesText(siteData('concertEnsembles'));
$jazzEnsemblesText = $this->ensemblesText(siteData('jazzEnsembles'));
$beginnerEnsemblesText = $this->ensemblesText(siteData('beginnerEnsembles'));
return view('admin.site-data', compact(
'dataStrings',
'dataText',
'officerText',
'concertEnsemblesText',
'jazzEnsemblesText',
'beginnerEnsemblesText'
));
}
public function update(SiteDataRequest $request)
{
foreach ($request->validated() as $key => $value) {
siteData($key, $value);
}
return redirect()->route('admin.site-data.index')->with('success', 'Data updated successfully.');
}
protected function officerText(array $officerDataArray): string
{
$officerText = '';
foreach ($officerDataArray as $officer) {
$officerText .= $officer['office'].', '.$officer['name'].', '.$officer['school'].PHP_EOL;
}
return $officerText;
}
protected function ensemblesText(array $ensembleDataArray): string
{
$ensembleText = '';
foreach ($ensembleDataArray as $ensemble) {
$ensembleText .= $ensemble['name'].', '.$ensemble['chair'].', '.$ensemble['clinician'].PHP_EOL;
}
return $ensembleText;
}
}

View File

@ -0,0 +1,99 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SiteDataRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
$this->merge([
'officers' => $this->parseOfficerText($this->input('officers', '')),
'concertEnsembles' => $this->parseEnsembleText($this->input('concertEnsembles', '')),
'beginnerEnsembles' => $this->parseEnsembleText($this->input('beginnerEnsembles', '')),
'jazzEnsembles' => $this->parseEnsembleText($this->input('jazzEnsembles', '')),
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'concertAuditionFee' => ['required', 'string'],
'concertClinicDates' => ['required', 'string'],
'concertClinicLocation' => ['required', 'string'],
'beginnerClinicDates' => ['required', 'string'],
'beginnerClinicLocation' => ['required', 'string'],
'concertAuditionDate' => ['required', 'string'],
'concertAuditionLocation' => ['required', 'string'],
'concertEntryDeadline' => ['required', 'string'],
'beginnerEntryDeadline' => ['required', 'string'],
'concertAuditionAdditionalInformation' => ['required', 'string'],
'beginnerAuditionAdditionalInformation' => ['required', 'string'],
'officers' => ['required', 'array'],
'officers.*.office' => ['required', 'string'],
'officers.*.name' => ['required', 'string'],
'officers.*.school' => ['required', 'string'],
'concertEnsembles' => ['required', 'array'],
'concertEnsembles.*.name' => ['required', 'string'],
'concertEnsembles.*.chair' => ['required', 'string'],
'concertEnsembles.*.clinician' => ['required', 'string'],
'jazzEnsembles' => ['required', 'array'],
'jazzEnsembles.*.name' => ['required', 'string'],
'jazzEnsembles.*.chair' => ['required', 'string'],
'jazzEnsembles.*.clinician' => ['required', 'string'],
'beginnerEnsembles' => ['required', 'array'],
'beginnerEnsembles.*.name' => ['required', 'string'],
'beginnerEnsembles.*.chair' => ['required', 'string'],
'beginnerEnsembles.*.clinician' => ['required', 'string'],
];
}
protected function parseOfficerText(string $text): array
{
$lines = explode(PHP_EOL, trim($text));
$officers = [];
foreach ($lines as $line) {
$parts = array_map('trim', explode(',', $line));
$officers[] = [
'office' => $parts[0] ?? 'TBA',
'name' => $parts[1] ?? 'TBA',
'school' => $parts[2] ?? 'TBA',
];
}
return $officers;
}
protected function parseEnsembleText(string $text): array
{
$lines = explode(PHP_EOL, trim($text));
$ensembles = [];
foreach ($lines as $line) {
$parts = array_map('trim', explode(',', $line));
$ensembles[] = [
'name' => $parts[0] ?? 'TBA',
'chair' => $parts[1] ?? 'TBA',
'clinician' => $parts[2] ?? 'TBA',
];
}
return $ensembles;
}
}

View File

@ -12,5 +12,5 @@ class SiteDataItem extends Model
protected $keyType = 'string'; protected $keyType = 'string';
protected $fillable = ['key', 'value', 'type']; protected $fillable = ['key', 'value', 'type', 'description'];
} }

View File

@ -27,7 +27,11 @@ class Admin extends Component
[ [
'name' => 'Users', 'name' => 'Users',
'link' => route('admin.users.index'), 'link' => route('admin.users.index'),
] ],
[
'name' => 'Site Data',
'link' => route('admin.site-data.index'),
],
]; ];
} }

View File

@ -26,78 +26,93 @@ return [
'defaults' => [ 'defaults' => [
'concertClinicDates' => [ 'concertClinicDates' => [
'key' => 'concertClinicDates', 'key' => 'concertClinicDates',
'description' => 'Concert Clinic Dates',
'value' => 'UNSPECIFIED DATE', 'value' => 'UNSPECIFIED DATE',
'type' => 'string', 'type' => 'string',
], ],
'concertClinicLocation' => [ 'concertClinicLocation' => [
'key' => 'concertClinicLocation', 'key' => 'concertClinicLocation',
'description' => 'Concert Clinic Location',
'value' => 'UNSPECIFIED SITE', 'value' => 'UNSPECIFIED SITE',
'type' => 'string', 'type' => 'string',
], ],
'beginnerClinicDates' => [ 'beginnerClinicDates' => [
'key' => 'beginnerClinicDates', 'key' => 'beginnerClinicDates',
'description' => 'Beginner Clinic Dates',
'value' => 'UNSPECIFIED DATE', 'value' => 'UNSPECIFIED DATE',
'type' => 'string', 'type' => 'string',
], ],
'beginnerClinicLocation' => [ 'beginnerClinicLocation' => [
'key' => 'beginnerClinicLocation', 'key' => 'beginnerClinicLocation',
'description' => 'Beginner Clinic Location',
'value' => 'UNSPECIFIED SITE', 'value' => 'UNSPECIFIED SITE',
'type' => 'string', 'type' => 'string',
], ],
'concertAuditionDate' => [ 'concertAuditionDate' => [
'key' => 'concertAuditionDate', 'key' => 'concertAuditionDate',
'description' => 'Concert Audition Date',
'value' => 'UNSPECIFIED DATE', 'value' => 'UNSPECIFIED DATE',
'type' => 'string', 'type' => 'string',
], ],
'concertAuditionLocation' => [ 'concertAuditionLocation' => [
'key' => 'concertAuditionLocation', 'key' => 'concertAuditionLocation',
'description' => 'Concert Audition Location',
'value' => 'UNSPECIFIED SITE', 'value' => 'UNSPECIFIED SITE',
'type' => 'string', 'type' => 'string',
], ],
'concertEntryDeadline' => [ 'concertEntryDeadline' => [
'key' => 'concertEntryDeadline', 'key' => 'concertEntryDeadline',
'description' => 'Concert Entry Deadline',
'value' => 'UNSPECIFIED DATE', 'value' => 'UNSPECIFIED DATE',
'type' => 'string', 'type' => 'string',
], ],
'beginnerEntryDeadline' => [ 'beginnerEntryDeadline' => [
'key' => 'beginnerEntryDeadline', 'key' => 'beginnerEntryDeadline',
'description' => 'Beginner Entry Deadline',
'value' => 'UNSPECIFIED DATE', 'value' => 'UNSPECIFIED DATE',
'type' => 'string', 'type' => 'string',
], ],
'officers' => [ 'officers' => [
'key' => 'officers', 'key' => 'officers',
'description' => 'Officers',
'value' => '[{"office":"No Officers Defined","name":"No Officers Defined","school":" "}]', 'value' => '[{"office":"No Officers Defined","name":"No Officers Defined","school":" "}]',
'type' => 'json', 'type' => 'json',
], ],
'concertEnsembles' => [ 'concertEnsembles' => [
'key' => 'concertEnsembles', 'key' => 'concertEnsembles',
'description' => 'Concert Ensembles',
'value' => '[{"name":"No Ensembles Defined","chair":" ","clinician":" "}]', 'value' => '[{"name":"No Ensembles Defined","chair":" ","clinician":" "}]',
'type' => 'json', 'type' => 'json',
], ],
'beginnerEnsembles' => [ 'beginnerEnsembles' => [
'key' => 'beginnerEnsembles', 'key' => 'beginnerEnsembles',
'description' => 'Beginner Ensembles',
'value' => '[{"name":"No Ensembles Defined","chair":" ","clinician":" "}]', 'value' => '[{"name":"No Ensembles Defined","chair":" ","clinician":" "}]',
'type' => 'json', 'type' => 'json',
], ],
'jazzEnsembles' => [ 'jazzEnsembles' => [
'key' => 'jazzEnsembles', 'key' => 'jazzEnsembles',
'description' => 'Jazz Ensembles',
'value' => '[{"name":"No Ensembles Defined","chair":" ","clinician":" "}]', 'value' => '[{"name":"No Ensembles Defined","chair":" ","clinician":" "}]',
'type' => 'json', 'type' => 'json',
], ],
'concertAuditionFee' => [ 'concertAuditionFee' => [
'key' => 'concertAuditionFee', 'key' => 'concertAuditionFee',
'description' => 'Concert Entry Fee',
'value' => 'NO FEE SET', 'value' => 'NO FEE SET',
'type' => 'string', 'type' => 'string',
], ],
'concertAuditionAdditionalInformation' => [ 'concertAuditionAdditionalInformation' => [
'key' => 'concertAuditionAdditionalInformation', 'key' => 'concertAuditionAdditionalInformation',
'description' => 'Additional information regarding concert auditions',
'value' => 'NO ADDITIONAL INFORMATION SET', 'value' => 'NO ADDITIONAL INFORMATION SET',
'type' => 'string', 'type' => 'text',
], ],
'beginnerAuditionAdditionalInformation' => [ 'beginnerAuditionAdditionalInformation' => [
'key' => 'beginnerAuditionAdditionalInformation', 'key' => 'beginnerAuditionAdditionalInformation',
'description' => 'Additional information regarding beginner nomination',
'value' => 'NO ADDITIONAL INFORMATION SET', 'value' => 'NO ADDITIONAL INFORMATION SET',
'type' => 'string', 'type' => 'text',
] ],
], ],
]; ];

View File

@ -10,6 +10,7 @@ return new class extends Migration
{ {
Schema::create('site_data_items', function (Blueprint $table) { Schema::create('site_data_items', function (Blueprint $table) {
$table->string('key')->primary(); $table->string('key')->primary();
$table->string('description');
$table->text('value'); $table->text('value');
$table->string('type'); $table->string('type');
$table->timestamps(); $table->timestamps();

View File

@ -10,17 +10,18 @@ class MeobdaData2526 extends Seeder
public function run(): void public function run(): void
{ {
$defaults = [ $defaults = [
['key' => 'concertAuditionFee', 'value' => '$10.00', 'type' => 'string'], ['key' => 'concertAuditionFee', 'description' => 'Concert Entry Fee', 'value' => '$10.00', 'type' => 'string'],
['key' => 'concertClinicDates', 'value' => 'February 2-3, 2026', 'type' => 'string'], ['key' => 'concertClinicDates', 'description' => 'Concert Clinic Dates', 'value' => 'February 2-3, 2026', 'type' => 'string'],
['key' => 'concertClinicLocation', 'value' => 'Oologah High School', 'type' => 'string'], ['key' => 'concertClinicLocation', 'description' => 'Concert Clinic Location', 'value' => 'Oologah High School', 'type' => 'string'],
['key' => 'beginnerClinicDates', 'value' => 'April 7, 2026', 'type' => 'string'], ['key' => 'beginnerClinicDates', 'description' => 'Beginner Clinic Dates', 'value' => 'April 7, 2026', 'type' => 'string'],
['key' => 'beginnerClinicLocation', 'value' => 'Wagoner High School', 'type' => 'string'], ['key' => 'beginnerClinicLocation', 'description' => 'Beginner Clinic Location', 'value' => 'Wagoner High School', 'type' => 'string'],
['key' => 'concertAuditionDate', 'value' => 'January 12, 2026', 'type' => 'string'], ['key' => 'concertAuditionDate', 'description' => 'Concert Audition Date', 'value' => 'January 12, 2026', 'type' => 'string'],
['key' => 'concertAuditionLocation', 'value' => 'Wagoner High School', 'type' => 'string'], ['key' => 'concertAuditionLocation', 'description' => 'Concert Audition Location', 'value' => 'Wagoner High School', 'type' => 'string'],
['key' => 'concertEntryDeadline', 'value' => 'December 19, 2025', 'type' => 'string'], ['key' => 'concertEntryDeadline', 'description' => 'Concert Entry Deadline', 'value' => 'December 19, 2025', 'type' => 'string'],
['key' => 'beginnerEntryDeadline', 'value' => 'March 13, 2026', 'type' => 'string'], ['key' => 'beginnerEntryDeadline', 'description' => 'Beginner Entry Deadline', 'value' => 'March 13, 2026', 'type' => 'string'],
[ [
'key' => 'officers', 'key' => 'officers',
'description' => 'Officers',
'value' => json_encode([ 'value' => json_encode([
['office' => 'President', 'name' => 'Keysto Stotz', 'school' => 'Skiatook'], ['office' => 'President', 'name' => 'Keysto Stotz', 'school' => 'Skiatook'],
['office' => 'Vice President', 'name' => 'Jon Matthews', 'school' => 'Oologah'], ['office' => 'Vice President', 'name' => 'Jon Matthews', 'school' => 'Oologah'],
@ -31,6 +32,7 @@ class MeobdaData2526 extends Seeder
], ],
[ [
'key' => 'concertEnsembles', 'key' => 'concertEnsembles',
'description' => 'Concert Ensembles',
'value' => json_encode([ 'value' => json_encode([
['name' => 'High School Band', 'chair' => 'Bruce Thompson', 'clinician' => ''], ['name' => 'High School Band', 'chair' => 'Bruce Thompson', 'clinician' => ''],
['name' => 'Junior High Band', 'chair' => 'Doug Finley', 'clinician' => ''], ['name' => 'Junior High Band', 'chair' => 'Doug Finley', 'clinician' => ''],
@ -40,6 +42,7 @@ class MeobdaData2526 extends Seeder
], ],
[ [
'key' => 'beginnerEnsembles', 'key' => 'beginnerEnsembles',
'description' => 'Beginner Ensembles',
'value' => json_encode([ 'value' => json_encode([
['name' => 'First & Second Year A', 'chair' => 'Renee Roberts', 'clinician' => ''], ['name' => 'First & Second Year A', 'chair' => 'Renee Roberts', 'clinician' => ''],
['name' => 'First & Second Year B', 'chair' => 'Madison West', 'clinician' => ''], ['name' => 'First & Second Year B', 'chair' => 'Madison West', 'clinician' => ''],
@ -49,6 +52,7 @@ class MeobdaData2526 extends Seeder
], ],
[ [
'key' => 'jazzEnsembles', 'key' => 'jazzEnsembles',
'description' => 'Jazz Ensembles',
'value' => json_encode([ 'value' => json_encode([
['name' => 'Jazz Band', 'chair' => 'Eric Noble', 'clinician' => ''], ['name' => 'Jazz Band', 'chair' => 'Eric Noble', 'clinician' => ''],
]), ]),
@ -56,21 +60,23 @@ class MeobdaData2526 extends Seeder
], ],
[ [
'key' => 'concertAuditionAdditionalInformation', 'key' => 'concertAuditionAdditionalInformation',
'description' => 'Additional information regarding concert auditions',
'value' => '9th Graders may audition for the Jr. High and/or HS Band. Those auditioning for both bands must pay a $10.00 audition fee for each instrument they audition on for each band. (A 9th grade trumpet player auditioning for the Jr. High and HS band would pay $20.00; a 9th grade sax player auditioning on alto and tenor for the Jr. High and HS band would pay $40.00). 'value' => '9th Graders may audition for the Jr. High and/or HS Band. Those auditioning for both bands must pay a $10.00 audition fee for each instrument they audition on for each band. (A 9th grade trumpet player auditioning for the Jr. High and HS band would pay $20.00; a 9th grade sax player auditioning on alto and tenor for the Jr. High and HS band would pay $40.00).
9th Graders, auditioning for both Jr. High and HS, who score well enough to be in the HS band will be placed in the HS band.', 9th Graders, auditioning for both Jr. High and HS, who score well enough to be in the HS band will be placed in the HS band.',
'type' => 'string', 'type' => 'text',
], ],
[ [
'key' => 'beginnerAuditionAdditionalInformation', 'key' => 'beginnerAuditionAdditionalInformation',
'description' => 'Additional information regarding beginner nomination',
'value' => 'Students are selected by director recommendation. Directors may being up to 9 students for each band, but no more than one drummer or two alto saxes for either. Entry fee is $10.00 and includes lunch.', 'value' => 'Students are selected by director recommendation. Directors may being up to 9 students for each band, but no more than one drummer or two alto saxes for either. Entry fee is $10.00 and includes lunch.',
'type' => 'string', 'type' => 'text',
] ],
]; ];
foreach ($defaults as $item) { foreach ($defaults as $item) {
SiteDataItem::updateOrCreate( SiteDataItem::updateOrCreate(
['key' => $item['key']], ['key' => $item['key']],
['value' => $item['value'], 'type' => $item['type']] ['description' => $item['description'], 'value' => $item['value'], 'type' => $item['type']]
); );
} }
} }

View File

@ -0,0 +1,49 @@
<x-layout.admin>
@if ($errors->any())
<div class="mt-2 text-sm text-red-600 dark:text-red-400">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<x-card class="max-w-3xl mx-auto ">
<x-slot:header class="bg-brand-700!">Site Data</x-slot:header>
<x-slot:body class="bg-white text-black border border-brand-700">
<div class="space-y-4">
<x-form method="PATCH" :action="route('admin.site-data.update')">
@foreach($dataStrings as $dataItem)
<x-form.input :name="$dataItem->key" :label="$dataItem->description" :value="$dataItem->value"/>
@endforeach
@foreach($dataText as $dataItem)
<x-form.textarea :name="$dataItem->key"
:label="$dataItem->description">{{ $dataItem->value }}</x-form.textarea>
@endforeach
<x-form.textarea name="officers" label="Officers (Office, Name, School)">
{{ $officerText }}
</x-form.textarea>
<x-form.textarea name="concertEnsembles" label="Concert Ensembles (Name, Chair, Clinician)">
{{ $concertEnsemblesText }}
</x-form.textarea>
<x-form.textarea name="beginnerEnsembles" label="Beginner Ensembles (Name, Chair, Clinician)">
{{ $beginnerEnsemblesText }}
</x-form.textarea>
<x-form.textarea name="jazzEnsembles" label="Jazz Ensembles (Name, Chair, Clinician)">
{{ $jazzEnsemblesText }}
</x-form.textarea>
<div class="mt-3 text-right">
<x-form.button type="submit">Save Changes</x-form.button>
</div>
</x-form>
</div>
</x-slot:body>
</x-card>
</x-layout.admin>

View File

@ -0,0 +1,10 @@
@props(['name','id', 'label'])
<div>
<label for="{{ $id ?? $name }}" class="block text-sm/6 font-medium text-gray-900 dark:text-white">{{ $label }}</label>
<div class="mt-2">
<textarea id="{{ $id ?? $name }}" name="{{ $name }}" rows="8" class="block w-full rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:placeholder:text-gray-500 dark:focus:outline-indigo-500">{{ $slot }}</textarea>
</div>
@error($name)
<p id="{{ $id ?? $name }}-error" class="mt-2 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
@enderror
</div>

View File

@ -104,6 +104,21 @@
</div> </div>
<main class="py-10 lg:pl-72"> <main class="py-10 lg:pl-72">
@if(session('success'))
<div class="mx-4 mb-4 rounded-md bg-green-50 p-4 sm:mx-6 lg:mx-8 dark:bg-green-900/20">
<div class="flex">
<div class="shrink-0">
<svg class="size-5 text-green-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm3.857-9.809a.75.75 0 0 0-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 1 0-1.06 1.061l2.5 2.5a.75.75 0 0 0 1.137-.089l4-5.5z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm font-medium text-green-800 dark:text-green-200">{{ session('success') }}</p>
</div>
</div>
</div>
@endif
<div class="px-4 sm:px-6 lg:px-8"> <div class="px-4 sm:px-6 lg:px-8">
{{ $slot }} {{ $slot }}
</div> </div>

View File

@ -1,6 +1,7 @@
<?php <?php
use App\Http\Controllers\Admin\DashboardController; use App\Http\Controllers\Admin\DashboardController;
use App\Http\Controllers\Admin\SiteDataController;
use App\Http\Controllers\Admin\UsersController; use App\Http\Controllers\Admin\UsersController;
use App\Http\Controllers\AuditionInformationPageController; use App\Http\Controllers\AuditionInformationPageController;
use App\Http\Controllers\ClinicInformationPageController; use App\Http\Controllers\ClinicInformationPageController;
@ -13,6 +14,10 @@ Route::get('/clinic-information', ClinicInformationPageController::class)->name(
Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () { Route::middleware(['auth'])->prefix('admin')->name('admin.')->group(function () {
Route::get('/', DashboardController::class)->name('dashboard'); Route::get('/', DashboardController::class)->name('dashboard');
Route::prefix('/site-data')->name('site-data.')->group(function () {
Route::get('/', [SiteDataController::class, 'index'])->name('index');
Route::patch('/', [SiteDataController::class, 'update'])->name('update');
});
Route::prefix('/users')->name('users.')->group(function () { Route::prefix('/users')->name('users.')->group(function () {
Route::get('/', [UsersController::class, 'index'])->name('index'); Route::get('/', [UsersController::class, 'index'])->name('index');
}); });