feat(ui): elegant wedding white/silver/gold redesign

Reskin the whole app via design tokens in tailwind-theme.css (remapped
color ramps) plus a define-once component layer in lib/styles/components.css
(.btn, .card, .input, .chip, .badge, .sheet, …). Buttons are muted/outlined
gold rather than flat fills. Self-host Inter + Fraunces (woff2) under the
existing font-src 'self' CSP. Restyle the shared components and the account,
admin, host, join, recover and upload screens against the new tokens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-18 17:28:42 +02:00
parent 5546fb82e6
commit f243bfe89a
22 changed files with 696 additions and 545 deletions

View File

@@ -163,9 +163,7 @@
<div class="mx-auto max-w-lg space-y-3 p-4">
<!-- Profile card -->
<div
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
>
<div class="card p-5">
<div class="flex items-center gap-4">
<div
class="flex h-14 w-14 shrink-0 items-center justify-center rounded-full text-xl font-bold
@@ -195,9 +193,7 @@
<!-- Dashboards section (host + admin only) -->
{#if role === 'host' || role === 'admin'}
<div
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
>
<div class="card overflow-hidden">
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
<h2
class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
@@ -211,7 +207,7 @@
>
<!-- Star icon -->
<svg
class="h-5 w-5 text-amber-500"
class="h-5 w-5 text-primary-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
@@ -270,10 +266,12 @@
<!-- PIN card -->
<div
class="rounded-xl border border-amber-200 bg-amber-50 p-5 dark:border-amber-800/60 dark:bg-amber-950/30"
class="rounded-xl border border-primary-200 bg-primary-50 p-5 dark:border-primary-800/60 dark:bg-primary-950/30"
>
<h2 class="mb-1 font-semibold text-amber-900 dark:text-amber-200">Wiederherstellungs-PIN</h2>
<p class="mb-3 text-sm text-amber-700 dark:text-amber-300/90">
<h2 class="mb-1 font-semibold text-primary-800 dark:text-primary-200">
Wiederherstellungs-PIN
</h2>
<p class="mb-3 text-sm text-primary-700 dark:text-primary-300/90">
Du brauchst diesen PIN, um dein Konto auf einem anderen Gerät wiederherzustellen. Schreib
ihn auf!
</p>
@@ -287,7 +285,7 @@
>
<button
onclick={copyPin}
class="inline-flex min-h-11 items-center rounded-md bg-amber-100 px-4 py-2 text-sm font-medium text-amber-800 transition hover:bg-amber-200 dark:bg-amber-900/40 dark:text-amber-200 dark:hover:bg-amber-900/60"
class="inline-flex min-h-11 items-center rounded-md bg-primary-100 px-4 py-2 text-sm font-medium text-primary-800 transition hover:bg-primary-200 dark:bg-primary-900/40 dark:text-primary-200 dark:hover:bg-primary-900/60"
>
{pinCopied ? 'Kopiert!' : 'Kopieren'}
</button>
@@ -337,9 +335,7 @@
</a>
<!-- Theme / Design -->
<div
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
>
<div class="card overflow-hidden">
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
<h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Design
@@ -394,9 +390,7 @@
</div>
<!-- Data mode -->
<div
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
>
<div class="card overflow-hidden">
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
<h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Datennutzung
@@ -439,9 +433,7 @@
<!-- Per-user quota widget -->
{#if $quotaStore.enabled && $quotaStore.limit_bytes != null}
<div
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
>
<div class="card p-5">
<h2
class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
>
@@ -472,9 +464,7 @@
<!-- Datenschutzhinweis (preformatted, plain text) -->
{#if $privacyNote.trim()}
<div
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
>
<div class="card p-5">
<h2
class="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
>
@@ -486,9 +476,7 @@
{/if}
<!-- Konto section -->
<div
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
>
<div class="card overflow-hidden">
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
<h2 class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Konto
@@ -611,16 +599,10 @@
<p class="mb-6 text-center text-sm text-gray-500 dark:text-gray-400">
Original-Dateien können deutlich mehr Datenvolumen verbrauchen. Am besten im WLAN aktivieren.
</p>
<button
onclick={confirmOriginalMode}
class="mb-3 w-full rounded-xl bg-blue-600 py-3 text-sm font-semibold text-white transition hover:bg-blue-700 active:bg-blue-700"
>
<button onclick={confirmOriginalMode} class="btn btn-primary btn-block mb-3">
Aktivieren
</button>
<button
onclick={() => (dataModeWarningOpen = false)}
class="w-full rounded-xl border border-gray-200 py-3 text-sm font-medium text-gray-700 transition hover:bg-gray-50 active:bg-gray-100 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:active:bg-gray-800"
>
<button onclick={() => (dataModeWarningOpen = false)} class="btn btn-secondary btn-block">
Abbrechen
</button>
</div>

View File

@@ -453,10 +453,7 @@
Kopieren
</button>
</div>
<button
onclick={() => (pinModal = null)}
class="w-full rounded-lg bg-blue-600 py-2 text-sm font-semibold text-white hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
>
<button onclick={() => (pinModal = null)} class="btn btn-primary btn-block btn-sm">
Schließen
</button>
{/if}
@@ -473,16 +470,10 @@
Diashow und Export, und die Sitzung wird beendet. Rückgängig machbar über „Entsperren“.
</p>
<div class="flex gap-2">
<button
onclick={() => (banTarget = null)}
class="flex-1 rounded-lg border border-gray-300 py-2 text-sm text-gray-700 hover:bg-gray-50 active:bg-gray-100 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:active:bg-gray-800"
<button onclick={() => (banTarget = null)} class="btn btn-secondary btn-sm flex-1"
>Abbrechen</button
>
<button
onclick={confirmBan}
disabled={banSubmitting}
class="flex-1 rounded-lg bg-red-600 py-2 text-sm font-medium text-white hover:bg-red-700 active:bg-red-700 disabled:opacity-50 dark:bg-red-500 dark:hover:bg-red-400 dark:active:bg-red-400"
>
<button onclick={confirmBan} disabled={banSubmitting} class="btn btn-danger btn-sm flex-1">
{banSubmitting ? 'Wird gesperrt…' : 'Sperren'}
</button>
</div>
@@ -543,33 +534,25 @@
<div class="space-y-3">
{#if stats}
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div
class="rounded-xl bg-white border border-gray-200 p-4 text-center dark:border-gray-700 dark:bg-gray-800"
>
<div class="card p-4 text-center">
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
{stats.user_count}
</p>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Gäste</p>
</div>
<div
class="rounded-xl bg-white border border-gray-200 p-4 text-center dark:border-gray-700 dark:bg-gray-800"
>
<div class="card p-4 text-center">
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
{stats.upload_count}
</p>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
</div>
<div
class="rounded-xl bg-white border border-gray-200 p-4 text-center dark:border-gray-700 dark:bg-gray-800"
>
<div class="card p-4 text-center">
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
{stats.comment_count}
</p>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Kommentare</p>
</div>
<div
class="rounded-xl bg-white border border-gray-200 p-4 text-center dark:border-gray-700 dark:bg-gray-800"
>
<div class="card p-4 text-center">
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
{diskPct(stats)} %
</p>
@@ -577,9 +560,7 @@
</div>
</div>
<!-- Disk bar -->
<div
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
>
<div class="card p-5">
<div
class="mb-1 flex items-center justify-between text-xs text-gray-500 dark:text-gray-400"
>
@@ -611,9 +592,7 @@
{:else if activeTab === 'config'}
<div class="relative space-y-3 pb-20">
{#each CONFIG_GROUPS as group (group.title)}
<div
class="rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
>
<div class="card">
<div class="border-b border-gray-100 px-5 py-3 dark:border-gray-700">
<h3
class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400"
@@ -652,7 +631,7 @@
id={field.key}
rows="6"
bind:value={configDraft[field.key]}
class="w-full resize-none rounded-lg border border-gray-300 bg-white px-3 py-2 font-mono text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
class="input resize-none font-mono text-sm"
></textarea>
{#if field.hint}
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{field.hint}</p>
@@ -670,7 +649,7 @@
min="0"
inputmode="decimal"
bind:value={configDraft[field.key]}
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
class="input text-sm"
/>
{#if field.hint}
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">{field.hint}</p>
@@ -686,11 +665,7 @@
<div
class="sticky bottom-0 -mx-4 border-t border-gray-100 bg-white px-5 py-3 dark:border-gray-800 dark:bg-gray-900 sm:mx-0 sm:rounded-b-xl"
>
<button
onclick={saveConfig}
disabled={saving}
class="w-full rounded-xl bg-blue-600 py-3 text-sm font-semibold text-white transition hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400"
>
<button onclick={saveConfig} disabled={saving} class="btn btn-primary btn-block">
{saving ? 'Wird gespeichert…' : 'Speichern'}
</button>
</div>
@@ -700,9 +675,7 @@
{:else if activeTab === 'export'}
<div class="space-y-3">
<!-- Gallery release -->
<div
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
>
<div class="card p-5">
<h3 class="mb-3 font-semibold text-gray-900 dark:text-gray-100">Galerie</h3>
<button
onclick={() =>
@@ -714,16 +687,14 @@
tone: 'danger',
run: releaseGallery
})}
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400"
class="btn btn-primary btn-sm"
>
Galerie freigeben
</button>
</div>
<!-- Export jobs -->
<div
class="rounded-xl border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"
>
<div class="card p-5">
<div class="mb-4 flex items-center justify-between">
<h3 class="font-semibold text-gray-900 dark:text-gray-100">Export-Jobs</h3>
<button
@@ -781,9 +752,7 @@
<!-- ── Nutzer tab ───────────────────────────────────────────────── -->
{:else if activeTab === 'users'}
<div
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
>
<div class="card overflow-hidden">
<!-- Search -->
<div class="p-4">
<div
@@ -824,21 +793,12 @@
>{user.display_name}</span
>
{#if user.role === 'host'}
<span
class="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900/40 dark:text-blue-200"
>Host</span
>
<span class="badge badge-primary">Host</span>
{:else if user.role === 'admin'}
<span
class="rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/40 dark:text-purple-200"
>Admin</span
>
<span class="badge badge-gold">Admin</span>
{/if}
{#if user.is_banned}
<span
class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/40 dark:text-red-200"
>Gesperrt</span
>
<span class="badge badge-danger">Gesperrt</span>
{/if}
</div>
<p class="text-xs text-gray-400 dark:text-gray-500">

View File

@@ -58,7 +58,7 @@
placeholder="Passwort"
autocomplete="current-password"
data-testid="admin-password-input"
class="mb-3 w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-lg text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
class="input mb-3 text-lg"
/>
{#if error}
@@ -71,7 +71,7 @@
type="submit"
disabled={loading || !password}
data-testid="admin-login-submit"
class="w-full rounded-lg bg-blue-600 px-4 py-3 text-lg font-medium text-white transition hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400"
class="btn btn-primary btn-lg btn-block"
>
{loading ? 'Wird angemeldet…' : 'Anmelden'}
</button>

View File

@@ -437,12 +437,7 @@
Kopieren
</button>
</div>
<button
onclick={() => (pinModal = null)}
class="w-full rounded-lg bg-blue-600 py-2 text-sm font-semibold text-white hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
>
Schließen
</button>
<button onclick={() => (pinModal = null)} class="btn btn-primary btn-block"> Schließen </button>
{/if}
</Modal>
@@ -458,17 +453,10 @@
ansehen, Keepsake herunterladen) bleibt bestehen. Rückgängig machbar über „Entsperren“.
</p>
<div class="flex gap-2">
<button
onclick={() => (banTarget = null)}
class="flex-1 rounded-lg border border-gray-300 py-2 text-sm text-gray-700 hover:bg-gray-50 active:bg-gray-100 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:active:bg-gray-800"
>
<button onclick={() => (banTarget = null)} class="btn btn-secondary flex-1">
Abbrechen
</button>
<button
onclick={confirmBan}
disabled={banSubmitting}
class="flex-1 rounded-lg bg-red-600 py-2 text-sm font-medium text-white hover:bg-red-700 active:bg-red-700 disabled:opacity-50 dark:bg-red-500 dark:hover:bg-red-400 dark:active:bg-red-400"
>
<button onclick={confirmBan} disabled={banSubmitting} class="btn btn-danger flex-1">
{banSubmitting ? 'Wird gesperrt…' : 'Sperren'}
</button>
</div>
@@ -551,15 +539,13 @@
{/if}
<!-- ── Statistiken ─────────────────────────────────────────────── -->
<div
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
>
<div class="card overflow-hidden">
<button
onclick={() => (statsOpen = !statsOpen)}
aria-expanded={statsOpen}
class="flex w-full items-center justify-between px-5 py-4"
>
<h2 class="font-semibold text-gray-900 dark:text-gray-100">Statistiken</h2>
<h2 class="section-title">Statistiken</h2>
<svg
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {statsOpen
? 'rotate-180'
@@ -580,50 +566,50 @@
<div
class="grid grid-cols-2 gap-3 border-t border-gray-100 p-4 dark:border-gray-700 sm:grid-cols-4"
>
<div class="rounded-xl bg-gray-50 p-4 text-center dark:bg-gray-900/60">
<div class="surface-muted p-4 text-center">
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">{users.length}</p>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Teilnehmer</p>
</div>
<div class="rounded-xl bg-gray-50 p-4 text-center dark:bg-gray-900/60">
<div class="surface-muted p-4 text-center">
<p class="text-2xl font-bold text-gray-900 dark:text-gray-100">
{users.reduce((s, u) => s + u.upload_count, 0)}
</p>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
</div>
<div class="rounded-xl bg-gray-50 p-4 text-center dark:bg-gray-900/60">
<p
class="text-2xl font-bold {event.uploads_locked
? 'text-red-600 dark:text-red-400'
: 'text-green-600 dark:text-green-400'}"
<div class="surface-muted flex flex-col items-center p-4 text-center">
<span
class="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-semibold {event.uploads_locked
? 'bg-red-100 text-red-700 dark:bg-red-950/50 dark:text-red-300'
: 'bg-green-100 text-green-700 dark:bg-green-950/50 dark:text-green-300'}"
>
<span class="h-1.5 w-1.5 rounded-full bg-current"></span>
{event.uploads_locked ? 'Gesperrt' : 'Offen'}
</p>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
</span>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">Uploads</p>
</div>
<div class="rounded-xl bg-gray-50 p-4 text-center dark:bg-gray-900/60">
<p
class="text-2xl font-bold {event.export_released
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-400 dark:text-gray-500'}"
<div class="surface-muted flex flex-col items-center p-4 text-center">
<span
class="inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-sm font-semibold {event.export_released
? 'bg-purple-100 text-purple-800 dark:bg-purple-950/50 dark:text-purple-200'
: 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300'}"
>
{event.export_released ? 'Ja' : 'Nein'}
</p>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">Freigegeben</p>
<span class="h-1.5 w-1.5 rounded-full bg-current"></span>
{event.export_released ? 'Freigegeben' : 'Noch nicht'}
</span>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">Galerie</p>
</div>
</div>
</div>
</div>
<!-- ── Event-Einstellungen ─────────────────────────────────────── -->
<div
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
>
<div class="card overflow-hidden">
<button
onclick={() => (settingsOpen = !settingsOpen)}
aria-expanded={settingsOpen}
class="flex w-full items-center justify-between px-5 py-4"
>
<h2 class="font-semibold text-gray-900 dark:text-gray-100">Event-Einstellungen</h2>
<h2 class="section-title">Event-Einstellungen</h2>
<svg
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {settingsOpen
? 'rotate-180'
@@ -644,10 +630,10 @@
<div class="flex flex-wrap gap-3 border-t border-gray-100 p-5 dark:border-gray-700">
<button
onclick={toggleEventLock}
class="rounded-lg px-4 py-2 text-sm font-medium transition
class="rounded-lg border px-4 py-2 text-sm font-medium transition
{event.uploads_locked
? 'bg-green-600 text-white hover:bg-green-700 dark:bg-green-500 dark:hover:bg-green-400'
: 'bg-amber-500 text-white hover:bg-amber-600 dark:bg-amber-500 dark:hover:bg-amber-400'}"
? 'border-green-600 bg-green-600 text-white hover:bg-green-700 dark:border-green-500 dark:bg-green-500 dark:hover:bg-green-400'
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700'}"
>
{event.uploads_locked ? 'Uploads wieder öffnen' : 'Uploads sperren'}
</button>
@@ -662,10 +648,7 @@
run: releaseGallery
})}
disabled={event.export_released}
class="rounded-lg px-4 py-2 text-sm font-medium transition
{event.export_released
? 'cursor-default bg-gray-100 text-gray-400 dark:bg-gray-800 dark:text-gray-500'
: 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400'}"
class="btn btn-primary btn-sm"
>
{event.export_released ? 'Galerie bereits freigegeben' : 'Galerie freigeben'}
</button>
@@ -751,15 +734,13 @@
</div>
<!-- ── Nutzerverwaltung ───────────────────────────────────────── -->
<div
class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800"
>
<div class="card overflow-hidden">
<button
onclick={() => (usersOpen = !usersOpen)}
aria-expanded={usersOpen}
class="flex w-full items-center justify-between px-5 py-4"
>
<h2 class="font-semibold text-gray-900 dark:text-gray-100">Nutzerverwaltung</h2>
<h2 class="section-title">Nutzerverwaltung</h2>
<svg
class="h-5 w-5 text-gray-400 dark:text-gray-500 transition-transform duration-200 {usersOpen
? 'rotate-180'
@@ -818,21 +799,12 @@
>{user.display_name}</span
>
{#if user.role === 'host'}
<span
class="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900/40 dark:text-blue-200"
>Host</span
>
<span class="badge badge-primary">Host</span>
{:else if user.role === 'admin'}
<span
class="rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700 dark:bg-purple-900/40 dark:text-purple-200"
>Admin</span
>
<span class="badge badge-gold">Admin</span>
{/if}
{#if user.is_banned}
<span
class="rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/40 dark:text-red-200"
>Gesperrt</span
>
<span class="badge badge-danger">Gesperrt</span>
{/if}
</div>
<p class="text-xs text-gray-400 dark:text-gray-500">

View File

@@ -139,142 +139,184 @@
}
</script>
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 dark:bg-gray-950">
<div class="w-full max-w-sm">
{#if nameTaken}
<!-- Name-taken state: sign in with PIN or choose a different name -->
<div
class="relative flex min-h-screen flex-col items-center justify-center overflow-hidden bg-gradient-to-b from-primary-50 via-gray-50 to-gray-50 px-4 py-10 dark:from-gray-900 dark:via-gray-950 dark:to-gray-950"
>
<!-- Soft celebratory glow behind the card — a champagne-gold and a silver wash
for the classic warm/cool wedding tension, kept subtle. -->
<div
aria-hidden="true"
class="pointer-events-none absolute -top-20 h-72 w-72 rounded-full bg-primary-200/30 blur-3xl dark:bg-primary-900/25"
></div>
<div
aria-hidden="true"
class="pointer-events-none absolute -bottom-24 right-0 h-72 w-72 rounded-full bg-gray-200/50 blur-3xl dark:bg-gray-800/40"
></div>
<div class="relative w-full max-w-sm">
<!-- Brand -->
<div class="mb-7 flex flex-col items-center text-center">
<div
class="mb-5 rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800/60 dark:bg-amber-950/30"
class="mb-3 flex h-16 w-16 items-center justify-center rounded-2xl bg-blue-600 text-white shadow-lg shadow-primary-600/30"
>
<p class="font-semibold text-amber-900 dark:text-amber-200">
{takenName}" ist bereits vergeben.
</p>
<p class="mt-1 text-sm text-amber-800 dark:text-amber-300/90">
Wähle einen anderen Namen, z.&nbsp;B. einen Spitznamen oder füge deinen Nachnamen hinzu („{takenName}
M." oder „{takenName} aus Berlin").
</p>
<svg class="h-8 w-8" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M4 8.5A2.5 2.5 0 0 1 6.5 6h1.2c.5 0 .95-.28 1.17-.72l.42-.85A1.5 1.5 0 0 1 10.9 3.6h2.2c.57 0 1.09.32 1.34.83l.42.85c.22.44.67.72 1.17.72h1.2A2.5 2.5 0 0 1 20 8.5v7A2.5 2.5 0 0 1 17.5 18h-11A2.5 2.5 0 0 1 4 15.5v-7Z"
stroke="currentColor"
stroke-width="1.6"
/>
<circle cx="12" cy="12" r="3.2" stroke="currentColor" stroke-width="1.6" />
</svg>
</div>
<p class="mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">
Falls du das bist, melde dich mit deinem PIN an:
</p>
<form
onsubmit={(e) => {
e.preventDefault();
handleInlineRecover();
}}
<span
class="font-display text-2xl font-semibold tracking-tight text-gray-900 dark:text-gray-100"
>EventSnap</span
>
<input
type="text"
value={recoveryPin}
oninput={onRecoveryPinInput}
placeholder="4-stelliger PIN"
maxlength={4}
inputmode="numeric"
pattern="[0-9]*"
data-testid="recovery-pin-input"
class="mb-3 w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-center text-2xl font-mono tracking-widest text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100"
/>
</div>
{#if recoveryError}
<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="recovery-error">
{recoveryError}
<!-- Invitation card -->
<div class="card p-6 shadow-xl">
{#if nameTaken}
<!-- Name-taken state: sign in with PIN or choose a different name -->
<div
class="mb-5 rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800/60 dark:bg-amber-950/30"
>
<p class="font-semibold text-amber-900 dark:text-amber-200">
{takenName}" ist bereits vergeben.
</p>
{/if}
<p class="mt-1 text-sm text-amber-800 dark:text-amber-300/90">
Wähle einen anderen Namen, z.&nbsp;B. einen Spitznamen oder füge deinen Nachnamen hinzu
(„{takenName}
M." oder „{takenName} aus Berlin").
</p>
</div>
<p class="mb-3 text-sm font-medium text-gray-700 dark:text-gray-300">
Falls du das bist, melde dich mit deinem PIN an:
</p>
<form
onsubmit={(e) => {
e.preventDefault();
handleInlineRecover();
}}
>
<input
type="text"
value={recoveryPin}
oninput={onRecoveryPinInput}
placeholder="4-stelliger PIN"
maxlength={4}
inputmode="numeric"
pattern="[0-9]*"
data-testid="recovery-pin-input"
class="input mb-3 text-center font-mono text-2xl tracking-widest"
/>
{#if recoveryError}
<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="recovery-error">
{recoveryError}
</p>
{/if}
<button
type="submit"
disabled={recoveryLoading || recoveryPin.length < 4}
data-testid="recovery-submit"
class="btn btn-primary btn-block mb-3"
>
{recoveryLoading ? 'Wird angemeldet...' : 'Anmelden'}
</button>
</form>
<button
type="submit"
disabled={recoveryLoading || recoveryPin.length < 4}
data-testid="recovery-submit"
class="mb-3 w-full rounded-lg bg-blue-600 px-4 py-3 font-medium text-white transition hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400"
onclick={tryDifferentName}
data-testid="try-different-name"
class="btn btn-secondary btn-block"
>
{recoveryLoading ? 'Wird angemeldet...' : 'Anmelden'}
Anderen Namen wählen
</button>
</form>
<button
onclick={tryDifferentName}
data-testid="try-different-name"
class="w-full rounded-lg border border-gray-300 px-4 py-3 font-medium text-gray-700 transition hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
>
Anderen Namen wählen
</button>
<!-- Forgot the PIN entirely — ask a host to reset it in-app. -->
{#if pinRequestSent}
<p
class="mt-3 rounded-lg bg-green-50 px-4 py-3 text-center text-sm text-green-700 dark:bg-green-950/30 dark:text-green-300"
>
Anfrage gesendet. Bitte einen Host, deine PIN zurückzusetzen — danach kannst du dich mit
der neuen PIN anmelden.
</p>
<!-- Forgot the PIN entirely — ask a host to reset it in-app. -->
{#if pinRequestSent}
<p
class="mt-3 rounded-lg bg-green-50 px-4 py-3 text-center text-sm text-green-700 dark:bg-green-950/30 dark:text-green-300"
>
Anfrage gesendet. Bitte einen Host, deine PIN zurückzusetzen — danach kannst du dich mit
der neuen PIN anmelden.
</p>
{:else}
<button
onclick={requestPinReset}
disabled={pinRequestLoading}
data-testid="request-pin-reset"
class="mt-3 w-full text-center text-sm text-blue-600 underline decoration-dotted underline-offset-2 hover:text-blue-700 disabled:opacity-50 dark:text-blue-400"
>
{pinRequestLoading ? 'Wird gesendet…' : 'PIN vergessen? Host um Zurücksetzen bitten'}
</button>
{/if}
{:else}
<button
onclick={requestPinReset}
disabled={pinRequestLoading}
data-testid="request-pin-reset"
class="mt-3 w-full text-center text-sm text-blue-600 underline decoration-dotted underline-offset-2 hover:text-blue-700 disabled:opacity-50 dark:text-blue-400"
<!-- Normal join form -->
<p class="mb-1 text-center text-sm font-medium text-gray-500 dark:text-gray-400">
Willkommen bei
</p>
{#if eventName}
<h1
class="mb-3 text-center text-3xl font-semibold text-gray-900 dark:text-gray-100"
data-testid="join-event-name"
>
{eventName}
</h1>
{:else}
<h1 class="mb-3 text-center text-3xl font-semibold text-gray-900 dark:text-gray-100">
dem Event
</h1>
{/if}
<p class="mb-6 text-center text-gray-600 dark:text-gray-400">
Gib deinen Namen ein, um Fotos zu teilen und die Galerie zu sehen.
</p>
<form
onsubmit={(e) => {
e.preventDefault();
handleJoin();
}}
>
{pinRequestLoading ? 'Wird gesendet…' : 'PIN vergessen? Host um Zurücksetzen bitten'}
</button>
{/if}
{:else}
<!-- Normal join form -->
<h1 class="mb-2 text-center text-2xl font-bold text-gray-900 dark:text-gray-100">
Willkommen!
</h1>
{#if eventName}
<p
class="mb-1 text-center text-lg font-semibold text-blue-600 dark:text-blue-400"
data-testid="join-event-name"
>
{eventName}
<input
type="text"
bind:value={displayName}
placeholder="Dein Name"
maxlength={50}
data-testid="join-name-input"
class="input mb-3 text-lg"
/>
{#if error}
<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="join-error">
{error}
</p>
{/if}
<button
type="submit"
disabled={loading || !displayName.trim()}
data-testid="join-submit"
class="btn btn-primary btn-lg btn-block"
>
{loading ? 'Wird geladen...' : 'Beitreten'}
</button>
</form>
<p class="mt-4 text-center text-sm">
<a
href="/recover"
data-testid="link-to-recover"
class="text-blue-600 hover:underline dark:text-blue-400"
>Ich habe bereits einen Account</a
>
</p>
{/if}
<p class="mb-6 text-center text-gray-600 dark:text-gray-400">
Gib deinen Namen ein, um dem Event beizutreten.
</p>
<form
onsubmit={(e) => {
e.preventDefault();
handleJoin();
}}
>
<input
type="text"
bind:value={displayName}
placeholder="Dein Name"
maxlength={50}
data-testid="join-name-input"
class="mb-3 w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-lg text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
/>
{#if error}
<p class="mb-3 text-sm text-red-600 dark:text-red-400" data-testid="join-error">
{error}
</p>
{/if}
<button
type="submit"
disabled={loading || !displayName.trim()}
data-testid="join-submit"
class="w-full rounded-lg bg-blue-600 px-4 py-3 text-lg font-medium text-white transition hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400"
>
{loading ? 'Wird geladen...' : 'Beitreten'}
</button>
</form>
<p class="mt-4 text-center text-sm">
<a
href="/recover"
data-testid="link-to-recover"
class="text-blue-600 hover:underline dark:text-blue-400">Ich habe bereits einen Account</a
>
</p>
{/if}
</div>
</div>
</div>
@@ -284,7 +326,7 @@
data-testid="pin-modal"
>
<div
class="w-full max-w-sm rounded-xl bg-white p-6 shadow-lg dark:bg-gray-900"
class="card w-full max-w-sm p-6 shadow-xl"
role="dialog"
aria-modal="true"
aria-labelledby="pin-modal-title"
@@ -298,18 +340,12 @@
wiederherzustellen.
</p>
<div
class="mb-4 flex items-center justify-center gap-3 rounded-lg bg-gray-100 p-4 dark:bg-gray-800"
>
<div class="surface-muted mb-4 flex items-center justify-center gap-3 p-4">
<span
class="text-4xl font-mono font-bold tracking-widest text-gray-900 dark:text-gray-100"
class="font-mono text-4xl font-bold tracking-widest text-gray-900 dark:text-gray-100"
data-testid="pin-display">{pin}</span
>
<button
onclick={copyPin}
data-testid="pin-copy"
class="min-h-11 min-w-11 rounded-md bg-gray-200 px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300 active:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 dark:active:bg-gray-600"
>
<button onclick={copyPin} data-testid="pin-copy" class="btn btn-secondary btn-sm min-h-11">
{copied ? 'Kopiert!' : 'Kopieren'}
</button>
</div>
@@ -317,14 +353,14 @@
<button
onclick={goToFeed}
data-testid="continue-to-feed"
class="mb-2 w-full rounded-lg bg-blue-600 px-4 py-3 font-medium text-white transition hover:bg-blue-700 active:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400 dark:active:bg-blue-400"
class="btn btn-primary btn-block mb-2"
>
Weiter zur Galerie
</button>
<button
type="button"
onclick={closePinModal}
class="w-full rounded-lg py-2 text-sm text-gray-500 hover:text-gray-700 active:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 dark:active:text-gray-200"
class="btn btn-ghost btn-block text-sm text-gray-500 dark:text-gray-400"
>
Schließen
</button>

View File

@@ -132,7 +132,7 @@
placeholder="Dein Name"
maxlength={50}
data-testid="recover-name-input"
class="mb-3 w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-lg text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
class="input mb-3 text-lg"
/>
<input
type="text"
@@ -143,7 +143,7 @@
inputmode="numeric"
pattern="[0-9]*"
data-testid="recover-pin-input"
class="mb-3 w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-center text-2xl font-mono tracking-widest text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:placeholder-gray-500"
class="input mb-3 text-center text-2xl font-mono tracking-widest"
/>
{#if error}
@@ -156,7 +156,7 @@
type="submit"
disabled={loading || !displayName.trim() || pin.length < 4}
data-testid="recover-submit"
class="w-full rounded-lg bg-blue-600 px-4 py-3 text-lg font-medium text-white transition hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400"
class="btn btn-primary btn-lg btn-block"
>
{loading ? 'Wird geladen...' : 'Wiederherstellen'}
</button>

View File

@@ -151,8 +151,7 @@
onclick={handleSubmit}
disabled={stagedFiles.length === 0 || submitting}
data-testid="upload-submit-header"
class="rounded-lg bg-blue-600 px-4 py-1.5 text-sm font-semibold text-white transition
hover:bg-blue-700 disabled:opacity-40 dark:bg-blue-500 dark:hover:bg-blue-400"
class="btn btn-primary btn-sm"
>
{submitting ? 'Wird hochgeladen…' : 'Hochladen'}
</button>
@@ -199,31 +198,30 @@
{:else}
<!-- No files: prompt to go back and pick some -->
<div class="flex flex-1 flex-col items-center justify-center gap-4 p-8 text-center">
<svg
class="h-16 w-16 text-gray-200 dark:text-gray-700"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1"
<div
class="flex h-20 w-20 items-center justify-center rounded-2xl bg-primary-50 dark:bg-primary-900/20"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M9 9.75h.008v.008H9V9.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"
/>
</svg>
<svg
class="h-9 w-9 text-primary-500 dark:text-primary-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M9 9.75h.008v.008H9V9.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM6 20.25h12A2.25 2.25 0 0020.25 18V6A2.25 2.25 0 0018 3.75H6A2.25 2.25 0 003.75 6v12A2.25 2.25 0 006 20.25z"
/>
</svg>
</div>
<div>
<p class="font-medium text-gray-500 dark:text-gray-400">Keine Dateien ausgewählt</p>
<p class="mt-1 text-sm text-gray-400 dark:text-gray-500">
Geh zurück und tippe auf den Plus-Button.
</p>
</div>
<button
onclick={cancel}
class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800"
>
Zurück
</button>
<button onclick={cancel} class="btn btn-secondary btn-sm"> Zurück </button>
</div>
{/if}
@@ -236,9 +234,7 @@
data-testid="upload-caption"
placeholder="Beschreibung hinzufügen… (#hashtags möglich)"
rows="4"
class="w-full resize-none rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-900
placeholder-gray-400 focus:border-blue-400 focus:bg-white focus:outline-none focus:ring-1 focus:ring-blue-200
dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500 dark:focus:bg-gray-800"
class="input resize-none text-sm"
></textarea>
<div class="mt-1 text-xs text-gray-500 text-right dark:text-gray-400">
{caption.length} / {MAX_CAPTION_LENGTH}
@@ -316,8 +312,7 @@
onclick={handleSubmit}
disabled={stagedFiles.length === 0 || submitting}
data-testid="upload-submit"
class="flex w-full items-center justify-center gap-2 rounded-xl bg-blue-600 py-3.5 text-sm font-semibold
text-white transition hover:bg-blue-700 active:scale-[0.98] disabled:opacity-40 dark:bg-blue-500 dark:hover:bg-blue-400"
class="btn btn-primary btn-block"
>
{#if submitting}
<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">