feat(ui): v0.16 features + dark mode across every page

Wires up everything from the previous commits into actual UI surfaces, and
applies Tailwind dark: variants throughout. All pages now support the
'system' / 'light' / 'dark' preference set in the onboarding step or in
Mein Konto → Design.

Layout & nav:
- routes/+layout.svelte: initTheme(), global pin-reset SSE handler that
  filters by user_id and calls clearPin(), one-shot /me/context fetch
  on boot to hydrate privacyNote + quota.
- components/BottomNav.svelte: dark variants on the frosted-glass bar.
- components/UploadSheet.svelte: dark variants on backdrop, sheet,
  source buttons.
- components/OnboardingGuide.svelte: new "Helles oder dunkles Design?"
  step (3-option custom-radio grid), reactive currentStep with proper
  type narrowing, dark variants throughout. Privacy-note nudge appears
  on the PIN step only when one is configured.

Feed:
- routes/feed/+page.svelte: diashow entry icon (tablet/desktop only),
  long-press → ContextSheet (Löschen for own posts, Original anzeigen
  for all), upload-deleted + feed-delta SSE handlers, dark variants on
  header, search, autocomplete, filter chips, empty states.
- components/FeedListCard.svelte: long-press wireup, double-tap-to-like,
  data-mode-aware mediaSrc via pickMediaUrl, kebab fallback for desktop,
  isOwn prop, dark variants.
- components/FeedGrid.svelte: long-press wireup, dark variants.
- components/LightboxModal.svelte: data-mode-aware src, double-tap heart
  burst, dark variants on card / comments / input.
- components/HashtagChips.svelte: dark variants.

Account:
- routes/account/+page.svelte: theme picker (3-button radio grid), data
  mode picker (with confirm sheet for Original), live quota widget,
  preformatted Datenschutzhinweis block, diashow tile (mobile only),
  pin now sourced from the $currentPin store so a global pin-reset
  clears it live, clearQueue() on explicit logout, dark variants
  across every card + both bottom sheets.

Upload:
- routes/upload/+page.svelte: per-user quota progress bar above the
  submit button, dark variants.

Host & Admin:
- routes/host/+page.svelte: PIN-reset confirm + one-time PIN modal,
  hosts may demote other hosts, canResetPinFor() helper, dark variants
  on all cards, modals, stats, toast.
- routes/admin/+page.svelte: Config form rebuilt as CONFIG_GROUPS with
  per-field kind (number / bool / text), renders toggles for the
  rate-limit + quota switches and a textarea for the privacy_note;
  Nutzer tab gains PIN reset + hosts-may-demote-hosts wiring; same
  one-time PIN modal; dark variants everywhere.
- routes/admin/login/+page.svelte: dark variants.

Join / Recover / Export:
- routes/join/+page.svelte: rename inline link to
  "Ich habe bereits einen Account", dark variants.
- routes/recover/+page.svelte: dark variants.
- routes/export/+page.svelte: dark variants on status cards + HTML
  guide modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-16 14:33:30 +02:00
parent 8a769b52bf
commit e619a3bd64
17 changed files with 1295 additions and 438 deletions

View File

@@ -86,20 +86,20 @@
}
</script>
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4">
<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="mb-5 rounded-lg border border-amber-200 bg-amber-50 p-4">
<p class="font-semibold text-amber-900">{takenName}" ist bereits vergeben.</p>
<p class="mt-1 text-sm text-amber-800">
<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>
<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">
<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>
@@ -111,17 +111,17 @@
maxlength={4}
inputmode="numeric"
pattern="[0-9]*"
class="mb-3 w-full rounded-lg border border-gray-300 px-4 py-3 text-center text-2xl font-mono tracking-widest focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200"
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"
/>
{#if recoveryError}
<p class="mb-3 text-sm text-red-600">{recoveryError}</p>
<p class="mb-3 text-sm text-red-600 dark:text-red-400">{recoveryError}</p>
{/if}
<button
type="submit"
disabled={recoveryLoading || recoveryPin.length < 4}
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"
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"
>
{recoveryLoading ? 'Wird angemeldet...' : 'Anmelden'}
</button>
@@ -129,15 +129,15 @@
<button
onclick={tryDifferentName}
class="w-full rounded-lg border border-gray-300 px-4 py-3 font-medium text-gray-700 transition hover:bg-gray-50"
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>
{:else}
<!-- Normal join form -->
<h1 class="mb-2 text-center text-2xl font-bold text-gray-900">Willkommen!</h1>
<p class="mb-6 text-center text-gray-600">Gib deinen Namen ein, um dem Event beizutreten.</p>
<h1 class="mb-2 text-center text-2xl font-bold text-gray-900 dark:text-gray-100">Willkommen!</h1>
<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
@@ -145,25 +145,24 @@
bind:value={displayName}
placeholder="Dein Name"
maxlength={50}
class="mb-3 w-full rounded-lg border border-gray-300 px-4 py-3 text-lg focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200"
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">{error}</p>
<p class="mb-3 text-sm text-red-600 dark:text-red-400">{error}</p>
{/if}
<button
type="submit"
disabled={loading || !displayName.trim()}
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"
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 text-gray-500">
Schon dabei?
<a href="/recover" class="text-blue-600 hover:underline">Mit PIN wiederherstellen</a>
<p class="mt-4 text-center text-sm">
<a href="/recover" class="text-blue-600 hover:underline dark:text-blue-400">Ich habe bereits einen Account</a>
</p>
{/if}
@@ -172,17 +171,17 @@
{#if showPinModal}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4">
<div class="w-full max-w-sm rounded-xl bg-white p-6 shadow-lg">
<h2 class="mb-2 text-xl font-bold text-gray-900">Dein Wiederherstellungs-PIN</h2>
<p class="mb-4 text-sm text-gray-600">
<div class="w-full max-w-sm rounded-xl bg-white p-6 shadow-lg dark:bg-gray-900">
<h2 class="mb-2 text-xl font-bold text-gray-900 dark:text-gray-100">Dein Wiederherstellungs-PIN</h2>
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Merke dir diesen PIN! Du brauchst ihn, um dein Konto auf einem anderen Gerät wiederherzustellen.
</p>
<div class="mb-4 flex items-center justify-center gap-3 rounded-lg bg-gray-100 p-4">
<span class="text-4xl font-mono font-bold tracking-widest text-gray-900">{pin}</span>
<div class="mb-4 flex items-center justify-center gap-3 rounded-lg bg-gray-100 p-4 dark:bg-gray-800">
<span class="text-4xl font-mono font-bold tracking-widest text-gray-900 dark:text-gray-100">{pin}</span>
<button
onclick={copyPin}
class="rounded-md bg-gray-200 px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-300"
class="rounded-md bg-gray-200 px-3 py-1 text-sm font-medium text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
>
{copied ? 'Kopiert!' : 'Kopieren'}
</button>
@@ -190,7 +189,7 @@
<button
onclick={goToFeed}
class="w-full rounded-lg bg-blue-600 px-4 py-3 font-medium text-white transition hover:bg-blue-700"
class="w-full rounded-lg bg-blue-600 px-4 py-3 font-medium text-white transition hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-400"
>
Weiter zur Galerie
</button>