diff --git a/CLAUDE.md b/CLAUDE.md index 6139626..4f7a888 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,20 @@ docker compose down Container und müssen mit `docker compose cp` ins Repo geholt werden; siehe `backend/README.md`. - **Type-safe client**: The PocketBase client is typed using auto-generated types in `frontend/src/lib/types.d.ts` -- **Collections**: users, teams, events, runs, riders, times +- **Collections**: users, teams, team_invites, events, runs, riders, times, + trails, trail_versions, trail_flags, trail_markers, trail_comments, + event_participants, event_series +- **Regeln**: Jede Zugriffsregel, die `@request.auth.id` erwähnt, beginnt mit + `@request.auth.id != "" && (…)`. Ohne diesen Vorspann ist eine *leere* + Relation gleich dem leeren `@request.auth.id` einer anonymen Anfrage — ein + Team ohne Admins stünde damit offen im Netz. Siehe + `backend/pb_migrations/1754501700_rules_require_login.js`. +- **Hooks**: `backend/pb_hooks/` enthält serverseitiges JS für das, was keine + Collection-Regel abbilden kann — derzeit die öffentlichen Routen der + Einladungslinks. Jeder Route-Handler läuft in einer eigenen JS-Laufzeit; + gemeinsamer Code gehört in ein Modul und wird per + ``require(`${__hooks}/…`)`` im Handler geholt. Details in + `backend/README.md`. - **Authentication**: Handled through `src/lib/stores/pocketbase.svelte.ts` with the `AuthStore` class - **File handling**: Use `getFileURL(record, file, options)` helper for PocketBase file URLs @@ -136,7 +149,18 @@ tippy.js, which the action initializes, updates and destroys. ### File Structure Notes -- Routes are in `src/routes/` following SvelteKit conventions +- Routes are in `src/routes/` following SvelteKit conventions. **Routennamen + immer auf Englisch** (`/invite/[token]`, nicht `/einladung/[token]`) — + Oberflächentexte sind deutsch, Pfade nicht. +- Der Menüpunkt **Team** (`/dashboard/team`) führt Team und Kader an einer + Stelle: Kennzahlen, Teamdaten, Einladungslinks und die Fahrerliste samt der + Verwaltungsknöpfe der Teamleitung. Unter **Einstellungen** steht nur noch, + was einen selbst betrifft (Profil, Flag-Typen). +- **Ein Fahrer, ein Name, eine Richtung**: Ein Konto entsteht immer am Fahrer + (`riders.createLogin`), es gibt kein Verknüpfen bestehender Konten und kein + Lösen. Angezeigt wird ausschließlich der Fahrername — der Kontoname wäre + eine zweite Wahrheit, die niemand geradeziehen könnte, weil + `users.updateRule` nur den Kontoinhaber selbst ändern lässt. - Reusable components in `src/lib/components/` - Stores use `.svelte.ts` extension for Svelte 5 runes - Static assets in `static/` diff --git a/NAMING.md b/NAMING.md new file mode 100644 index 0000000..faf414f --- /dev/null +++ b/NAMING.md @@ -0,0 +1,31 @@ +Die "Wanderer"-Schiene + + Shredder: Ziemlich actiongeladen, 100% MTB-Jargon und hat genau wie "Wanderer" dieses aktive, ausführende Element. + + Kurbler: Die direktere, etwas rotzigere deutsche Alternative. Hat einen leichten Retro-Charme. + + Dropper: Angelehnt an die Dropper-Post (absenkbare Sattelstütze) oder das "Reindroppen" in den Trail. + +Fokus auf Trail & Community + + TrailTribe: Betont den Vereins- und Gruppenaspekt hervorragend. + + DirtBase: Perfekt für die Traildatenbank – kurz, prägnant und technisch sauber. + + FlowHub: Verbindet das ultimative MTB-Gefühl ("Flow") mit dem modernen Plattform-Gedanken für Eventveranstalter. + +Fokus auf Zeitnahme & Action + + SendSync: "Send it!" gepaart mit der schnellen Datensynchronisation einer Webapp. + + PacePeak: Klingt sportlich, kompetitiv und extrem passend für Rennveranstalter und Bestzeitenjäger. + + TrailTicker: Etwas verspielter, bringt aber das Thema Zeitmessung und Live-Updates sofort auf den Punkt. + +Kurz & Knackig (Startup-Vibe) + + Berm: Englisch für die Anliegerkurve. Kurz, international und unter Mountainbikern ein fester Begriff. + + Roost: Der aufgeworfene Dreck in der Kurve. Hat Energie und ist sehr spezifisch für den Offroad-Sport. + + Stollen: Ein sehr deutsches, geerdetes Wort (Stollenreifen), das MTB-Flair mit technischer Präzision verbindet. diff --git a/TODO.md b/TODO.md index 467d0c5..e69de29 100644 --- a/TODO.md +++ b/TODO.md @@ -1,118 +0,0 @@ -# Offene Punkte - -Stand: 2026-09-02, abgeglichen mit der produktiven Instanz. Die fachlichen -Entscheidungen sind gefallen und gebaut; was hier steht, passiert außerhalb des -Repos oder wartet auf den nächsten Deploy. - -## Vor dem nächsten Deploy - -### `PUBLIC_CARTO_API_KEY` in Coolify setzen - -Die Kartenkacheln kommen von CARTO. Der Schlüssel muss den Browser erreichen, -deshalb das `PUBLIC_`-Präfix — ohne das reicht SvelteKit ihn nicht weiter. - -Gelesen wird er über `$env/dynamic/public`, das heißt: Die Variable muss in der -**Laufzeitumgebung** des `node build`-Prozesses stehen, nicht nur zur Buildzeit. -In Coolify als normale Umgebungsvariable eintragen genügt. Fehlt sie, laufen die -Kacheln ohne Schlüssel weiter — die Karte bleibt also sichtbar. - -### Migrationen: sechs gelaufen, fünf offen - -Abgeglichen mit `api.stammtisch-hersbruck.de` (nur gelesen): Der Backend-Deploy -hat alles bis `1754500900` mitgenommen, ab `1754501000` steht es noch aus. - -| Datei | Was sie tut | Stand | -|---|---|---| -| `1754500500_riders_user_and_admin_rules` | `riders.user`, Fahrerpflege nur Teamleitung, `users.viewRule`/`createRule` | gelaufen | -| `1754500600_events_dates_instead_of_status` | `events.status` raus, `starts`/`ends` rein | gelaufen | -| `1754500700_trail_status_meta` | `trails.status_changed`, `status_by` | gelaufen | -| `1754500800_trail_markers_custom` | `trail_markers.label`/`icon`, `flag` optional | gelaufen | -| `1754500900_users_superadmin` | `users.superadmin` samt Schutz vor Selbstbeförderung | gelaufen | -| `1754501000_teams_logo` | `teams.logo` | **offen** | -| `1754501100_trails_status_report` | Mitglieder dürfen den Trail-Zustand melden | **offen** | -| `1754501200_users_list_for_invites` | `users.listRule` gelockert (Kontosuche) | **offen** | -| `1754501300_event_participants` | neue Collection, **verschiebt Daten**, entfernt `riders.event`/`number` | **offen** | -| `1754501400_events_timing_participation` | `events.timing`/`participation` | **offen** | -| `1754501500_event_series` | `event_series`, `events.series` | **offen** | - -Bis zum nächsten Deploy laufen deshalb ins Leere: Team-Logo, Zustandsmeldung -durch Mitglieder, Kontosuche per E-Mail, Teilnehmerlisten, die Event-Schalter -und die Serien. Die Oberfläche dafür ist da und wird beim Speichern eine -Fehlermeldung von PocketBase zeigen. - -**Zwei Dinge sind beim Nachziehen nicht zurückzunehmen:** - -1. `1754501300` legt für jeden Fahrer mit Event einen Teilnehmer an und entfernt - danach `riders.event` und `riders.number`. Eine Startnummer an einem Fahrer - **ohne** Event hat danach keinen Platz mehr und geht verloren — sie war - ohnehin nirgends sichtbar, weil alle Listen nach Event filterten. -2. Bestehende Events werden auf `timing = true` und `participation = - geschlossen` gesetzt. Genau so wurden sie bisher benutzt; wer einen - Stammtisch daraus machen will, schaltet die Zeitnahme im Dialog ab. - -Ebenfalls bewusst so: Nach `1754501200` kann jeder Eingeloggte Konten -auflisten, damit sich bestehende Konten per E-Mail ins Team holen lassen. -E-Mail-Adressen bleiben über `emailVisibility` verborgen. - -### Typen einmal neu erzeugen - -`frontend/src/lib/types.d.ts` wurde bei allen Schema-Änderungen von Hand -gepflegt. Grund: `pocketbase-typegen` braucht native sqlite3-Bindings, die in -der Arbeitsumgebung nicht gebaut werden durften. - -```bash -cd frontend && npm run generate-pocketbase-types -``` - -Die Datei sollte danach unverändert bleiben. Tut sie es nicht, gilt das Ergebnis -des Generators. - -### Regeln einmal durchspielen - -Die Zugriffsregeln der neuen Collections konnten nirgends laufen — weder Docker -noch eine lokale PocketBase-Instanz standen zur Verfügung. Sie sind nach dem -Muster der vorhandenen Collections geschrieben, aber ungetestet. Nach dem Deploy -einmal ausprobieren: - -- Ein einfaches Teammitglied trägt sich bei einem **offenen** Event selbst ein. -- Dasselbe bei einem Event **mit Bestätigung** — es muss als „Angefragt" - landen, nicht als „Dabei". -- Bei einem **geschlossenen** Event darf es gar nicht gehen. -- Ein Mitglied meldet einen Trail-Zustand (muss gehen) und versucht, den Namen - des Trails zu ändern (darf nicht gehen). - -Der Selbsteintrag setzt außerdem voraus, dass das Konto der Person mit einem -Fahrer verknüpft ist (Einstellungen → Fahrer & Konten). Ohne Verknüpfung -erscheint der Knopf gar nicht erst. - -### Logo des Stammtischs zuordnen - -`teams.logo` gibt es ab `1754501000`. Solange nichts hinterlegt ist, zeigt die -App weiterhin das Wappen aus `static/` — für den Stammtisch Hersbruck sieht es -also unverändert aus. Wer es als Datei am Team haben will, lädt es nach dem -Deploy unter Einstellungen → Team → Logo hoch. - -## Kleinigkeiten - -- Wer Regeln direkt im PocketBase-Admin ändert, hinterlässt keine Spur im Repo. - Die Migrationen bleiben die Quelle: Sie setzen dieselben Werte noch einmal und - überschreiben eine abweichende Handänderung stillschweigend. -- Die Rolle **Superadmin** ist gesetzt, gibt in der App aber noch keine - besonderen Rechte. Sie ist bewusst nur in PocketBase vergebbar: Die - `updateRule` der users-Collection schließt `superadmin` vom Selbstsetzen aus. - Sobald klar ist, was sie dürfen soll, kommen die Regeln dazu. -- Flag-Typen sind **teamspezifisch**. Ein neues Team bekommt beim Anlegen den - hartkodierten Standardsatz aus `trailFlags.svelte.ts` und kann ihn frei - ändern. -- „Jedes Teammitglied ist ein Fahrer" hält die Oberfläche ein, das Backend - erzwingt es nicht: Ein Konto kann im Team sein, ohne an einem Fahrer zu - hängen. Solche Fälle stehen unter „Fahrer & Konten" mit dem Vermerk „kein - Fahrer" und lassen sich per Knopf übernehmen. -- „Als Fahrer übernehmen" rät den Namen aus dem Kontonamen und setzt ihn als - Vornamen. „Max Mustermann" auf zwei Felder aufzuteilen wäre geraten und läge - bei Doppelnamen daneben; der Name lässt sich danach korrigieren. Wenn das zu - grob ist, müsste der Knopf stattdessen nach Vor- und Nachname fragen. -- Serien erzeugen bis zu zwölf Termine je Klick und überspringen, was schon - angelegt ist. Ein Termin, der ausfällt, wird als Event gelöscht oder - verschoben — die Serie bleibt davon unberührt. -- `NAMING.md` und `frontend/static/pedaler.svg` liegen bewusst nur lokal. diff --git a/frontend/src/lib/stores/pocketbase.svelte.ts b/frontend/src/lib/stores/pocketbase.svelte.ts index 413423b..af8f23f 100644 --- a/frontend/src/lib/stores/pocketbase.svelte.ts +++ b/frontend/src/lib/stores/pocketbase.svelte.ts @@ -27,19 +27,19 @@ class AuthStore { api.authStore.loadFromCookie(cookie) } + /** + * Anmelden. Ohne Argumente gelten die Felder des Stores, die das + * Login-Formular füllt; mit Argumenten die übergebenen — die + * Einladungsseite meldet damit das eben angelegte Konto an, ohne den + * Umweg über die Formularfelder. + */ login = async (user = auth.username, password = auth.password) => { this.loading = true this.error = '' - - if (this.error) { - this.loading = false - return - } - try { - await api.collection('users').authWithPassword(auth.username, auth.password, this.params) + await api.collection('users').authWithPassword(user, password, this.params) this.user = api.authStore.record as AuthRecord | null diff --git a/frontend/src/lib/stores/teams.svelte.ts b/frontend/src/lib/stores/teams.svelte.ts index 498748e..4807784 100644 --- a/frontend/src/lib/stores/teams.svelte.ts +++ b/frontend/src/lib/stores/teams.svelte.ts @@ -246,19 +246,6 @@ export class TeamStore { return user } - /** User per E-Mail suchen (für Einladungs-UI) */ - async findUserByEmail(email: string): Promise { - try { - const res = await api.collection('users').getFirstListItem( - `email = "${email.replaceAll('"', '')}"`, - { requestKey: null }, - ) - return res - } catch { - return null - } - } - destroy() { if (this.unsubscribe) { this.unsubscribe() diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte index 1598cea..2ee044d 100644 --- a/frontend/src/routes/dashboard/+page.svelte +++ b/frontend/src/routes/dashboard/+page.svelte @@ -42,7 +42,7 @@ Team {teams.active.name} · {stats.totalEvents} Events, {stats.totalRiders} Fahrer

{:else} -

Kein Team aktiv. Team verwalten.

+

Kein Team aktiv. Team verwalten.

{/if} diff --git a/frontend/src/routes/dashboard/events/[id]/+page.svelte b/frontend/src/routes/dashboard/events/[id]/+page.svelte index 973492f..8b14b66 100644 --- a/frontend/src/routes/dashboard/events/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/events/[id]/+page.svelte @@ -3,7 +3,7 @@ import { goto } from '$app/navigation' import { getEventContext } from '$lib/stores/events.svelte' import { getRunContext } from '$lib/stores/runs.svelte' - import { fullName, getRiderContext } from '$lib/stores/riders.svelte' + import { riderName, getRiderContext } from '$lib/stores/riders.svelte' import { getTimeContext } from '$lib/stores/times.svelte' import { getTeamContext } from '$lib/stores/teams.svelte' import { @@ -283,7 +283,7 @@ {#if canManage && available.length === 0 && riders.scoped.length === 0}

Das Team hat noch keine Fahrer. - Jetzt anlegen + Jetzt anlegen

{/if} @@ -322,8 +322,8 @@ - {#if rider && fullName(rider)} - {fullName(rider)} + {#if rider && riderName(rider)} + {riderName(rider)} {:else} — ohne Name — {/if} @@ -507,12 +507,12 @@ {form.rider - ? fullName(riders.getById(form.rider) ?? ({} as any)) || 'Fahrer wählen…' + ? riderName(riders.getById(form.rider) ?? ({} as any)) || 'Fahrer wählen…' : 'Fahrer wählen…'} {#each available as r (r.id)} - {fullName(r) || '— ohne Name —'} + {riderName(r) || '— ohne Name —'} {/each} diff --git a/frontend/src/routes/dashboard/riders/+page.svelte b/frontend/src/routes/dashboard/riders/+page.svelte deleted file mode 100644 index 7e159f7..0000000 --- a/frontend/src/routes/dashboard/riders/+page.svelte +++ /dev/null @@ -1,134 +0,0 @@ - - -Fahrer - -
-
-
-

Fahrer

-

- {riders.scoped.length} im Team {teams.active?.name ?? ''} -

-
- {#if riders.canManage} - - {/if} -
- -
- - -
- - {#if riders.scoped.length === 0} - - - -

Noch keine Fahrer

-

- {#if riders.canManage} - Leg sie in den Einstellungen an. - {:else} - Die Teamleitung legt Fahrer in den Einstellungen an. - {/if} -

- - - {:else if roster.length === 0} - - Keine Treffer. - - {:else} - - - - - - Name - Events - Zeiten - Konto - - - - {#each roster as r (r.id)} - {@const path = riderPath(r.id)} - - - {#if path} - {fullName(r) || '— ohne Name —'} - {:else} - {fullName(r) || '— ohne Name —'} - {/if} - - - {participants.byRider(r.id).length} - - - {times.getByRider(r.id).length} - - - {#if r.user} - - - {teams.userLabel(r.user)} - - {:else} - - {/if} - - - {/each} - - - - - {/if} -
diff --git a/frontend/src/routes/dashboard/settings/+layout.svelte b/frontend/src/routes/dashboard/settings/+layout.svelte index 15ae4cd..8c39ecb 100644 --- a/frontend/src/routes/dashboard/settings/+layout.svelte +++ b/frontend/src/routes/dashboard/settings/+layout.svelte @@ -2,27 +2,19 @@ /** * Rahmen der Einstellungen: eine Überschrift, darunter Reiter je Bereich. * - * „Team" ist die Übersicht des Teams, „Fahrer & Konten" führt die - * Menschen: Jedes Mitglied ist ein Fahrer, ein Konto hat aber nicht jeder. - * Beides an einer Stelle, weil es sonst zwei Wahrheiten über dieselbe - * Person gäbe. Der Reiter erscheint nur für die Teamleitung. + * Hier steht nur noch, was einen selbst betrifft — das eigene Profil und + * die Flag-Typen. Team und Kader sind unter „Team" zu Hause; sie sind + * keine Einstellung, sondern der Gegenstand der Anwendung. */ import { page } from '$app/state' - import { getTeamContext } from '$lib/stores/teams.svelte' - import { Flag, User, Users, Bike } from 'lucide-svelte' + import { Flag, User } from 'lucide-svelte' let { children } = $props() - const teams = getTeamContext() - - const tabs = $derived([ + const tabs = [ { href: '/dashboard/settings', label: 'Profil', icon: User }, - { href: '/dashboard/settings/team', label: 'Team', icon: Users }, - ...(teams.isAdmin() - ? [{ href: '/dashboard/settings/riders', label: 'Fahrer & Konten', icon: Bike }] - : []), { href: '/dashboard/settings/flags', label: 'Flag-Typen', icon: Flag }, - ]) + ] // Das Profil liegt auf der Wurzel; ohne den Sonderfall wäre sein Reiter // auf jedem Unterpfad mit aktiv. diff --git a/frontend/src/routes/dashboard/settings/riders/+page.svelte b/frontend/src/routes/dashboard/settings/riders/+page.svelte deleted file mode 100644 index 3504d4a..0000000 --- a/frontend/src/routes/dashboard/settings/riders/+page.svelte +++ /dev/null @@ -1,587 +0,0 @@ - - -{#if !canManage} - - - Fahrer und Konten verwaltet, wer das Team führt. Den Kader siehst du - unter Fahrer. - - -{:else} -
-
-

- Jedes Teammitglied ist ein Fahrer — ein Konto braucht es dafür nicht. - Wer sich anmelden können soll, bekommt eins. -

- -
- - {#if rows.length === 0} - - - Noch niemand im Team. - - - {:else} - - - - - - Name - Konto - Rolle - Events - Aktionen - - - - {#each rows as row (row.rider?.id ?? row.userId)} - - - - - {row.name} - {#if !row.rider} - kein Fahrer - {/if} - - - - - {#if row.userId} - - {teams.userLabel(row.userId)} - - {:else} - kein Konto - {/if} - - - - {#if row.role === 'owner'} - - - Owner - - {:else if row.role === 'admin'} - - - Admin - - {:else if row.role === 'mitglied'} - Mitglied - {:else} - - {/if} - - - - {row.rider ? participants.byRider(row.rider.id).length : '—'} - - - - {#if iAmOwner && row.userId && row.role !== 'owner'} - - {/if} - - {#if row.rider && !row.userId} - - {:else if row.rider && row.userId} - - {:else if row.userId} - - {/if} - - {#if row.rider} - - {/if} - - {#if row.userId && row.role !== 'owner'} - - {/if} - - {#if row.rider} - - {/if} - - - {/each} - - - - - {/if} -
-{/if} - - - - - - {editing ? 'Fahrer bearbeiten' : 'Person anlegen'} - - {editing - ? 'Der Name gilt teamweit; Startnummern hängen am jeweiligen Event.' - : 'Kommt als Fahrer in den Kader. Ein Konto ist optional.'} - - - -
-
-
- - -
-
- - -
-
- - {#if !editing} - - - {#if form.withAccount} -
- - -
-
- - -

- Das Konto kommt zugleich ins Team. Gib das Passwort weiter; es lässt - sich unter „Profil" ändern. -

-
- {/if} - {/if} - - {#if error} -

{error}

- {/if} -
- - - - - -
-
- - - - - - - Konto für {accountRider ? fullName(accountRider) : 'Fahrer'} - - - Mit einem Konto kann sich die Person selbst anmelden. - - - -
- - -
- -
- {#if accountMode === 'neu'} -
- - -
-
- - -
- {:else} -
- -
- e.key === 'Enter' && searchAccount()} - /> - -
-
- - {#if found} -
- -
-

{found.name || found.email}

-

- Wird ins Team aufgenommen und verknüpft. -

-
-
- {/if} - {/if} - - {#if accountError} -

{accountError}

- {/if} -
- - - - {#if accountMode === 'neu'} - - {:else} - - {/if} - -
-
diff --git a/frontend/src/routes/dashboard/settings/team/+page.svelte b/frontend/src/routes/dashboard/settings/team/+page.svelte deleted file mode 100644 index 6efbd10..0000000 --- a/frontend/src/routes/dashboard/settings/team/+page.svelte +++ /dev/null @@ -1,352 +0,0 @@ - - -
- {#if teams.active} -
- {#each stats as stat (stat.label)} - {@const Icon = stat.icon} - - -
-

{stat.label}

- -
-

{stat.value}

- {#if stat.hint} -

{stat.hint}

- {/if} -
-
- {/each} -
- {/if} - -
-

- Teams, in denen du bist. Das aktive Team bestimmt, welche Events, Fahrer und - Trails du siehst. -

- -
- - {#if teams.records.length === 0} - - - -

Noch in keinem Team

-

Erstelle dein erstes Team.

- -
-
- {:else} - {#each teams.records as t (t.id)} - {@const iAmOwner = t.owner === me} - {@const iAmAdmin = iAmOwner || (t.admins ?? []).includes(me ?? '')} - - -
-
- -
- - {t.name} - {#if t.id === teams.activeId} - Aktiv - {/if} - - - {(t.users ?? []).length} Mitglied{(t.users ?? []).length === 1 ? '' : 'er'} - -
-
-
- {#if t.id !== teams.activeId} - - {/if} - {#if iAmAdmin} - - - {/if} - {#if !iAmOwner} - - {:else} - - {/if} -
-
-
- - {(t.users ?? []).length} Mitglied{(t.users ?? []).length === 1 ? '' : 'er'} - · verwaltet unter Fahrer - -
- {/each} - {/if} -
- - - - - - Neues Team - Du wirst automatisch Owner. - -
-
- - -
- {#if createError} -

{createError}

- {/if} -
- - - - -
-
- - - - - - Team umbenennen - -
-
- - -
- {#if renameError} -

{renameError}

- {/if} -
- - - - -
-
- - - - - - Logo für „{logoTeam?.name}" - - Erscheint in der Kopfzeile und blass im Seitenhintergrund. Ohne eigenes - Logo steht dort das Wappen des Stammtischs. - - - -
-
- -
- - (logoFile = e.currentTarget.files?.[0] ?? null)} - /> -

PNG, SVG oder WebP, bis 5 MB.

-
-
- - {#if logoError} -

{logoError}

- {/if} -
- - - - {#if logoTeam?.logo} - - {/if} - - -
-
diff --git a/frontend/src/routes/dashboard/team/+page.svelte b/frontend/src/routes/dashboard/team/+page.svelte new file mode 100644 index 0000000..30e949e --- /dev/null +++ b/frontend/src/routes/dashboard/team/+page.svelte @@ -0,0 +1,953 @@ + + +Team + +
+
+ +
+

{team?.name ?? 'Team'}

+

+ {(team?.users ?? []).length} Mitglied{(team?.users ?? []).length === 1 ? '' : 'er'} + · {riders.scoped.length} im Kader +

+
+ + + {#if team} +
+ {#if canManage} + + + + {/if} + + {#if iAmOwner} + + {:else} + + {/if} +
+ {/if} +
+ + {#if !team} + + + +

Noch in keinem Team

+

Erstelle dein erstes Team.

+ +
+
+ {:else} +
+ {#each stats as stat (stat.label)} + {@const Icon = stat.icon} + + +
+

{stat.label}

+ +
+

{stat.value}

+ {#if stat.hint} +

{stat.hint}

+ {/if} +
+
+ {/each} +
+ + + + +
+
+ + +
+ {#if canManage} + + {/if} +
+
+ + + {#if rows.length === 0} +
+ +

Noch niemand im Team

+

+ {#if canManage} + Leg die erste Person an oder verschick einen Einladungslink. + {:else} + Die Teamleitung legt den Kader an. + {/if} +

+
+ {:else if filtered.length === 0} +
Keine Treffer.
+ {:else} + + + + Name + Konto + Rolle + Events + Zeiten + {#if canManage} + Aktionen + {/if} + + + + {#each filtered as row (row.rider?.id ?? row.userId)} + {@const path = row.rider ? riderPath(row.rider.id) : null} + + + + + {#if path} + {row.name} + {:else} + {row.name} + {/if} + {#if !row.rider} + kein Fahrer + {/if} + + + + + + {#if row.userId} + + + kann sich anmelden + + {:else} + kein Konto + {/if} + + + + {#if row.role === 'owner'} + + + Owner + + {:else if row.role === 'admin'} + + + Admin + + {:else if row.role === 'mitglied'} + Mitglied + {:else} + + {/if} + + + + {row.rider ? participants.byRider(row.rider.id).length : '—'} + + + + {row.rider ? times.getByRider(row.rider.id).length : '—'} + + + {#if canManage} + + {#if row.rider && !row.userId} + + {/if} + + + + {#if row.userId && row.role !== 'owner'} + + {/if} + + {#if row.rider} + + {/if} + + {/if} + + {/each} + + + {/if} +
+
+ {/if} + +
+ + + + + + {isEdit ? 'Person bearbeiten' : 'Person anlegen'} + + {isEdit + ? 'Der Name gilt teamweit; Startnummern hängen am jeweiligen Event.' + : 'Kommt als Fahrer in den Kader. Ein Konto ist optional.'} + + + +
+ {#if editing || !isEdit} +
+ + +
+ {/if} + + + {#if canSetRole} + +
+ + +

+ Admins verwalten Kader, Einladungslinks und Teamdaten. Löschen und + Rollen bleiben beim Owner. +

+
+ {:else if isEdit && editingRole === 'owner'} + +

+ Owner des Teams — diese Rolle lässt sich hier nicht ändern. +

+ {/if} + + {#if !isEdit} + + + {#if form.withAccount} +
+ + +
+
+ + +

+ Das Konto kommt zugleich ins Team. Gib das Passwort weiter; es lässt + sich unter „Einstellungen" ändern. +

+
+ {/if} + {/if} + + {#if error} +

{error}

+ {/if} +
+ + + + + +
+
+ + + + + + + Konto für {accountRider ? riderName(accountRider) : 'Fahrer'} + + + Mit einem Konto kann sich die Person selbst anmelden. Gib das Passwort + weiter; ändern lässt es sich unter „Einstellungen". + + + +
+
+ + +
+
+ + +
+ + {#if accountError} +

{accountError}

+ {/if} +
+ + + + + +
+
+ + + + + + Einladungslinks + + Wer den Link öffnet, legt sich selbst ein Konto an und steht danach im + Team {team?.name ?? ''} und im Kader. Gib ihn nur weiter, wem du das + zutraust — der Link ist das Passwort. + + + +
+ {#if invites.scoped.length > 0} +
+ {#each invites.scoped as invite (invite.id)} +
+ e.currentTarget.select()} + /> + + {inviteHint(invite)} + + + +
+ {/each} +
+ + {/if} + +
+ + +

0 = läuft nicht ab.

+
+
+ + +

0 = beliebig oft.

+
+ + {#if inviteError} +

{inviteError}

+ {/if} +
+ + + + + +
+
+ + + + + + + + + Team umbenennen + +
+
+ + +
+ {#if renameError} +

{renameError}

+ {/if} +
+ + + + +
+
+ + + + + + Logo für „{logoTeam?.name}" + + Erscheint in der Kopfzeile und blass im Seitenhintergrund. Ohne eigenes + Logo steht dort das Wappen des Stammtischs. + + + +
+
+ +
+ + (logoFile = e.currentTarget.files?.[0] ?? null)} + /> +

PNG, SVG oder WebP, bis 5 MB.

+
+
+ + {#if logoError} +

{logoError}

+ {/if} +
+ + + + {#if logoTeam?.logo} + + {/if} + + +
+
diff --git a/frontend/static/pedaler.svg b/frontend/static/pedaler.svg new file mode 100644 index 0000000..8e9c4e4 --- /dev/null +++ b/frontend/static/pedaler.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +