feat: add create feature (needs some more work tho)

This commit is contained in:
unurled 2025-06-25 17:55:47 +02:00
parent ef6dadb148
commit 1485f3daf5
5 changed files with 413 additions and 1 deletions

View file

@ -0,0 +1,221 @@
<?php
namespace App\Http\Controllers;
use App\Models\Competition;
use App\Models\Field;
use App\Models\SchedulingMode;
use App\Models\Team;
use App\Models\User;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class TournamentController extends Controller
{
/**
* Show the form for creating a new tournament.
*
* @return \Inertia\Response
*/
public function create()
{
return Inertia::render('tournaments/Create');
}
/**
* Store a newly created tournament in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(Request $request)
{
// Validate the request data
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'location' => 'nullable|string|max:255',
'start_date' => 'required|date',
'end_date' => 'required|date|after_or_equal:start_date',
'registration_deadline' => 'nullable|date|before_or_equal:start_date',
'max_teams' => 'nullable|integer|min:2',
'status' => 'required|in:draft,public,private',
'team_ids' => 'nullable|array',
'team_ids.*' => 'exists:teams,id',
'field_ids' => 'nullable|array',
'field_ids.*' => 'exists:fields,id',
]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
// Create the tournament (competition)
$tournament = new Competition();
$tournament->name = $request->name;
$tournament->description = $request->description;
$tournament->location = $request->location;
$tournament->start_date = $request->start_date;
$tournament->end_date = $request->end_date;
$tournament->registration_deadline = $request->registration_deadline;
$tournament->max_teams = $request->max_teams;
$tournament->status = $request->status;
$tournament->owner = Auth::id();
$tournament->save();
// Associate teams with the tournament
if ($request->has('team_ids') && is_array($request->team_ids)) {
$tournament->teams()->attach($request->team_ids, ['status' => 'confirmed']);
}
// Associate fields with the tournament
if ($request->has('field_ids') && is_array($request->field_ids)) {
$tournament->fields()->attach($request->field_ids);
}
// Create a default scheduling mode
$schedulingMode = new SchedulingMode();
$schedulingMode->competition_id = $tournament->id;
$schedulingMode->name = 'Default';
$schedulingMode->description = 'Default scheduling mode';
$schedulingMode->algorithm = 'round_robin';
$schedulingMode->config = json_encode([
'match_duration' => 90, // 90 minutes
'break_between_matches' => 15, // 15 minutes
]);
$schedulingMode->sequence_order = 1;
$schedulingMode->status = 'active';
$schedulingMode->save();
// Set as current scheduling mode
$tournament->current_scheduling_mode_id = $schedulingMode->id;
$tournament->save();
return redirect()->route('tournaments.show', $tournament->id)
->with('success', 'Tournament created successfully!');
}
/**
* Display the specified tournament.
*
* @param \App\Models\Competition $tournament
* @return \Inertia\Response
*/
public function show(Competition $tournament)
{
// Load relationships
$tournament->load(['teams', 'fields', 'matches', 'breakPeriods']);
return Inertia::render('tournaments/Show', [
'tournament' => $tournament,
]);
}
/**
* Show the form for editing the specified tournament.
*
* @param \App\Models\Competition $tournament
* @return \Inertia\Response
*/
public function edit(Competition $tournament)
{
// Load relationships
$tournament->load(['teams', 'fields']);
// Get all fields and teams for dropdowns
$fields = Field::all();
$teams = Team::all();
return Inertia::render('tournaments/Edit', [
'tournament' => $tournament,
'fields' => $fields,
'teams' => $teams,
]);
}
/**
* Update the specified tournament in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Competition $tournament
* @return \Illuminate\Http\RedirectResponse
*/
public function update(Request $request, Competition $tournament)
{
// Check if user is tournament owner
if ($tournament->owner != Auth::id()) {
return redirect()->back()->with('error', 'You are not authorized to edit this tournament.');
}
// Validate the request data
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'description' => 'nullable|string',
'location' => 'nullable|string|max:255',
'start_date' => 'required|date',
'end_date' => 'required|date|after_or_equal:start_date',
'registration_deadline' => 'nullable|date|before_or_equal:start_date',
'max_teams' => 'nullable|integer|min:2',
'status' => 'required|in:draft,public,private',
'team_ids' => 'nullable|array',
'team_ids.*' => 'exists:teams,id',
'field_ids' => 'nullable|array',
'field_ids.*' => 'exists:fields,id',
]);
if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}
// Update the tournament
$tournament->name = $request->name;
$tournament->description = $request->description;
$tournament->location = $request->location;
$tournament->start_date = $request->start_date;
$tournament->end_date = $request->end_date;
$tournament->registration_deadline = $request->registration_deadline;
$tournament->max_teams = $request->max_teams;
$tournament->status = $request->status;
$tournament->save();
// Sync teams
if ($request->has('team_ids')) {
$teamSync = [];
foreach ($request->team_ids as $teamId) {
$teamSync[$teamId] = ['status' => 'confirmed'];
}
$tournament->teams()->sync($teamSync);
}
// Sync fields
if ($request->has('field_ids')) {
$tournament->fields()->sync($request->field_ids);
}
return redirect()->route('tournaments.show', $tournament->id)
->with('success', 'Tournament updated successfully!');
}
/**
* Remove the specified tournament from storage.
*
* @param \App\Models\Competition $tournament
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Competition $tournament)
{
// Check if user is tournament owner
if ($tournament->owner != Auth::id()) {
return redirect()->back()->with('error', 'You are not authorized to delete this tournament.');
}
// Delete the tournament
$tournament->delete();
return redirect()->route('home')
->with('success', 'Tournament deleted successfully!');
}
}

View file

@ -24,6 +24,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"concurrently": "^9.1.2",
"date-fns": "^4.1.0",
"embla-carousel-svelte": "^8.6.0",
"eslint": "^9.27.0",
"eslint-config-prettier": "^10.1.5",
@ -62,5 +63,6 @@
"@rollup/rollup-linux-x64-gnu": "4.41.1",
"@tailwindcss/oxide-linux-x64-gnu": "^4.1.8",
"lightningcss-linux-x64-gnu": "^1.30.1"
}
},
"packageManager": "pnpm@10.12.3+sha512.467df2c586056165580ad6dfb54ceaad94c5a30f80893ebdec5a44c5aa73c205ae4a5bb9d5ed6bb84ea7c249ece786642bbb49d06a307df218d03da41c317417"
}

8
pnpm-lock.yaml generated
View file

@ -50,6 +50,9 @@ importers:
concurrently:
specifier: ^9.1.2
version: 9.1.2
date-fns:
specifier: ^4.1.0
version: 4.1.0
embla-carousel-svelte:
specifier: ^8.6.0
version: 8.6.0(svelte@5.34.7)
@ -1237,6 +1240,9 @@ packages:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
engines: {node: '>=18'}
date-fns@4.1.0:
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
@ -3801,6 +3807,8 @@ snapshots:
whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0
date-fns@4.1.0: {}
dayjs@1.11.13:
optional: true

View file

@ -0,0 +1,170 @@
<script>
import Button from '@/components/ui/button/button.svelte';
import Calendar from '@/components/ui/calendar/calendar.svelte';
import Input from '@/components/ui/input/input.svelte';
import { Label } from '@/components/ui/label';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
import Textarea from '@/components/ui/textarea/textarea.svelte';
import AppLayout from '@/layouts/AppLayout.svelte';
import { router } from '@inertiajs/svelte';
import { format } from 'date-fns';
import { CalendarIcon } from 'lucide-svelte';
import { cn } from 'tailwind-variants';
// Form data
let form = $state({
name: '',
description: '',
location: '',
start_date: '',
end_date: '',
registration_deadline: '',
max_teams: 10,
status: 'draft',
field_ids: [],
team_ids: [],
});
// Form errors from backend
let errors = $state({});
// Date objects for calendar components
let startDate = $state(null);
// Status options
const statusOptions = [
{ value: 'draft', label: 'Draft' },
{ value: 'public', label: 'Public' },
{ value: 'private', label: 'Private' },
];
// Function to format dates for form submission
function formatDateForSubmission(date) {
return date ? format(date, 'yyyy-MM-dd') : '';
}
// Handle form submission
function handleSubmit(e) {
e.preventDefault();
// Update form with formatted dates
form.start_date = formatDateForSubmission(startDate);
// Submit the form
router.post('/tournaments', form, {
onError: (err) => {
errors = err;
},
onSuccess: () => {
// The redirect is handled by the controller
},
});
}
// Breadcrumbs
const breadcrumbs = [
{ title: 'Home', href: '/' },
{ title: 'Create Tournament', href: '/tournaments/create' },
];
</script>
<AppLayout {breadcrumbs}>
<div class="container mx-auto py-10">
<h1 class="text-2xl font-bold mb-6">Create New Tournament</h1>
<div class="bg-white dark:bg-gray-800 shadow-sm rounded-lg p-6">
<form onsubmit={handleSubmit} class="space-y-6">
<!-- Tournament Name -->
<div class="space-y-2">
<Label for="name">Tournament Name*</Label>
<Input id="name" type="text" bind:value={form.name} placeholder="Enter tournament name" />
{#if errors.name}
<p class="text-destructive text-sm">{errors.name}</p>
{/if}
</div>
<!-- Description -->
<div class="space-y-2">
<Label for="description">Description</Label>
<Textarea id="description" bind:value={form.description} placeholder="Enter tournament description" />
<p class="text-muted-foreground text-sm">Provide a brief description of the tournament.</p>
{#if errors.description}
<p class="text-destructive text-sm">{errors.description}</p>
{/if}
</div>
<!-- Location -->
<div class="space-y-2">
<Label for="location">Location</Label>
<Input id="location" type="text" bind:value={form.location} placeholder="Enter location" />
{#if errors.location}
<p class="text-destructive text-sm">{errors.location}</p>
{/if}
</div>
<!-- Dates Section -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Start Date -->
<div class="space-y-2">
<Label>Start Date*</Label>
<Popover>
<PopoverTrigger>
<Button variant="outline" class={cn('w-full pl-3 text-left font-normal', !startDate && 'text-muted-foreground')}>
{#if startDate}
{format(startDate, 'PPP')}
{:else}
<span>Pick a date</span>
{/if}
<CalendarIcon class="ml-auto h-4 w-4 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0" align="start">
<Calendar initialFocus type="single" bind:value={startDate} />
</PopoverContent>
</Popover>
{#if errors.start_date}
<p class="text-destructive text-sm">{errors.start_date}</p>
{/if}
</div>
</div>
<!-- Configuration Section -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Max Teams -->
<div class="space-y-2">
<Label for="max_teams">Maximum Teams</Label>
<Input id="max_teams" type="number" min="2" bind:value={form.max_teams} />
<p class="text-muted-foreground text-sm">Maximum number of teams that can participate.</p>
{#if errors.max_teams}
<p class="text-destructive text-sm">{errors.max_teams}</p>
{/if}
</div>
<!-- Status -->
<div class="space-y-2">
<Label>Tournament Status*</Label>
<Select type="single" bind:value={form.status}>
<SelectTrigger class="w-full capitalize">{form.status ? form.status : 'Select a status'}</SelectTrigger>
<SelectContent>
{#each statusOptions as option (option.value)}
<SelectItem value={option.value}>{option.label}</SelectItem>
{/each}
</SelectContent>
</Select>
<p class="text-muted-foreground text-sm">Draft: Only you can see. Public: Anyone can see. Private: Only invited users.</p>
{#if errors.status}
<p class="text-destructive text-sm">{errors.status}</p>
{/if}
</div>
</div>
<!-- Submit Buttons -->
<div class="flex justify-end gap-4 mt-8">
<Button type="button" variant="outline" href="/">Cancel</Button>
<Button type="submit">Create Tournament</Button>
</div>
</form>
</div>
</div>
</AppLayout>

View file

@ -1,6 +1,7 @@
<?php
use App\Http\Controllers\CompetitionController;
use App\Http\Controllers\TournamentController;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
@ -22,5 +23,15 @@ Route::get('dashboard', function () {
return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
// Tournament routes
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('tournaments/create', [TournamentController::class, 'create'])->name('tournaments.create');
Route::post('tournaments', [TournamentController::class, 'store'])->name('tournaments.store');
Route::get('tournaments/{tournament}', [TournamentController::class, 'show'])->name('tournaments.show');
Route::get('tournaments/{tournament}/edit', [TournamentController::class, 'edit'])->name('tournaments.edit');
Route::put('tournaments/{tournament}', [TournamentController::class, 'update'])->name('tournaments.update');
Route::delete('tournaments/{tournament}', [TournamentController::class, 'destroy'])->name('tournaments.destroy');
});
require __DIR__ . '/settings.php';
require __DIR__ . '/auth.php';