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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014dh9W1i7aLSdPYJzQPid5o
This commit is contained in:
parent
fb2652dcbf
commit
4fa0de8753
8 changed files with 250 additions and 21 deletions
43
TODO.md
43
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
|
`listRule` (z. B. `@request.auth.id != ""`). Das erlaubt allerdings, alle
|
||||||
Konten der Instanz aufzulisten — deshalb bewusst nicht ohne Rücksprache.
|
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
|
## Vor dem nächsten Deploy
|
||||||
|
|
||||||
### `PUBLIC_CARTO_API_KEY` in Coolify setzen
|
### `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
|
### 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`
|
- `1754500500_riders_user_and_admin_rules.js`
|
||||||
- `1754500600_events_dates_instead_of_status.js`
|
- `1754500600_events_dates_instead_of_status.js`
|
||||||
- `1754500700_trail_status_meta.js`
|
- `1754500700_trail_status_meta.js`
|
||||||
- `1754500800_trail_markers_custom.js`
|
- `1754500800_trail_markers_custom.js`
|
||||||
- `1754500900_users_superadmin.js`
|
- `1754500900_users_superadmin.js`
|
||||||
|
- `1754501000_teams_logo.js`
|
||||||
|
|
||||||
Solange sie nicht gelaufen sind, scheitert alles, was auf den neuen Regeln
|
Solange sie nicht gelaufen sind, scheitert alles, was auf den neuen Regeln
|
||||||
aufbaut. Am deutlichsten beim Anlegen eines Mitglieds oder eines Fahrer-Logins:
|
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
|
Die Datei sollte danach unverändert bleiben. Tut sie es nicht, gilt das Ergebnis
|
||||||
des Generators.
|
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
|
## Kleinigkeiten
|
||||||
|
|
||||||
- Der Melder eines Trail-Markers („wer") zeigt bis zum Deploy „Unbekannt". Die
|
- Der Melder eines Trail-Markers („wer") zeigt bis zum Deploy „Unbekannt". Die
|
||||||
|
|
|
||||||
|
|
@ -334,6 +334,27 @@ migrate((app) => {
|
||||||
"system": false,
|
"system": false,
|
||||||
"type": "relation"
|
"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,
|
"hidden": false,
|
||||||
"id": "autodate2990389176",
|
"id": "autodate2990389176",
|
||||||
|
|
|
||||||
38
backend/pb_migrations/1754501000_teams_logo.js
Normal file
38
backend/pb_migrations/1754501000_teams_logo.js
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})
|
||||||
|
|
@ -1,10 +1,28 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// Groesse ueber eine Klasse steuerbar, damit dasselbe Logo im Header
|
/**
|
||||||
// (klein) und auf der Login-Karte (gross) verwendet werden kann.
|
* Logo eines Teams. Ist eins hinterlegt, steht es allein; sonst bleibt das
|
||||||
let { class: className = 'w-24 h-24' }: { class?: string } = $props()
|
* Wappen des Stammtischs als Rückfalloption — zwei übereinanderliegende
|
||||||
|
* SVGs, weil das Radl über dem Schildl leicht übersteht.
|
||||||
|
*
|
||||||
|
* Groesse ueber eine Klasse steuerbar, damit dasselbe Logo im Header
|
||||||
|
* (klein) und auf der Login-Karte (gross) verwendet werden kann.
|
||||||
|
*/
|
||||||
|
import { getFileURL } from '$lib/stores/pocketbase.svelte'
|
||||||
|
import type { TeamsResponse } from '$lib/types'
|
||||||
|
|
||||||
|
let {
|
||||||
|
class: className = 'w-24 h-24',
|
||||||
|
team = null,
|
||||||
|
}: { class?: string; team?: TeamsResponse | null } = $props()
|
||||||
|
|
||||||
|
const url = $derived(team?.logo ? getFileURL(team, team.logo) : null)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative {className}" id="logo">
|
<div class="relative {className}" id="logo">
|
||||||
<img alt="" class="absolute inset-0 h-full w-full object-contain" src="/schildl.svg">
|
{#if url}
|
||||||
<img alt="" class="absolute inset-0 h-full w-full scale-110 object-contain" src="/radl.svg">
|
<img alt="" class="absolute inset-0 h-full w-full object-contain" src={url} />
|
||||||
|
{:else}
|
||||||
|
<img alt="" class="absolute inset-0 h-full w-full object-contain" src="/schildl.svg">
|
||||||
|
<img alt="" class="absolute inset-0 h-full w-full scale-110 object-contain" src="/radl.svg">
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,20 @@ export class TeamStore {
|
||||||
return await api.collection('teams').update(id, { name })
|
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) */
|
/** Mitglied hinzufügen (Owner/Admin only) */
|
||||||
async addMember(teamId: string, userId: string) {
|
async addMember(teamId: string, userId: string) {
|
||||||
const team = this.records.find((t) => t.id === teamId)
|
const team = this.records.find((t) => t.id === teamId)
|
||||||
|
|
|
||||||
1
frontend/src/lib/types.d.ts
vendored
1
frontend/src/lib/types.d.ts
vendored
|
|
@ -86,6 +86,7 @@ export type TeamsRecord = {
|
||||||
admins?: RecordIdString[]
|
admins?: RecordIdString[]
|
||||||
created?: IsoDateString
|
created?: IsoDateString
|
||||||
id: string
|
id: string
|
||||||
|
logo?: string
|
||||||
name: string
|
name: string
|
||||||
owner: RecordIdString
|
owner: RecordIdString
|
||||||
updated?: IsoDateString
|
updated?: IsoDateString
|
||||||
|
|
|
||||||
|
|
@ -101,15 +101,28 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if auth.user}
|
{#if auth.user}
|
||||||
<div class="min-h-screen bg-background">
|
<div class="relative min-h-screen bg-background">
|
||||||
<header class="border-b bg-card">
|
<!--
|
||||||
|
Das Logo als Wasserzeichen: fest in der Ecke, gedreht und sehr
|
||||||
|
blass. aria-hidden und pointer-events-none, damit es weder
|
||||||
|
vorgelesen wird noch Klicks abfängt. Im Dunkelmodus etwas
|
||||||
|
kräftiger, sonst verschwindet es im dunklen Grund.
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="pointer-events-none fixed -bottom-24 -right-24 z-0 select-none opacity-[0.04] dark:opacity-[0.07]"
|
||||||
|
>
|
||||||
|
<Logo team={teams.active} class="size-[32rem] max-w-[70vw] rotate-12" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header class="relative z-10 border-b bg-card">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex min-h-16 items-center justify-between gap-4 py-2">
|
<div class="flex min-h-16 items-center justify-between gap-4 py-2">
|
||||||
<!-- Links: Logo und Menü. Das Logo fuellt seine Box, in
|
<!-- Links: Logo und Menü. Das Logo fuellt seine Box, in
|
||||||
voller Groesse wuerde es die 64 px hohe Leiste
|
voller Groesse wuerde es die 64 px hohe Leiste
|
||||||
ueberragen und beschnitten wirken. -->
|
ueberragen und beschnitten wirken. -->
|
||||||
<div class="flex items-center gap-3 min-w-0">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<Logo class="h-11 w-11 shrink-0" />
|
<Logo team={teams.active} class="h-11 w-11 shrink-0" />
|
||||||
|
|
||||||
<nav class="hidden md:flex items-center gap-1">
|
<nav class="hidden md:flex items-center gap-1">
|
||||||
{#each nav as item}
|
{#each nav as item}
|
||||||
|
|
@ -233,7 +246,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main class="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import * as Dialog from '@/components/ui/dialog'
|
import * as Dialog from '@/components/ui/dialog'
|
||||||
import Avatar from '$lib/components/Avatar.svelte'
|
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'
|
import type { TeamsResponse } from '$lib/types'
|
||||||
|
|
||||||
const teams = getTeamContext()
|
const teams = getTeamContext()
|
||||||
|
|
@ -82,6 +83,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Logo ----
|
||||||
|
let logoDialog = $state(false)
|
||||||
|
let logoTeam = $state<TeamsResponse | null>(null)
|
||||||
|
let logoFile = $state<File | null>(null)
|
||||||
|
let logoBusy = $state(false)
|
||||||
|
let logoError = $state<string | null>(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 ----
|
// ---- Mitglied anlegen ----
|
||||||
// Bestehende Konten lassen sich nicht suchen: users.listRule gibt nur den
|
// Bestehende Konten lassen sich nicht suchen: users.listRule gibt nur den
|
||||||
// eigenen Datensatz frei, und das soll auch so bleiben. Ein neues Konto
|
// eigenen Datensatz frei, und das soll auch so bleiben. Ein neues Konto
|
||||||
|
|
@ -175,16 +205,19 @@
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex flex-wrap items-start justify-between gap-2">
|
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||||
<div>
|
<div class="flex items-center gap-3">
|
||||||
<CardTitle class="flex items-center gap-2">
|
<Logo team={t} class="size-10 shrink-0" />
|
||||||
{t.name}
|
<div>
|
||||||
{#if t.id === teams.activeId}
|
<CardTitle class="flex items-center gap-2">
|
||||||
<Badge>Aktiv</Badge>
|
{t.name}
|
||||||
{/if}
|
{#if t.id === teams.activeId}
|
||||||
</CardTitle>
|
<Badge>Aktiv</Badge>
|
||||||
<CardDescription>
|
{/if}
|
||||||
{(t.users ?? []).length} Mitglied{(t.users ?? []).length === 1 ? '' : 'er'}
|
</CardTitle>
|
||||||
</CardDescription>
|
<CardDescription>
|
||||||
|
{(t.users ?? []).length} Mitglied{(t.users ?? []).length === 1 ? '' : 'er'}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
{#if t.id !== teams.activeId}
|
{#if t.id !== teams.activeId}
|
||||||
|
|
@ -196,6 +229,10 @@
|
||||||
<Button variant="outline" size="sm" onclick={() => openRename(t)}>
|
<Button variant="outline" size="sm" onclick={() => openRename(t)}>
|
||||||
Umbenennen
|
Umbenennen
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onclick={() => openLogo(t)}>
|
||||||
|
<Image class="size-4 mr-1" />
|
||||||
|
Logo
|
||||||
|
</Button>
|
||||||
<Button variant="outline" size="sm" onclick={() => openMember(t)}>
|
<Button variant="outline" size="sm" onclick={() => openMember(t)}>
|
||||||
<UserPlus class="size-4 mr-1" />
|
<UserPlus class="size-4 mr-1" />
|
||||||
Mitglied
|
Mitglied
|
||||||
|
|
@ -345,3 +382,49 @@
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|
||||||
|
<!-- Logo -->
|
||||||
|
<Dialog.Root bind:open={logoDialog}>
|
||||||
|
<Dialog.Content>
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title>Logo für „{logoTeam?.name}"</Dialog.Title>
|
||||||
|
<Dialog.Description>
|
||||||
|
Erscheint in der Kopfzeile und blass im Seitenhintergrund. Ohne eigenes
|
||||||
|
Logo steht dort das Wappen des Stammtischs.
|
||||||
|
</Dialog.Description>
|
||||||
|
</Dialog.Header>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<Logo team={logoTeam} class="size-20 shrink-0" />
|
||||||
|
<div class="space-y-1 min-w-0">
|
||||||
|
<Label for="team-logo">Datei</Label>
|
||||||
|
<input
|
||||||
|
id="team-logo"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="block w-full text-sm"
|
||||||
|
onchange={(e) => (logoFile = e.currentTarget.files?.[0] ?? null)}
|
||||||
|
/>
|
||||||
|
<p class="text-xs text-muted-foreground">PNG, SVG oder WebP, bis 5 MB.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if logoError}
|
||||||
|
<p class="text-sm text-destructive">{logoError}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button variant="ghost" onclick={() => (logoDialog = false)}>Abbrechen</Button>
|
||||||
|
{#if logoTeam?.logo}
|
||||||
|
<Button variant="outline" disabled={logoBusy} onclick={() => saveLogo(null)}>
|
||||||
|
Entfernen
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button disabled={!logoFile || logoBusy} onclick={() => saveLogo(logoFile)}>
|
||||||
|
{logoBusy ? 'Wird gespeichert …' : 'Speichern'}
|
||||||
|
</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue