feat(theme+comments): runtime colour theme and COMMENTS_ENABLED switch
Colour theme is configurable at runtime from two seed colours (brand + accent); neutrals stay fixed for contrast safety. Tailwind v4 var()-based tokens let a :root:root override recolour everything with no rebuild; the 50->950 ramps are derived via a color-mix ladder. Config lives in the DB config table (admin UI: Config > Farbschema, presets + custom pickers + live preview), served on the public /event endpoint with env defaults (THEME_PRESET/PRIMARY/ACCENT), propagated live via event-updated SSE, and cached in localStorage for a no-flash boot. The keepsake export mirrors the same ladder in Rust so offline archives match the event theme. COMMENTS_ENABLED (env, default true) is a boot-time kill-switch: the backend rejects new comments with 403 and the frontend hides the comment button (feed card) and panel/composer (lightbox). Existing comments stay in the DB, hidden, and return when re-enabled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
import { avatarPalette, initials } from '$lib/avatar';
|
||||
import { vibrate } from '$lib/haptics';
|
||||
import { now } from '$lib/now';
|
||||
import { commentsEnabled } from '$lib/event-config-store';
|
||||
import HeartBurst from './HeartBurst.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -205,19 +206,27 @@
|
||||
</svg>
|
||||
{upload.like_count}
|
||||
</button>
|
||||
<button
|
||||
onclick={() => oncomment(upload.id)}
|
||||
class="flex items-center gap-1.5 text-sm font-medium text-gray-500 transition-colors hover:text-blue-500 active:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 dark:active:text-blue-400"
|
||||
>
|
||||
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
{upload.comment_count}
|
||||
</button>
|
||||
{#if $commentsEnabled}
|
||||
<button
|
||||
onclick={() => oncomment(upload.id)}
|
||||
class="flex items-center gap-1.5 text-sm font-medium text-gray-500 transition-colors hover:text-blue-500 active:text-blue-500 dark:text-gray-400 dark:hover:text-blue-400 dark:active:text-blue-400"
|
||||
>
|
||||
<svg
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||
/>
|
||||
</svg>
|
||||
{upload.comment_count}
|
||||
</button>
|
||||
{/if}
|
||||
{#if isOwn}
|
||||
<span class="ml-auto text-xs text-gray-400 dark:text-gray-500">Eigener Beitrag</span>
|
||||
{/if}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { modalInert } from '$lib/actions/modal-inert';
|
||||
import { toastError } from '$lib/toast-store';
|
||||
import { vibrate } from '$lib/haptics';
|
||||
import { commentsEnabled } from '$lib/event-config-store';
|
||||
import HeartBurst from './HeartBurst.svelte';
|
||||
|
||||
const COMMENT_MAX = 500;
|
||||
@@ -224,81 +225,91 @@
|
||||
{/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 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 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)}
|
||||
<!-- Comments list + composer — hidden entirely when comments are disabled instance-wide -->
|
||||
{#if $commentsEnabled}
|
||||
<div class="flex-1 overflow-y-auto p-3">
|
||||
{#if comments.length === 0}
|
||||
<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 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 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"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if comment.user_id === userId}
|
||||
<button
|
||||
onclick={() => deleteComment(comment.id)}
|
||||
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">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Comment input -->
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitComment();
|
||||
}}
|
||||
class="border-t border-gray-100 p-3 dark:border-gray-800"
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newComment}
|
||||
placeholder="Kommentar schreiben..."
|
||||
maxlength={COMMENT_MAX}
|
||||
class="input flex-1"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !newComment.trim()}
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
Senden
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 text-right text-xs"
|
||||
class:text-gray-400={newComment.length < 450}
|
||||
class:dark:text-gray-500={newComment.length < 450}
|
||||
class:text-amber-600={newComment.length >= 450 && newComment.length < COMMENT_MAX}
|
||||
class:dark:text-amber-400={newComment.length >= 450 && newComment.length < COMMENT_MAX}
|
||||
class:text-red-600={newComment.length >= COMMENT_MAX}
|
||||
class:dark:text-red-400={newComment.length >= COMMENT_MAX}
|
||||
<!-- Comment input -->
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitComment();
|
||||
}}
|
||||
class="border-t border-gray-100 p-3 dark:border-gray-800"
|
||||
>
|
||||
{newComment.length}/{COMMENT_MAX}
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newComment}
|
||||
placeholder="Kommentar schreiben..."
|
||||
maxlength={COMMENT_MAX}
|
||||
class="input flex-1"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !newComment.trim()}
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
Senden
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1 text-right text-xs"
|
||||
class:text-gray-400={newComment.length < 450}
|
||||
class:dark:text-gray-500={newComment.length < 450}
|
||||
class:text-amber-600={newComment.length >= 450 && newComment.length < COMMENT_MAX}
|
||||
class:dark:text-amber-400={newComment.length >= 450 && newComment.length < COMMENT_MAX}
|
||||
class:text-red-600={newComment.length >= COMMENT_MAX}
|
||||
class:dark:text-red-400={newComment.length >= COMMENT_MAX}
|
||||
>
|
||||
{newComment.length}/{COMMENT_MAX}
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user