diff --git a/.env b/.env index 3f47c3b..93365f7 100644 --- a/.env +++ b/.env @@ -5,4 +5,4 @@ ADMIN_PASSWORD=admin JWT_COOKIE_SECURE=false ENABLE_HTTPS_REDIRECT=false WEB_PORT=3000 -CSRF_ALLOWED_ORIGINS=http://192.168.1.204:3000 +CSRF_ALLOWED_ORIGINS=http://localhost:3000 diff --git a/api/src/models/tournament.rs b/api/src/models/tournament.rs index 5d5ba6b..c930270 100644 --- a/api/src/models/tournament.rs +++ b/api/src/models/tournament.rs @@ -4,6 +4,10 @@ use sqlx::FromRow; use std::collections::HashMap; use std::collections::HashSet; +pub const ATTENDANCE_ID_FIELD_ID: &str = "attendance-id"; +pub const ATTENDANCE_ID_FIELD_LABEL: &str = "Deltagar-ID"; +pub const ATTENDANCE_ID_FIELD_PLACEHOLDER: &str = "Ange ditt deltagar-ID från närvarolistan"; + #[derive(Debug, FromRow, Clone)] pub struct TournamentInfo { pub id: i32, @@ -173,6 +177,7 @@ impl TournamentSignupConfig { } self.entry_fields = normalize_signup_fields(self.entry_fields); + ensure_attendance_id_field(&mut self.entry_fields); self.participant_fields = normalize_signup_fields(self.participant_fields); self @@ -213,6 +218,65 @@ fn normalize_signup_fields(mut fields: Vec) -> Vec) { + let mut attendance_index = None; + for (index, field) in fields.iter_mut().enumerate() { + if field.id == ATTENDANCE_ID_FIELD_ID { + attendance_index = Some(index); + sanitize_attendance_id_field(field); + break; + } + } + + match attendance_index { + Some(index) => { + if index != 0 { + let field = fields.remove(index); + fields.insert(0, field); + } + } + None => { + fields.insert(0, default_attendance_id_field()); + } + } +} + +fn sanitize_attendance_id_field(field: &mut TournamentSignupField) { + field.id = ATTENDANCE_ID_FIELD_ID.to_string(); + field.label = sanitize_attendance_label(&field.label); + field.field_type = TournamentFieldType::Text; + field.required = true; + field.unique = true; + field.placeholder = Some( + field + .placeholder + .as_ref() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| ATTENDANCE_ID_FIELD_PLACEHOLDER.to_string()), + ); +} + +fn sanitize_attendance_label(label: &str) -> String { + let trimmed = label.trim(); + if trimmed.is_empty() { + ATTENDANCE_ID_FIELD_LABEL.to_string() + } else { + trimmed.to_string() + } +} + +fn default_attendance_id_field() -> TournamentSignupField { + TournamentSignupField { + id: ATTENDANCE_ID_FIELD_ID.to_string(), + label: ATTENDANCE_ID_FIELD_LABEL.to_string(), + field_type: TournamentFieldType::Text, + required: true, + placeholder: Some(ATTENDANCE_ID_FIELD_PLACEHOLDER.to_string()), + unique: true, + } +} + fn normalize_field_id(input: &str) -> String { let mut slug = input .trim() diff --git a/api/src/routes/tournaments.rs b/api/src/routes/tournaments.rs index bda7e2e..6bc973e 100644 --- a/api/src/routes/tournaments.rs +++ b/api/src/routes/tournaments.rs @@ -1,13 +1,14 @@ use crate::auth::AuthUser; use crate::error::ApiError; use crate::models::{ - AppEvent, CreateTournamentRequest, TournamentFieldType, TournamentInfo, TournamentInfoData, - TournamentItemResponse, TournamentListResponse, TournamentParticipantRow, + AppEvent, CreateTournamentRequest, Person, TournamentFieldType, TournamentInfo, + TournamentInfoData, TournamentItemResponse, TournamentListResponse, TournamentParticipantRow, TournamentParticipantValueRow, TournamentRegistrationDetailResponse, TournamentRegistrationItem, TournamentRegistrationListResponse, TournamentRegistrationResponse, TournamentRegistrationRow, TournamentRegistrationValueRow, TournamentSection, TournamentSectionRecord, TournamentSignupConfig, TournamentSignupField, TournamentSignupFieldRecord, TournamentSignupSubmission, UpdateTournamentRequest, + ATTENDANCE_ID_FIELD_ID, }; use crate::AppState; use rocket::http::Status; @@ -107,6 +108,46 @@ fn build_registration_url(slug: &str) -> String { format!("/tournament/{slug}") } +fn first_non_attendance_entry_value( + fields: &[TournamentSignupField], + values: &HashMap, +) -> Option { + for field in fields { + if field.id == ATTENDANCE_ID_FIELD_ID { + continue; + } + if let Some(value) = values.get(&field.id) { + let trimmed = value.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + } + None +} + +fn build_entry_label( + attendance_id: i32, + person: &Person, + config: &TournamentSignupConfig, + entry_values: &HashMap, +) -> Option { + let primary = first_non_attendance_entry_value(&config.entry_fields, entry_values) + .unwrap_or_else(|| { + let first_name = person.first_name.trim(); + let last_name = person.last_name.trim(); + let full = format!("{first_name} {last_name}"); + full.trim().to_string() + }); + + let label = format!("{attendance_id} – {primary}").trim().to_string(); + if label.is_empty() { + None + } else { + Some(label) + } +} + fn signup_fields_equal(left: &[TournamentSignupField], right: &[TournamentSignupField]) -> bool { if left.len() != right.len() { return false; @@ -219,6 +260,7 @@ fn build_signup_config( entry_fields, participant_fields, } + .normalized() } async fn load_tournament_data( @@ -1062,12 +1104,6 @@ fn validate_submission( Ok(()) } -fn trimmed(value: Option<&String>) -> Option { - value - .map(|v| v.trim().to_string()) - .filter(|v| !v.is_empty()) -} - #[rocket::post("/slug//signup", data = "")] pub async fn create_registration_by_slug( state: &rocket::State, @@ -1136,42 +1172,54 @@ pub async fn create_registration_by_slug( let mut tx = state.db.begin().await?; - let entry_label = config - .entry_fields - .first() - .and_then(|field| entry_values.get(&field.id)) - .and_then(|value| trimmed(Some(value))); - - let entry_label_requires_unique = config - .entry_fields - .first() - .map(|field| field.unique) - .unwrap_or(false); - - if entry_label_requires_unique { - if let Some(label) = entry_label.clone() { - let is_duplicate = sqlx::query_scalar::<_, bool>( - r#" - SELECT EXISTS ( - SELECT 1 - FROM tournament_registrations - WHERE tournament_id = $1 AND entry_label = $2 - ) - "#, - ) - .bind(info.id) - .bind(&label) - .fetch_one(&state.db) - .await?; - - if is_duplicate { - return Err(ApiError::bad_request( - "Den här spelaren eller laget är redan anmäld till turneringen.", - )); - } - } + if !field_map.contains_key(ATTENDANCE_ID_FIELD_ID) { + return Err(ApiError::bad_request( + "Turneringen är felkonfigurerad och saknar obligatoriskt deltagar-ID-fält.", + )); } + let attendance_id_value = entry_values + .get(ATTENDANCE_ID_FIELD_ID) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .ok_or_else(|| ApiError::bad_request("Ange ditt deltagar-ID från närvarolistan."))?; + + let attendance_id: i32 = attendance_id_value.parse().map_err(|_| { + ApiError::bad_request("Deltagar-ID måste vara ett heltal från närvarolistan.") + })?; + + let person = sqlx::query_as::<_, Person>( + r#" + SELECT + id, + first_name, + last_name, + grade, + parent_name, + parent_phone_number, + checked_in, + inside, + visitor, + sleeping_spot + FROM persons + WHERE id = $1 + "#, + ) + .bind(attendance_id) + .fetch_optional(&mut *tx) + .await? + .ok_or_else(|| { + ApiError::bad_request("Det angivna deltagar-ID:t finns inte i närvarolistan.") + })?; + + let canonical_attendance = attendance_id.to_string(); + entry_values.insert( + ATTENDANCE_ID_FIELD_ID.to_string(), + canonical_attendance.clone(), + ); + + let entry_label = build_entry_label(attendance_id, &person, &config, &entry_values); + for field in &config.entry_fields { if !field.unique { continue; @@ -1192,7 +1240,7 @@ pub async fn create_registration_by_slug( WHERE r.tournament_id = $1 AND v.signup_field_id = $2 AND v.value = $3 - ) + ) "#, ) .bind(info.id) @@ -1602,45 +1650,53 @@ pub async fn update_registration_by_slug( let mut tx = state.db.begin().await?; - let entry_label = config - .entry_fields - .first() - .and_then(|field| entry_values.get(&field.id)) - .and_then(|value| trimmed(Some(value))); - - let entry_label_requires_unique = config - .entry_fields - .first() - .map(|field| field.unique) - .unwrap_or(false); - - if entry_label_requires_unique { - if let Some(label) = entry_label.clone() { - let is_duplicate = sqlx::query_scalar::<_, bool>( - r#" - SELECT EXISTS ( - SELECT 1 - FROM tournament_registrations - WHERE tournament_id = $1 - AND entry_label = $2 - AND id <> $3 - ) - "#, - ) - .bind(info.id) - .bind(&label) - .bind(registration.id) - .fetch_one(&state.db) - .await?; - - if is_duplicate { - return Err(ApiError::bad_request( - "Den här spelaren eller laget är redan anmäld till turneringen.", - )); - } - } + if !field_map.contains_key(ATTENDANCE_ID_FIELD_ID) { + return Err(ApiError::bad_request( + "Turneringen är felkonfigurerad och saknar obligatoriskt deltagar-ID-fält.", + )); } + let attendance_id_value = entry_values + .get(ATTENDANCE_ID_FIELD_ID) + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) + .ok_or_else(|| ApiError::bad_request("Ange ditt deltagar-ID från närvarolistan."))?; + + let attendance_id: i32 = attendance_id_value.parse().map_err(|_| { + ApiError::bad_request("Deltagar-ID måste vara ett heltal från närvarolistan.") + })?; + + let person = sqlx::query_as::<_, Person>( + r#" + SELECT + id, + first_name, + last_name, + grade, + parent_name, + parent_phone_number, + checked_in, + inside, + visitor, + sleeping_spot + FROM persons + WHERE id = $1 + "#, + ) + .bind(attendance_id) + .fetch_optional(&mut *tx) + .await? + .ok_or_else(|| { + ApiError::bad_request("Det angivna deltagar-ID:t finns inte i närvarolistan.") + })?; + + entry_values.insert( + ATTENDANCE_ID_FIELD_ID.to_string(), + attendance_id.to_string(), + ); + + let entry_label = build_entry_label(attendance_id, &person, &config, &entry_values); + for field in &config.entry_fields { if !field.unique { continue; diff --git a/web/bun.lock b/web/bun.lock index 5264d16..3beb7ca 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -8,6 +8,7 @@ "@sveltejs/kit": "^2.22.0", "@sveltejs/vite-plugin-svelte": "^6.0.0", "@tailwindcss/vite": "^4.0.0", + "dotenv": "^16.4.5", "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.3", "prettier-plugin-tailwindcss": "^0.6.11", @@ -210,6 +211,8 @@ "devalue": ["devalue@5.3.2", "", {}, "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], "esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="], diff --git a/web/src/routes/(admin)/admin/tournament/+page.svelte b/web/src/routes/(admin)/admin/tournament/+page.svelte index 56909a1..46dd15c 100644 --- a/web/src/routes/(admin)/admin/tournament/+page.svelte +++ b/web/src/routes/(admin)/admin/tournament/+page.svelte @@ -16,9 +16,28 @@ type TabKey = 'overview' | 'create' | 'manage'; -type SignupFieldForm = TournamentSignupField & { - placeholder: string; -}; + type SignupFieldForm = TournamentSignupField & { + placeholder: string; + }; + + const ATTENDANCE_FIELD_ID = 'attendance-id'; + const ATTENDANCE_FIELD_LABEL = 'Deltagar-ID'; + const ATTENDANCE_FIELD_PLACEHOLDER = 'Ange ditt deltagar-ID från närvarolistan'; + + function isAttendanceField(field: { id: string }): boolean { + return field.id === ATTENDANCE_FIELD_ID; + } + + function createAttendanceField(): SignupFieldForm { + return { + id: ATTENDANCE_FIELD_ID, + label: ATTENDANCE_FIELD_LABEL, + field_type: 'text', + required: true, + placeholder: ATTENDANCE_FIELD_PLACEHOLDER, + unique: true + }; + } type SignupConfigForm = { mode: 'solo' | 'team'; @@ -59,65 +78,111 @@ type SignupFieldForm = TournamentSignupField & { return sections.map((section) => ({ title: section.title, body: section.body })); } -const fieldTypeOptions: { value: TournamentFieldType; label: string }[] = [ - { value: 'text', label: 'Text' }, - { value: 'email', label: 'E-post' }, - { value: 'tel', label: 'Telefon' }, - { value: 'discord', label: 'Discord' } -]; + const fieldTypeOptions: { value: TournamentFieldType; label: string }[] = [ + { value: 'text', label: 'Text' }, + { value: 'email', label: 'E-post' }, + { value: 'tel', label: 'Telefon' }, + { value: 'discord', label: 'Discord' } + ]; -let fieldIdCounter = 0; + let fieldIdCounter = 0; -function nextFieldId(label: string) { - const base = slugify(label) || 'field'; - fieldIdCounter += 1; - return `${base}-${fieldIdCounter.toString(36)}`; -} + function nextFieldId(label: string) { + const base = slugify(label) || 'field'; + fieldIdCounter += 1; + return `${base}-${fieldIdCounter.toString(36)}`; + } -function createSignupField(label: string, field_type: TournamentFieldType = 'text'): SignupFieldForm { - return { - id: nextFieldId(label), - label, - field_type, - required: true, - placeholder: '', - unique: false - }; -} + function createSignupField( + label: string, + field_type: TournamentFieldType = 'text' + ): SignupFieldForm { + return { + id: nextFieldId(label), + label, + field_type, + required: true, + placeholder: '', + unique: false + }; + } -function cloneSignupField(field: TournamentSignupField): SignupFieldForm { - return { - id: field.id || nextFieldId(field.label), - label: field.label, - field_type: field.field_type ?? 'text', - required: Boolean(field.required), - placeholder: field.placeholder ?? '', - unique: Boolean(field.unique) - }; -} + function cloneSignupField(field: TournamentSignupField): SignupFieldForm { + if (field.id === ATTENDANCE_FIELD_ID) { + const label = field.label?.trim() || ATTENDANCE_FIELD_LABEL; + const placeholder = (field.placeholder ?? '').trim() || ATTENDANCE_FIELD_PLACEHOLDER; + return { + id: ATTENDANCE_FIELD_ID, + label, + field_type: 'text', + required: true, + placeholder, + unique: true + }; + } + return { + id: field.id || nextFieldId(field.label), + label: field.label, + field_type: field.field_type ?? 'text', + required: Boolean(field.required), + placeholder: field.placeholder ?? '', + unique: Boolean(field.unique) + }; + } + + function ensureAttendanceField(fields: SignupFieldForm[]): SignupFieldForm[] { + let attendanceField: SignupFieldForm | null = null; + const others: SignupFieldForm[] = []; + + for (const field of fields) { + if (isAttendanceField(field)) { + const label = field.label.trim() || ATTENDANCE_FIELD_LABEL; + const placeholder = field.placeholder.trim() || ATTENDANCE_FIELD_PLACEHOLDER; + attendanceField = { + id: ATTENDANCE_FIELD_ID, + label, + field_type: 'text', + required: true, + placeholder, + unique: true + }; + } else { + others.push(field); + } + } + + if (!attendanceField) { + attendanceField = createAttendanceField(); + } + + return [attendanceField, ...others]; + } function createDefaultSignup(): SignupConfigForm { return { mode: 'solo', team_size: { min: 1, max: 1 }, - entry_fields: [createSignupField('Lag / spelarnamn')], + entry_fields: ensureAttendanceField([createSignupField('Lag / spelarnamn')]), participant_fields: [] }; } function cloneSignupConfig(config: TournamentSignupConfig | null | undefined): SignupConfigForm { if (!config) return createDefaultSignup(); - const entry_fields = (config.entry_fields ?? []).map(cloneSignupField); - const participant_fields = (config.participant_fields ?? []).map(cloneSignupField); - if (entry_fields.length === 0) { - entry_fields.push(createSignupField('Lag/Spelarnamn')); + let entry_fields = ensureAttendanceField((config.entry_fields ?? []).map(cloneSignupField)); + let participant_fields = (config.participant_fields ?? []).map(cloneSignupField); + if (entry_fields.length === 1) { + entry_fields = ensureAttendanceField([ + ...entry_fields, + createSignupField('Lag / spelarnamn') + ]); } if (participant_fields.length === 0) { participant_fields.push(createSignupField('Spelare')); } const mode = config.mode === 'team' ? 'team' : 'solo'; if (mode === 'solo') { - participant_fields.length = 0; + participant_fields = []; } return { mode, @@ -136,12 +201,17 @@ function cloneSignupField(field: TournamentSignupField): SignupFieldForm { } function addEntryField(form: TournamentForm) { - form.signup.entry_fields = [...form.signup.entry_fields, createSignupField('Nytt fält')]; + form.signup.entry_fields = ensureAttendanceField([ + ...form.signup.entry_fields, + createSignupField('Nytt fält') + ]); } function removeEntryField(form: TournamentForm, index: number) { - if (form.signup.entry_fields.length <= 1) return; - form.signup.entry_fields = form.signup.entry_fields.filter((_, idx) => idx !== index); + const target = form.signup.entry_fields[index]; + if (!target || isAttendanceField(target)) return; + const remaining = form.signup.entry_fields.filter((_, idx) => idx !== index); + form.signup.entry_fields = ensureAttendanceField(remaining); } function addParticipantField(form: TournamentForm) { @@ -153,7 +223,9 @@ function cloneSignupField(field: TournamentSignupField): SignupFieldForm { function removeParticipantField(form: TournamentForm, index: number) { if (form.signup.participant_fields.length <= 1) return; - form.signup.participant_fields = form.signup.participant_fields.filter((_, idx) => idx !== index); + form.signup.participant_fields = form.signup.participant_fields.filter( + (_, idx) => idx !== index + ); } function setSignupMode(form: TournamentForm, mode: 'solo' | 'team') { @@ -245,6 +317,19 @@ function cloneSignupField(field: TournamentSignupField): SignupFieldForm { const allowedTypes: TournamentFieldType[] = ['text', 'email', 'tel', 'discord']; const normalizeField = (field: SignupFieldForm): TournamentSignupField => { + if (isAttendanceField(field)) { + const label = field.label.trim() || ATTENDANCE_FIELD_LABEL; + const placeholder = field.placeholder.trim() || ATTENDANCE_FIELD_PLACEHOLDER; + + return { + id: ATTENDANCE_FIELD_ID, + label, + field_type: 'text', + required: true, + placeholder, + unique: true + }; + } const baseId = field.id.trim() || field.label; const id = slugify(baseId) || 'field'; const label = field.label.trim() || 'Fält'; @@ -257,7 +342,7 @@ function cloneSignupField(field: TournamentSignupField): SignupFieldForm { field_type: type, required: Boolean(field.required), placeholder: placeholder ? placeholder : null, - unique: Boolean(field.unique), + unique: Boolean(field.unique) }; }; @@ -277,11 +362,8 @@ function cloneSignupField(field: TournamentSignupField): SignupFieldForm { max = 1; } - const entry_fields = signup.entry_fields.map(normalizeField); - const participant_fields = - mode === 'team' - ? signup.participant_fields.map(normalizeField) - : []; + const entry_fields = ensureAttendanceField(signup.entry_fields).map(normalizeField); + const participant_fields = mode === 'team' ? signup.participant_fields.map(normalizeField) : []; return { mode, @@ -356,10 +438,7 @@ function cloneSignupField(field: TournamentSignupField): SignupFieldForm { await refreshPromise; } - function setTab( - tab: TabKey, - options: { slug?: string | null; replace?: boolean } = {} - ) { + function setTab(tab: TabKey, options: { slug?: string | null; replace?: boolean } = {}) { const currentTab = activeTab(); const currentSlug = slugParam(); const targetSlug = options.slug ?? null; @@ -393,7 +472,7 @@ function cloneSignupField(field: TournamentSignupField): SignupFieldForm { manageState.error = ''; setTab('manage', { slug: tournament.slug, - replace: options.replace ?? false, + replace: options.replace ?? false }); } @@ -448,8 +527,7 @@ function cloneSignupField(field: TournamentSignupField): SignupFieldForm { void refreshTournaments({ preferredId: tournament.id }); }, (tournamentId) => { - const preferredId = - manageState.selectedId === tournamentId ? null : manageState.selectedId; + const preferredId = manageState.selectedId === tournamentId ? null : manageState.selectedId; void refreshTournaments({ preferredId: preferredId ?? undefined }); } ); @@ -502,14 +580,14 @@ function cloneSignupField(field: TournamentSignupField): SignupFieldForm { } }); -let manageState = $state({ - form: createEmptyForm(), - saving: false, - success: '', - error: '', - autoSlug: false, - selectedId: null -}); + let manageState = $state({ + form: createEmptyForm(), + saving: false, + success: '', + error: '', + autoSlug: false, + selectedId: null + }); let manageSlug = $state(''); $effect(() => { @@ -719,38 +797,40 @@ let manageState = $state({ } } -function overviewCardSubtitle(tournament: TournamentInfo) { - const start = formatDateTime(tournament.start_at); - return start ? `${tournament.game} • ${start}` : tournament.game; -} + function overviewCardSubtitle(tournament: TournamentInfo) { + const start = formatDateTime(tournament.start_at); + return start ? `${tournament.game} • ${start}` : tournament.game; + } -function registrationSummary(tournament: TournamentInfo) { - const totalRegistrations = tournament.total_registrations ?? 0; - const totalParticipants = tournament.total_participants ?? 0; + function registrationSummary(tournament: TournamentInfo) { + const totalRegistrations = tournament.total_registrations ?? 0; + const totalParticipants = tournament.total_participants ?? 0; - if (tournament.signup_config.mode === 'team') { - if (totalRegistrations === 0) { - return 'Inga anmälningar ännu'; + if (tournament.signup_config.mode === 'team') { + if (totalRegistrations === 0) { + return 'Inga anmälningar ännu'; + } + const teamLabel = `${totalRegistrations} ${totalRegistrations === 1 ? 'lag' : 'lag'}`; + const playerLabel = `${totalParticipants} ${totalParticipants === 1 ? 'spelare' : 'spelare'}`; + return `${teamLabel} · ${playerLabel}`; } - const teamLabel = `${totalRegistrations} ${totalRegistrations === 1 ? 'lag' : 'lag'}`; - const playerLabel = `${totalParticipants} ${totalParticipants === 1 ? 'spelare' : 'spelare'}`; - return `${teamLabel} · ${playerLabel}`; + + const count = Math.max(totalParticipants, totalRegistrations); + return count === 0 + ? 'Inga anmälningar ännu' + : `${count} ${count === 1 ? 'spelare' : 'spelare'}`; } - const count = Math.max(totalParticipants, totalRegistrations); - return count === 0 ? 'Inga anmälningar ännu' : `${count} ${count === 1 ? 'spelare' : 'spelare'}`; -} + const selectedTournamentInfo = $derived(() => { + if (manageState.selectedId === null) { + return null; + } + return tournaments.find((item) => item.id === manageState.selectedId) ?? null; + }); -const selectedTournamentInfo = $derived(() => { - if (manageState.selectedId === null) { - return null; + function sectionKey(index: number) { + return `section-${index}`; } - return tournaments.find((item) => item.id === manageState.selectedId) ?? null; -}); - -function sectionKey(index: number) { - return `section-${index}`; -} @@ -765,34 +845,40 @@ function sectionKey(index: number) {

Översikt

Filtrera turneringar och öppna deras publika sidor.

-
- -
+
+ +
{#if filteredOverview().length > 0}
{#each filteredOverview() as tournament (tournament.id)} -
+
-

{tournament.game}

+

+ {tournament.game} +

{tournament.title}

{#if tournament.tagline}

{tournament.tagline}

{/if} {#if tournament.start_at} -

{formatDateTime(tournament.start_at) ?? tournament.start_at}

+

+ {formatDateTime(tournament.start_at) ?? tournament.start_at} +

{/if}

{registrationSummary(tournament)}

@@ -801,14 +887,14 @@ function sectionKey(index: number) { href={`/tournament/${tournament.slug}`} target="_blank" rel="noreferrer" - class="rounded-full bg-indigo-600 px-4 py-2 text-xs font-semibold uppercase tracking-wide text-white transition hover:bg-indigo-700" + class="rounded-full bg-indigo-600 px-4 py-2 text-xs font-semibold tracking-wide text-white uppercase transition hover:bg-indigo-700" > Öppna sida {#if tournament.slug} Visa anmälningar @@ -816,7 +902,7 @@ function sectionKey(index: number) { @@ -825,7 +911,9 @@ function sectionKey(index: number) { {/each}
{:else} -

+

Inga turneringar hittades. Skapa en ny via fliken "Skapa ny".

{/if} @@ -834,7 +922,9 @@ function sectionKey(index: number) {

Skapa turnering

-

Fälten används även för att automatiskt bygga anmälningssidan.

+

+ Fälten används även för att automatiskt bygga anmälningssidan. +

@@ -921,7 +1011,9 @@ function sectionKey(index: number) {
-

Sektioner för anmälningssidan

+

+ Sektioner för anmälningssidan +

{#if createState.form.sections.length === 0} -

+

Lägg till sektioner som beskriver regler, format eller andra detaljer.

{:else} @@ -941,7 +1035,8 @@ function sectionKey(index: number) {