From 4fa0de8753e2e1d06d64445d0e25cdc1855b4d0d Mon Sep 17 00:00:00 2001 From: Daniel Michelberger Date: Wed, 2 Sep 2026 15:42:04 +0200 Subject: [PATCH] feat: Logo pro Team, auch als Wasserzeichen im Seitenhintergrund teams.logo nimmt eine Datei auf; hochgeladen wird sie in den Team-Einstellungen. Ohne eigenes Logo bleibt das Wappen aus static/ als Rueckfalloption - fuer den Stammtisch Hersbruck aendert sich damit nichts, fuer jedes weitere Team waere es fremdes Vereinswappen gewesen. Dasselbe Logo liegt gedreht und sehr blass in der rechten unteren Ecke der Seite. Es haengt fest im Bildschirm, faengt keine Klicks ab und ist fuer Screenreader nicht vorhanden; im Dunkelmodus etwas kraeftiger, sonst verschwaende es im dunklen Grund. In TODO.md ausserdem der Plan fuer Ausfahrten: Sie werden kein zweites Konzept neben Events, sondern Schalter daran - timing, participation (offen/bestaetigung/geschlossen) und Serien, die echte Einzeltermine erzeugen. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o --- TODO.md | 43 ++++++- .../pb_migrations/1754400000_init_schema.js | 21 ++++ .../pb_migrations/1754501000_teams_logo.js | 38 +++++++ frontend/src/lib/components/Logo.svelte | 28 ++++- frontend/src/lib/stores/teams.svelte.ts | 14 +++ frontend/src/lib/types.d.ts | 1 + frontend/src/routes/dashboard/+layout.svelte | 21 +++- .../dashboard/settings/team/+page.svelte | 105 ++++++++++++++++-- 8 files changed, 250 insertions(+), 21 deletions(-) create mode 100644 backend/pb_migrations/1754501000_teams_logo.js diff --git a/TODO.md b/TODO.md index cb9931e..0bd550e 100644 --- a/TODO.md +++ b/TODO.md @@ -36,6 +36,39 @@ Ein **bestehendes** fremdes Konto in ein Team zu holen braucht eine offenere `listRule` (z. B. `@request.auth.id != ""`). Das erlaubt allerdings, alle Konten der Instanz aufzulisten — deshalb bewusst nicht ohne Rücksprache. +### Ausfahrten: geplant, noch nicht gebaut + +Ausfahrten werden **nicht** zu einem zweiten Konzept neben Events, sondern +Events bekommen Schalter. Ein Bikepark-Ausflug mit gestoppten Läufen ist beides +zugleich; eine Trennung wäre willkürlich und würde Datum, Rechte und Oberfläche +doppelt führen. + +Geplant, in dieser Reihenfolge: + +1. **`events.timing`** (bool). Runs und Zeitnahme erscheinen nur, wenn gesetzt. + Der wöchentliche Stammtisch braucht sie nicht. +2. **Teilnahme** als eigene Collection `event_participants` (Event, Person, + Status, Zeitpunkt) — nicht als Liste am Event, sonst hängen Status und + Zeitpunkt nirgends, und Gäste ohne Konto passen nicht hinein. + + `events.participation` steuert, wie man hineinkommt: + + | Wert | Bedeutung | + |---|---| + | `offen` | Jedes Teammitglied trägt sich selbst ein | + | `bestaetigung` | Anmeldung möglich, Ersteller oder Team-Admin bestätigt | + | `geschlossen` | Nur Ersteller oder Team-Admin trägt Teilnehmer ein | + +3. **Serien** (`event_series`): Regel, Zeitraum und Vorlage; daraus entstehen + echte Einzeltermine. Bewusst nicht ein Datensatz mit Wiederholungsregel, aus + dem Termine nur gerechnet werden — sobald „wer kommt am 12.?" oder „diese + Woche fällt aus" beantwortet werden soll, braucht jeder Termin einen eigenen + Datensatz. + +Offen: Sind Teilnehmer **Nutzerkonten** oder **Fahrer**? Für den Stammtisch +wären es Konten, für eine gemeinsame Ausfahrt eher Fahrer (die es auch ohne +Konto gibt). Beides zugleich geht, macht die Collection aber breiter. + ## Vor dem nächsten Deploy ### `PUBLIC_CARTO_API_KEY` in Coolify setzen @@ -50,13 +83,14 @@ Kacheln ohne Schlüssel weiter — die Karte bleibt also sichtbar. ### Migrationen greifen beim nächsten Start -Fünf neue Migrationen liegen in `backend/pb_migrations/`: +Sechs neue Migrationen liegen in `backend/pb_migrations/`: - `1754500500_riders_user_and_admin_rules.js` - `1754500600_events_dates_instead_of_status.js` - `1754500700_trail_status_meta.js` - `1754500800_trail_markers_custom.js` - `1754500900_users_superadmin.js` +- `1754501000_teams_logo.js` Solange sie nicht gelaufen sind, scheitert alles, was auf den neuen Regeln aufbaut. Am deutlichsten beim Anlegen eines Mitglieds oder eines Fahrer-Logins: @@ -85,6 +119,13 @@ cd frontend && npm run generate-pocketbase-types Die Datei sollte danach unverändert bleiben. Tut sie es nicht, gilt das Ergebnis des Generators. +### 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 - Der Melder eines Trail-Markers („wer") zeigt bis zum Deploy „Unbekannt". Die diff --git a/backend/pb_migrations/1754400000_init_schema.js b/backend/pb_migrations/1754400000_init_schema.js index 6848b17..7681761 100644 --- a/backend/pb_migrations/1754400000_init_schema.js +++ b/backend/pb_migrations/1754400000_init_schema.js @@ -334,6 +334,27 @@ migrate((app) => { "system": false, "type": "relation" }, + { + "help": "", + "hidden": false, + "id": "file_team_logo", + "maxSelect": 1, + "maxSize": 5242880, + "mimeTypes": [ + "image/jpeg", + "image/png", + "image/svg+xml", + "image/gif", + "image/webp" + ], + "name": "logo", + "presentable": false, + "protected": false, + "required": false, + "system": false, + "thumbs": null, + "type": "file" + }, { "hidden": false, "id": "autodate2990389176", diff --git a/backend/pb_migrations/1754501000_teams_logo.js b/backend/pb_migrations/1754501000_teams_logo.js new file mode 100644 index 0000000..465eb19 --- /dev/null +++ b/backend/pb_migrations/1754501000_teams_logo.js @@ -0,0 +1,38 @@ +/// + +// Jedes Team bringt sein eigenes Logo mit. +// +// Bisher steckte das Wappen fest in der Anwendung (static/schildl.svg und +// static/radl.svg). Für den Stammtisch Hersbruck stimmt das, für jedes +// weitere Team wäre es fremdes Vereinswappen. Ohne hinterlegte Datei bleibt +// die bisherige Darstellung als Rückfalloption bestehen. +// +// 5 MB, weil die vorhandenen Wappen als Inkscape-SVG je rund 2,5 MB wiegen. + +migrate((app) => { + const teams = app.findCollectionByNameOrId('teams') + + if (!teams.fields.getByName('logo')) { + teams.fields.add(new Field({ + id: 'file_team_logo', + name: 'logo', + type: 'file', + maxSelect: 1, + maxSize: 5242880, + mimeTypes: [ + 'image/jpeg', + 'image/png', + 'image/svg+xml', + 'image/gif', + 'image/webp', + ], + })) + } + + app.save(teams) +}, (app) => { + const teams = app.findCollectionByNameOrId('teams') + + teams.fields.removeByName('logo') + app.save(teams) +}) diff --git a/frontend/src/lib/components/Logo.svelte b/frontend/src/lib/components/Logo.svelte index bf740d7..b58ef14 100644 --- a/frontend/src/lib/components/Logo.svelte +++ b/frontend/src/lib/components/Logo.svelte @@ -1,10 +1,28 @@ diff --git a/frontend/src/lib/stores/teams.svelte.ts b/frontend/src/lib/stores/teams.svelte.ts index 4bb9790..498748e 100644 --- a/frontend/src/lib/stores/teams.svelte.ts +++ b/frontend/src/lib/stores/teams.svelte.ts @@ -112,6 +112,20 @@ export class TeamStore { return await api.collection('teams').update(id, { name }) } + /** + * Logo setzen oder entfernen. Mit Datei als FormData — ein leeres Feld im + * FormData würde ein vorhandenes Bild löschen, deshalb der zweite Weg. + */ + async setLogo(id: string, file: File | null) { + if (!file) { + return await api.collection('teams').update(id, { logo: null }) + } + + const data = new FormData() + data.append('logo', file) + return await api.collection('teams').update(id, data) + } + /** Mitglied hinzufügen (Owner/Admin only) */ async addMember(teamId: string, userId: string) { const team = this.records.find((t) => t.id === teamId) diff --git a/frontend/src/lib/types.d.ts b/frontend/src/lib/types.d.ts index f5f5bd9..6e488be 100644 --- a/frontend/src/lib/types.d.ts +++ b/frontend/src/lib/types.d.ts @@ -86,6 +86,7 @@ export type TeamsRecord = { admins?: RecordIdString[] created?: IsoDateString id: string + logo?: string name: string owner: RecordIdString updated?: IsoDateString diff --git a/frontend/src/routes/dashboard/+layout.svelte b/frontend/src/routes/dashboard/+layout.svelte index 0e3adb4..2662b5a 100644 --- a/frontend/src/routes/dashboard/+layout.svelte +++ b/frontend/src/routes/dashboard/+layout.svelte @@ -101,15 +101,28 @@ {#if auth.user} -
-
+
+ + + +
- +
-
+
{@render children?.()}
diff --git a/frontend/src/routes/dashboard/settings/team/+page.svelte b/frontend/src/routes/dashboard/settings/team/+page.svelte index ed3b320..8dd40ec 100644 --- a/frontend/src/routes/dashboard/settings/team/+page.svelte +++ b/frontend/src/routes/dashboard/settings/team/+page.svelte @@ -13,7 +13,8 @@ import { Label } from '@/components/ui/label' import * as Dialog from '@/components/ui/dialog' import Avatar from '$lib/components/Avatar.svelte' - import { Crown, LogOut, Plus, Shield, Trash2, UserMinus, UserPlus, Users } from 'lucide-svelte' + import Logo from '$lib/components/Logo.svelte' + import { Crown, Image, LogOut, Plus, Shield, Trash2, UserMinus, UserPlus, Users } from 'lucide-svelte' import type { TeamsResponse } from '$lib/types' const teams = getTeamContext() @@ -82,6 +83,35 @@ } } + // ---- Logo ---- + let logoDialog = $state(false) + let logoTeam = $state(null) + let logoFile = $state(null) + let logoBusy = $state(false) + let logoError = $state(null) + + function openLogo(t: TeamsResponse) { + logoTeam = t + logoFile = null + logoError = null + logoDialog = true + } + + async function saveLogo(file: File | null) { + if (!logoTeam) return + + logoBusy = true + logoError = null + try { + await teams.setLogo(logoTeam.id, file) + logoDialog = false + } catch (e: any) { + logoError = e.message ?? 'Das Logo konnte nicht gespeichert werden.' + } finally { + logoBusy = false + } + } + // ---- Mitglied anlegen ---- // Bestehende Konten lassen sich nicht suchen: users.listRule gibt nur den // eigenen Datensatz frei, und das soll auch so bleiben. Ein neues Konto @@ -175,16 +205,19 @@
-
- - {t.name} - {#if t.id === teams.activeId} - Aktiv - {/if} - - - {(t.users ?? []).length} Mitglied{(t.users ?? []).length === 1 ? '' : 'er'} - +
+ +
+ + {t.name} + {#if t.id === teams.activeId} + Aktiv + {/if} + + + {(t.users ?? []).length} Mitglied{(t.users ?? []).length === 1 ? '' : 'er'} + +
{#if t.id !== teams.activeId} @@ -196,6 +229,10 @@ + + {#if logoTeam?.logo} + + {/if} + + + +