add round creation

This commit is contained in:
unurled 2025-07-06 14:56:26 +02:00
parent 9cc0ac3089
commit 764d0c0526
25 changed files with 356 additions and 27 deletions

View file

@ -30,6 +30,7 @@
"bits-ui": "^2.8.6",
"clsx": "^2.1.1",
"drizzle-kit": "^0.30.2",
"formsnap": "^2.0.1",
"mode-watcher": "^1.1.0",
"oxlint": "^1.5.0",
"prettier": "^3.4.2",
@ -38,6 +39,7 @@
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-sonner": "^1.0.5",
"sveltekit-superforms": "^2.26.1",
"tailwind-merge": "^3.3.1",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.0.0",

8
src/app.d.ts vendored
View file

@ -30,3 +30,11 @@ export interface NavItem {
isActive?: boolean;
requireAdmin?: boolean;
}
export enum SchedulingMode {
single = 'signle',
double = 'double',
swiss = 'swiss',
round_robin = 'round_robin',
double_round_robin = 'double_round_robin'
}

View file

@ -9,7 +9,7 @@
import { cn } from '$lib/utils';
import { getContext } from 'svelte';
import Nav from '$lib/components/nav.svelte';
import type { User } from '@/server/db/schema/users';
import type { User } from '$lib/server/db/schema/users';
let {
breadcrumbs,

View file

@ -4,8 +4,8 @@
import { Button } from '$lib/components/ui/button';
import type { NavItem } from '../../app';
import { Home, Trophy, Calendar, UserCircle, Settings } from 'lucide-svelte';
import type { User } from '@/server/db/schema/users';
import { type Role } from '@/server/db/schema/roles';
import type { User } from '$lib/server/db/schema/users';
import { type Role } from '$lib/server/db/schema/roles';
import { page } from '$app/state';
let { className }: { className?: string } = $props();

View file

@ -0,0 +1,41 @@
<script lang="ts">
import { superForm, type Infer, type SuperValidated } from 'sveltekit-superforms';
import { FormControl } from '../ui/form';
import FormDescription from '../ui/form/form-description.svelte';
import FormFieldErrors from '../ui/form/form-field-errors.svelte';
import FormField from '../ui/form/form-field.svelte';
import FormLabel from '../ui/form/form-label.svelte';
import { formSchema, type FormSchema } from './create-schema';
import { zodClient } from 'sveltekit-superforms/adapters';
import RadioGroup from '../ui/radio-group/radio-group.svelte';
import RadioGroupItem from '../ui/radio-group/radio-group-item.svelte';
import { SchedulingMode } from '@/app';
let { data }: { data: { form: SuperValidated<Infer<FormSchema>> } } = $props();
const form = superForm(data.form, {
validators: zodClient(formSchema)
});
const { form: formData, enhance } = form;
</script>
<form method="post" use:enhance>
<FormField {form} name="scheduling_mode">
<RadioGroup required bind:value={$formData.scheduling_mode} name="scheduling_mode">
{#each Object.values(SchedulingMode) as mode}
<div class="flex items-center gap-4">
<FormControl>
{#snippet children({ props })}
<RadioGroupItem value={mode} {...props} />
<FormLabel>{mode}</FormLabel>
{/snippet}
</FormControl>
</div>
{/each}
</RadioGroup>
<FormDescription />
<FormFieldErrors />
</FormField>
</form>

View file

@ -0,0 +1,7 @@
import z from 'zod';
export const formSchema = z.object({
scheduling_mode: z.nativeEnum(SchedulingMode)
});
export type FormSchema = typeof formSchema;

View file

@ -0,0 +1,10 @@
<script lang="ts">
import CreateRound from './create-round.svelte';
let { rounds, createForm } = $props();
</script>
{JSON.stringify(rounds)}
<div></div>
<CreateRound data={{ form: createForm }} />

View file

@ -0,0 +1,7 @@
<script lang="ts">
import * as Button from "$lib/components/ui/button/index.js";
let { ref = $bindable(null), ...restProps }: Button.Props = $props();
</script>
<Button.Root bind:ref type="submit" {...restProps} />

View file

@ -0,0 +1,17 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChild<FormPrimitive.DescriptionProps> = $props();
</script>
<FormPrimitive.Description
bind:ref
data-slot="form-description"
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

View file

@ -0,0 +1,24 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPathLeaves<T>">
import * as FormPrimitive from "formsnap";
import type { FormPathLeaves } from "sveltekit-superforms";
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
form,
name,
children: childrenProp,
...restProps
}: WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> &
FormPrimitive.ElementFieldProps<T, U> = $props();
</script>
<FormPrimitive.ElementField {form} {name}>
{#snippet children({ constraints, errors, tainted, value })}
<div bind:this={ref} class={cn("space-y-2", className)} {...restProps}>
{@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
</div>
{/snippet}
</FormPrimitive.ElementField>

View file

@ -0,0 +1,30 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
errorClasses,
children: childrenProp,
...restProps
}: WithoutChild<FormPrimitive.FieldErrorsProps> & {
errorClasses?: string | undefined | null;
} = $props();
</script>
<FormPrimitive.FieldErrors
bind:ref
class={cn("text-destructive text-sm font-medium", className)}
{...restProps}
>
{#snippet children({ errors, errorProps })}
{#if childrenProp}
{@render childrenProp({ errors, errorProps })}
{:else}
{#each errors as error (error)}
<div {...errorProps} class={cn(errorClasses)}>{error}</div>
{/each}
{/if}
{/snippet}
</FormPrimitive.FieldErrors>

View file

@ -0,0 +1,29 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from "formsnap";
import type { FormPath } from "sveltekit-superforms";
import { cn, type WithElementRef, type WithoutChildren } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
form,
name,
children: childrenProp,
...restProps
}: FormPrimitive.FieldProps<T, U> &
WithoutChildren<WithElementRef<HTMLAttributes<HTMLDivElement>>> = $props();
</script>
<FormPrimitive.Field {form} {name}>
{#snippet children({ constraints, errors, tainted, value })}
<div
bind:this={ref}
data-slot="form-item"
class={cn("space-y-2", className)}
{...restProps}
>
{@render childrenProp?.({ constraints, errors, tainted, value: value as T[U] })}
</div>
{/snippet}
</FormPrimitive.Field>

View file

@ -0,0 +1,15 @@
<script lang="ts" generics="T extends Record<string, unknown>, U extends FormPath<T>">
import * as FormPrimitive from "formsnap";
import type { FormPath } from "sveltekit-superforms";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
form,
name,
...restProps
}: WithoutChild<FormPrimitive.FieldsetProps<T, U>> = $props();
</script>
<FormPrimitive.Fieldset bind:ref {form} {name} class={cn("space-y-2", className)} {...restProps} />

View file

@ -0,0 +1,24 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { Label } from "$lib/components/ui/label/index.js";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
children,
class: className,
...restProps
}: WithoutChild<FormPrimitive.LabelProps> = $props();
</script>
<FormPrimitive.Label {...restProps} bind:ref>
{#snippet child({ props })}
<Label
{...props}
data-slot="form-label"
class={cn("data-[fs-error]:text-destructive", className)}
>
{@render children?.()}
</Label>
{/snippet}
</FormPrimitive.Label>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import * as FormPrimitive from "formsnap";
import { cn, type WithoutChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChild<FormPrimitive.LegendProps> = $props();
</script>
<FormPrimitive.Legend
bind:ref
class={cn("data-[fs-error]:text-destructive text-sm font-medium leading-none", className)}
{...restProps}
/>

View file

@ -0,0 +1,33 @@
import * as FormPrimitive from "formsnap";
import Description from "./form-description.svelte";
import Label from "./form-label.svelte";
import FieldErrors from "./form-field-errors.svelte";
import Field from "./form-field.svelte";
import Fieldset from "./form-fieldset.svelte";
import Legend from "./form-legend.svelte";
import ElementField from "./form-element-field.svelte";
import Button from "./form-button.svelte";
const Control = FormPrimitive.Control;
export {
Field,
Control,
Label,
Button,
FieldErrors,
Description,
Fieldset,
Legend,
ElementField,
//
Field as FormField,
Control as FormControl,
Description as FormDescription,
Label as FormLabel,
FieldErrors as FormFieldErrors,
Fieldset as FormFieldset,
Legend as FormLegend,
ElementField as FormElementField,
Button as FormButton,
};

View file

@ -0,0 +1,10 @@
import Root from "./radio-group.svelte";
import Item from "./radio-group-item.svelte";
export {
Root,
Item,
//
Root as RadioGroup,
Item as RadioGroupItem,
};

View file

@ -0,0 +1,31 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
import CircleIcon from "@lucide/svelte/icons/circle";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: WithoutChildrenOrChild<RadioGroupPrimitive.ItemProps> = $props();
</script>
<RadioGroupPrimitive.Item
bind:ref
data-slot="radio-group-item"
class={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 shadow-xs aspect-square size-4 shrink-0 rounded-full border outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...restProps}
>
{#snippet children({ checked })}
<div data-slot="radio-group-indicator" class="relative flex items-center justify-center">
{#if checked}
<CircleIcon
class="fill-primary absolute left-1/2 top-1/2 size-2 -translate-x-1/2 -translate-y-1/2"
/>
{/if}
</div>
{/snippet}
</RadioGroupPrimitive.Item>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { RadioGroup as RadioGroupPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
value = $bindable(""),
...restProps
}: RadioGroupPrimitive.RootProps = $props();
</script>
<RadioGroupPrimitive.Root
bind:ref
bind:value
data-slot="radio-group"
class={cn("grid gap-3", className)}
{...restProps}
/>

View file

@ -3,7 +3,7 @@
import '../app.css';
import { setContext } from 'svelte';
import type { PageProps } from './$types';
import { Toaster } from '@/components/ui/sonner';
import { Toaster } from '$lib/components/ui/sonner';
let {
children,

View file

@ -1,4 +1,4 @@
import { getCompetitionsWithAll } from '@/server/db/schema/competitions';
import { getCompetitionsWithAll } from '$lib/server/db/schema/competitions';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {

View file

@ -16,7 +16,7 @@
import { Skeleton } from '$lib/components/ui/skeleton';
import type { PageProps } from './$types.js';
import { toast } from 'svelte-sonner';
import { formatDate } from '@/utils';
import { formatDate } from '$lib/utils';
const breadcrumbs: BreadcrumbItem[] = [
{

View file

@ -1,10 +1,13 @@
import { db } from '@/server/db';
import { db } from '$lib/server/db';
import type { Actions, PageServerLoad } from './$types';
import { competitions, getCompetitionWithAll } from '@/server/db/schema/competitions';
import { competitions, getCompetitionWithAll } from '$lib/server/db/schema/competitions';
import { error, redirect } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import { and } from 'drizzle-orm';
import { teams } from '@/server/db/schema/teams';
import { teams } from '$lib/server/db/schema/teams';
import { superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import { formSchema } from '$lib/components/rounds/create-schema';
export const load: PageServerLoad = async ({ params, locals }) => {
try {
@ -21,7 +24,8 @@ export const load: PageServerLoad = async ({ params, locals }) => {
}
return {
competition: competition
competition: competition,
createForm: await superValidate(zod(formSchema))
};
} catch (e) {
return redirect(302, '/');

View file

@ -1,21 +1,22 @@
<script lang="ts">
import Badge from '@/components/ui/badge/badge.svelte';
import Badge from '$lib/components/ui/badge/badge.svelte';
import type { PageProps } from './$types';
import { CalendarDays, MapPin, Users, Trophy, Clock, Trash2Icon } from 'lucide-svelte';
import { formatDate } from '@/utils';
import Button from '@/components/ui/button/button.svelte';
import Card from '@/components/ui/card/card.svelte';
import CardHeader from '@/components/ui/card/card-header.svelte';
import CardContent from '@/components/ui/card/card-content.svelte';
import CardFooter from '@/components/ui/card/card-footer.svelte';
import Input from '@/components/ui/input/input.svelte';
import { formatDate } from '$lib/utils';
import Button from '$lib/components/ui/button/button.svelte';
import Card from '$lib/components/ui/card/card.svelte';
import CardHeader from '$lib/components/ui/card/card-header.svelte';
import CardContent from '$lib/components/ui/card/card-content.svelte';
import CardFooter from '$lib/components/ui/card/card-footer.svelte';
import Input from '$lib/components/ui/input/input.svelte';
import { enhance } from '$app/forms';
import Textarea from '@/components/ui/textarea/textarea.svelte';
import Tabs from '@/components/ui/tabs/tabs.svelte';
import TabsList from '@/components/ui/tabs/tabs-list.svelte';
import TabsTrigger from '@/components/ui/tabs/tabs-trigger.svelte';
import TabsContent from '@/components/ui/tabs/tabs-content.svelte';
import Textarea from '$lib/components/ui/textarea/textarea.svelte';
import Tabs from '$lib/components/ui/tabs/tabs.svelte';
import TabsList from '$lib/components/ui/tabs/tabs-list.svelte';
import TabsTrigger from '$lib/components/ui/tabs/tabs-trigger.svelte';
import TabsContent from '$lib/components/ui/tabs/tabs-content.svelte';
import { Svelvet } from 'svelvet';
import Rounds from '$lib/components/rounds/rounds.svelte';
let { data }: PageProps = $props();
@ -58,7 +59,7 @@
</div>
</div>
<Tabs value="general">
<Tabs value="rounds">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="rounds">Rounds</TabsTrigger>
@ -165,7 +166,9 @@
</TabsContent>
<TabsContent value="rounds">
<Svelvet id="rounds-canvas" TD minimap controls locked></Svelvet>
{#if !data.competition.rounds || data.competition.rounds.length === 0}
<Rounds createForm={data.createForm} rounds={data.competition.rounds} />
{/if}
</TabsContent>
</Tabs>
</div>

View file

@ -9,8 +9,7 @@ const config = {
kit: {
adapter: adapter(),
alias: {
'@/*': './src/lib/*',
'~': './src/'
'@/*': './src/*'
}
}
};