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 $fillable = ['key', 'value', 'type'];
protected $fillable = ['key', 'value', 'type', 'description'];
}

View File

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

View File

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

View File

@ -10,17 +10,18 @@ class MeobdaData2526 extends Seeder
public function run(): void
{
$defaults = [
['key' => 'concertAuditionFee', 'value' => '$10.00', 'type' => 'string'],
['key' => 'concertClinicDates', 'value' => 'February 2-3, 2026', 'type' => 'string'],
['key' => 'concertClinicLocation', 'value' => 'Oologah High School', 'type' => 'string'],
['key' => 'beginnerClinicDates', 'value' => 'April 7, 2026', 'type' => 'string'],
['key' => 'beginnerClinicLocation', 'value' => 'Wagoner High School', 'type' => 'string'],
['key' => 'concertAuditionDate', 'value' => 'January 12, 2026', 'type' => 'string'],
['key' => 'concertAuditionLocation', 'value' => 'Wagoner High School', 'type' => 'string'],
['key' => 'concertEntryDeadline', 'value' => 'December 19, 2025', 'type' => 'string'],
['key' => 'beginnerEntryDeadline', 'value' => 'March 13, 2026', 'type' => 'string'],
['key' => 'concertAuditionFee', 'description' => 'Concert Entry Fee', 'value' => '$10.00', 'type' => 'string'],
['key' => 'concertClinicDates', 'description' => 'Concert Clinic Dates', 'value' => 'February 2-3, 2026', 'type' => 'string'],
['key' => 'concertClinicLocation', 'description' => 'Concert Clinic Location', 'value' => 'Oologah High School', 'type' => 'string'],
['key' => 'beginnerClinicDates', 'description' => 'Beginner Clinic Dates', 'value' => 'April 7, 2026', 'type' => 'string'],
['key' => 'beginnerClinicLocation', 'description' => 'Beginner Clinic Location', 'value' => 'Wagoner High School', 'type' => 'string'],
['key' => 'concertAuditionDate', 'description' => 'Concert Audition Date', 'value' => 'January 12, 2026', 'type' => 'string'],
['key' => 'concertAuditionLocation', 'description' => 'Concert Audition Location', 'value' => 'Wagoner High School', 'type' => 'string'],
['key' => 'concertEntryDeadline', 'description' => 'Concert Entry Deadline', 'value' => 'December 19, 2025', 'type' => 'string'],
['key' => 'beginnerEntryDeadline', 'description' => 'Beginner Entry Deadline', 'value' => 'March 13, 2026', 'type' => 'string'],
[
'key' => 'officers',
'description' => 'Officers',
'value' => json_encode([
['office' => 'President', 'name' => 'Keysto Stotz', 'school' => 'Skiatook'],
['office' => 'Vice President', 'name' => 'Jon Matthews', 'school' => 'Oologah'],
@ -31,6 +32,7 @@ class MeobdaData2526 extends Seeder
],
[
'key' => 'concertEnsembles',
'description' => 'Concert Ensembles',
'value' => json_encode([
['name' => 'High School Band', 'chair' => 'Bruce Thompson', 'clinician' => ''],
['name' => 'Junior High Band', 'chair' => 'Doug Finley', 'clinician' => ''],
@ -40,6 +42,7 @@ class MeobdaData2526 extends Seeder
],
[
'key' => 'beginnerEnsembles',
'description' => 'Beginner Ensembles',
'value' => json_encode([
['name' => 'First & Second Year A', 'chair' => 'Renee Roberts', 'clinician' => ''],
['name' => 'First & Second Year B', 'chair' => 'Madison West', 'clinician' => ''],
@ -49,6 +52,7 @@ class MeobdaData2526 extends Seeder
],
[
'key' => 'jazzEnsembles',
'description' => 'Jazz Ensembles',
'value' => json_encode([
['name' => 'Jazz Band', 'chair' => 'Eric Noble', 'clinician' => ''],
]),
@ -56,21 +60,23 @@ class MeobdaData2526 extends Seeder
],
[
'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).
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',
'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.',
'type' => 'string',
]
'type' => 'text',
],
];
foreach ($defaults as $item) {
SiteDataItem::updateOrCreate(
['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>
<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">
{{ $slot }}
</div>

View File

@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\Admin\DashboardController;
use App\Http\Controllers\Admin\SiteDataController;
use App\Http\Controllers\Admin\UsersController;
use App\Http\Controllers\AuditionInformationPageController;
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::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::get('/', [UsersController::class, 'index'])->name('index');
});