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:
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { FeedUpload } from '$lib/types';
|
||||
import { api, ApiError } from '$lib/api';
|
||||
import { api } from '$lib/api';
|
||||
import { getUserId } from '$lib/auth';
|
||||
import { dataMode, pickMediaUrl } from '$lib/data-mode-store';
|
||||
import { doubletap } from '$lib/actions/doubletap';
|
||||
|
||||
interface CommentDto {
|
||||
id: string;
|
||||
@@ -24,6 +26,15 @@
|
||||
let newComment = $state('');
|
||||
let loading = $state(false);
|
||||
let userId = getUserId();
|
||||
let heartBurst = $state(false);
|
||||
|
||||
const mediaSrc = $derived(pickMediaUrl($dataMode, upload));
|
||||
|
||||
function triggerHeartBurst() {
|
||||
heartBurst = true;
|
||||
onlike(upload.id);
|
||||
setTimeout(() => (heartBurst = false), 700);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
loadComments();
|
||||
@@ -77,10 +88,19 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@keyframes heart-burst {
|
||||
0% { transform: scale(0.5); opacity: 0; }
|
||||
30% { transform: scale(1.2); opacity: 1; }
|
||||
70% { transform: scale(1); opacity: 1; }
|
||||
100% { transform: scale(1.4); opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4" role="dialog">
|
||||
<div class="flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl bg-white">
|
||||
<div class="flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl bg-white dark:bg-gray-900">
|
||||
<!-- Media -->
|
||||
<div class="relative bg-black">
|
||||
<button onclick={onclose} class="absolute right-2 top-2 z-10 rounded-full bg-black/50 p-1.5 text-white hover:bg-black/70">
|
||||
@@ -88,36 +108,59 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
{#if isVideo(upload.mime_type)}
|
||||
<video
|
||||
src={upload.preview_url ?? ''}
|
||||
controls
|
||||
class="max-h-[50vh] w-full object-contain"
|
||||
poster={upload.thumbnail_url ?? undefined}
|
||||
></video>
|
||||
{:else}
|
||||
<img
|
||||
src={upload.preview_url ?? ''}
|
||||
alt=""
|
||||
class="max-h-[50vh] w-full object-contain"
|
||||
/>
|
||||
{/if}
|
||||
<div
|
||||
class="relative"
|
||||
use:doubletap
|
||||
ondoubletap={triggerHeartBurst}
|
||||
>
|
||||
{#if isVideo(upload.mime_type)}
|
||||
<video
|
||||
src={mediaSrc}
|
||||
controls
|
||||
class="max-h-[50vh] w-full object-contain"
|
||||
poster={upload.thumbnail_url ?? undefined}
|
||||
></video>
|
||||
{:else}
|
||||
<img
|
||||
src={mediaSrc}
|
||||
alt=""
|
||||
class="max-h-[50vh] w-full object-contain select-none"
|
||||
draggable="false"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if heartBurst}
|
||||
<span
|
||||
class="pointer-events-none absolute inset-0 flex items-center justify-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
class="h-24 w-24 text-red-500 drop-shadow-lg"
|
||||
style="animation: heart-burst 700ms ease-out forwards;"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M12 21s-7-4.534-9.5-9.034C.5 9.466 2.5 5 7 5c2.09 0 3.534 1.083 5 3 1.466-1.917 2.91-3 5-3 4.5 0 6.5 4.466 4.5 6.966C19 16.466 12 21 12 21z" />
|
||||
</svg>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info + Comments -->
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
<div class="border-b border-gray-100 p-3">
|
||||
<div class="border-b border-gray-100 p-3 dark:border-gray-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<span class="font-medium text-gray-900">{upload.uploader_name}</span>
|
||||
<span class="ml-2 text-xs text-gray-400">{formatTime(upload.created_at)}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{upload.uploader_name}</span>
|
||||
<span class="ml-2 text-xs text-gray-400 dark:text-gray-500">{formatTime(upload.created_at)}</span>
|
||||
</div>
|
||||
<button
|
||||
onclick={() => onlike(upload.id)}
|
||||
class="flex items-center gap-1 rounded-full px-2.5 py-1 text-sm transition {
|
||||
upload.liked_by_me
|
||||
? 'bg-red-50 text-red-600'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
? 'bg-red-50 text-red-600 dark:bg-red-950/40 dark:text-red-300'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700'
|
||||
}"
|
||||
>
|
||||
<svg class="h-4 w-4 {upload.liked_by_me ? 'fill-current' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -127,27 +170,27 @@
|
||||
</button>
|
||||
</div>
|
||||
{#if upload.caption}
|
||||
<p class="mt-1 text-sm text-gray-700">{upload.caption}</p>
|
||||
<p class="mt-1 text-sm text-gray-700 dark:text-gray-300">{upload.caption}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Comments list -->
|
||||
<div class="flex-1 overflow-y-auto p-3">
|
||||
{#if comments.length === 0}
|
||||
<p class="text-center text-sm text-gray-400">Noch keine Kommentare.</p>
|
||||
<p class="text-center text-sm text-gray-400 dark:text-gray-500">Noch keine Kommentare.</p>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
{#each comments as comment (comment.id)}
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="flex-1">
|
||||
<span class="text-sm font-medium text-gray-900">{comment.uploader_name}</span>
|
||||
<span class="ml-1 text-sm text-gray-700">{comment.body}</span>
|
||||
<div class="mt-0.5 text-xs text-gray-400">{formatTime(comment.created_at)}</div>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-gray-100">{comment.uploader_name}</span>
|
||||
<span class="ml-1 text-sm text-gray-700 dark:text-gray-300">{comment.body}</span>
|
||||
<div class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">{formatTime(comment.created_at)}</div>
|
||||
</div>
|
||||
{#if comment.user_id === userId}
|
||||
<button
|
||||
onclick={() => deleteComment(comment.id)}
|
||||
class="shrink-0 text-gray-400 hover:text-red-500"
|
||||
class="shrink-0 text-gray-400 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-400"
|
||||
aria-label="Löschen"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -164,19 +207,19 @@
|
||||
<!-- Comment input -->
|
||||
<form
|
||||
onsubmit={(e) => { e.preventDefault(); submitComment(); }}
|
||||
class="flex gap-2 border-t border-gray-100 p-3"
|
||||
class="flex gap-2 border-t border-gray-100 p-3 dark:border-gray-800"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newComment}
|
||||
placeholder="Kommentar schreiben..."
|
||||
maxlength={500}
|
||||
class="flex-1 rounded-lg border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none"
|
||||
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-blue-500 focus:outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-500"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !newComment.trim()}
|
||||
class="rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-700 disabled:opacity-50"
|
||||
class="rounded-lg bg-blue-600 px-3 py-2 text-sm font-medium text-white transition hover:bg-blue-700 disabled:opacity-50 dark:bg-blue-500 dark:hover:bg-blue-400"
|
||||
>
|
||||
Senden
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user