wip: not loading themes until reload, missing in dashboard, bad alignment and scaling

This commit is contained in:
rasmusq
2025-11-28 00:26:43 +01:00
parent 85f8671c72
commit 7c6ff9458f
21 changed files with 417 additions and 20 deletions

View File

@@ -3,26 +3,30 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
import type { Snippet } from 'svelte';
import { getCardStyle } from '$lib/utils/colors';
import ThemeCard from '$lib/components/themes/ThemeCard.svelte';
let {
title,
description,
itemCount,
color = null,
theme = null,
children
}: {
title: string;
description?: string | null;
itemCount: number;
color?: string | null;
theme?: string | null;
children?: Snippet;
} = $props();
const cardStyle = $derived(getCardStyle(color));
</script>
<Card style={cardStyle} class="h-full flex flex-col">
<CardHeader class="flex-shrink-0">
<Card style={cardStyle} class="h-full flex flex-col relative overflow-hidden">
<ThemeCard themeName={theme} color={color} />
<CardHeader class="flex-shrink-0 relative z-10">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-1 sm:gap-2">
<CardTitle class="text-lg flex items-center gap-2 flex-1 min-w-0">
<span class="truncate">{title}</span>
@@ -35,7 +39,7 @@
<CardDescription class="line-clamp-3 whitespace-pre-line">{description}</CardDescription>
{/if}
</CardHeader>
<CardContent class="space-y-2 flex-1 flex flex-col justify-end">
<CardContent class="space-y-2 flex-1 flex flex-col justify-end relative z-10">
{#if children}
<div>
{@render children()}

View File

@@ -149,6 +149,7 @@
description={getWishlistDescription(item)}
itemCount={wishlist.items?.length || 0}
color={wishlist.color}
theme={wishlist.theme}
>
{@render actions(item, unlocked)}
</WishlistCard>

View File

@@ -2,13 +2,41 @@
import { Button } from '$lib/components/ui/button';
import { ThemeToggle } from '$lib/components/ui/theme-toggle';
import { LanguageToggle } from '$lib/components/ui/language-toggle';
import ThemePicker from '$lib/components/ui/theme-picker.svelte';
import { signOut } from '@auth/sveltekit/client';
import { languageStore } from '$lib/stores/language.svelte';
import { enhance } from '$app/forms';
let { userName, userEmail }: { userName?: string | null; userEmail?: string | null } = $props();
let {
userName,
userEmail,
dashboardTheme = 'none'
}: {
userName?: string | null;
userEmail?: string | null;
dashboardTheme?: string;
} = $props();
const t = $derived(languageStore.t);
const isAuthenticated = $derived(!!userName || !!userEmail);
let currentTheme = $state(dashboardTheme);
async function handleThemeChange(theme: string) {
currentTheme = theme;
// Submit form to update theme
const formData = new FormData();
formData.append('theme', theme);
await fetch('?/updateDashboardTheme', {
method: 'POST',
body: formData
});
// Reload to apply new theme
window.location.reload();
}
</script>
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
@@ -21,6 +49,9 @@
{/if}
</div>
<div class="flex items-center gap-1 sm:gap-2 flex-shrink-0">
{#if isAuthenticated}
<ThemePicker value={currentTheme} onValueChange={handleThemeChange} />
{/if}
<LanguageToggle />
<ThemeToggle />
{#if isAuthenticated}

View File

@@ -1,11 +1,23 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import ThemeBackground from '$lib/components/themes/ThemeBackground.svelte';
let { children, maxWidth = '6xl' }: { children: Snippet; maxWidth?: string } = $props();
let {
children,
maxWidth = '6xl',
theme = null,
themeColor = null
}: {
children: Snippet;
maxWidth?: string;
theme?: string | null;
themeColor?: string | null;
} = $props();
</script>
<div class="min-h-screen p-4 md:p-8">
<div class="max-w-{maxWidth} mx-auto space-y-6">
<div class="min-h-screen p-4 md:p-8 relative overflow-hidden">
<ThemeBackground themeName={theme} color={themeColor} />
<div class="max-w-{maxWidth} mx-auto space-y-6 relative z-10">
{@render children()}
</div>
</div>

View File

@@ -0,0 +1,37 @@
<script lang="ts">
import TopPattern from './svgs/TopPattern.svelte';
import BottomPattern from './svgs/BottomPattern.svelte';
import { getTheme, getThemeColor } from '$lib/utils/themes';
let {
themeName,
color,
showTop = true,
showBottom = true
}: {
themeName?: string | null;
color?: string | null;
showTop?: boolean;
showBottom?: boolean;
} = $props();
const theme = $derived(getTheme(themeName));
const themeColor = $derived(getThemeColor(color));
</script>
{#if theme.pattern !== 'none'}
{#if showTop}
<TopPattern
pattern={theme.pattern}
color={themeColor}
opacity={theme.opacity}
/>
{/if}
{#if showBottom}
<BottomPattern
pattern={theme.pattern}
color={themeColor}
opacity={theme.opacity}
/>
{/if}
{/if}

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import CardPattern from './svgs/CardPattern.svelte';
import { getTheme, getThemeColor } from '$lib/utils/themes';
let {
themeName,
color
}: {
themeName?: string | null;
color?: string | null;
} = $props();
const theme = $derived(getTheme(themeName));
const themeColor = $derived(getThemeColor(color));
</script>
{#if theme.pattern !== 'none'}
<CardPattern
pattern={theme.pattern}
color={themeColor}
opacity={theme.opacity}
/>
{/if}

View File

@@ -0,0 +1,38 @@
<script lang="ts">
let {
color = 'currentColor',
opacity = 0.1,
pattern = 'waves'
}: {
color?: string;
opacity?: number;
pattern?: 'waves' | 'geometric' | 'dots';
} = $props();
</script>
<div class="absolute bottom-0 left-0 right-0 pointer-events-none overflow-hidden" style="opacity: {opacity};">
{#if pattern === 'waves'}
<svg class="w-full h-auto" viewBox="0 0 1440 200" xmlns="http://www.w3.org/2000/svg">
<path
fill={color}
d="M0,100 C240,50 480,150 720,100 C960,50 1200,150 1440,100 L1440,200 L0,200 Z"
/>
</svg>
{:else if pattern === 'geometric'}
<svg class="w-full h-auto" viewBox="0 0 1440 200" xmlns="http://www.w3.org/2000/svg">
<path
fill={color}
d="M0,200 L720,50 L1440,200 Z"
/>
</svg>
{:else if pattern === 'dots'}
<svg class="w-full h-auto" viewBox="0 0 1440 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots-pattern-bottom" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
<circle cx="20" cy="20" r="3" fill={color} />
</pattern>
</defs>
<rect width="1440" height="200" fill="url(#dots-pattern-bottom)" />
</svg>
{/if}
</div>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
let {
color = 'currentColor',
opacity = 0.08,
pattern = 'waves'
}: {
color?: string;
opacity?: number;
pattern?: 'waves' | 'geometric' | 'dots';
} = $props();
</script>
<div class="absolute bottom-0 left-0 right-0 pointer-events-none overflow-hidden rounded-b-lg" style="opacity: {opacity};">
{#if pattern === 'waves'}
<svg class="w-full h-auto" viewBox="0 0 400 80" xmlns="http://www.w3.org/2000/svg">
<path
fill={color}
d="M0,40 C100,20 200,60 300,40 C350,30 375,35 400,40 L400,80 L0,80 Z"
/>
</svg>
{:else if pattern === 'geometric'}
<svg class="w-full h-auto" viewBox="0 0 400 80" xmlns="http://www.w3.org/2000/svg">
<path
fill={color}
d="M0,80 L200,20 L400,80 Z"
/>
</svg>
{:else if pattern === 'dots'}
<svg class="w-full h-auto" viewBox="0 0 400 80" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots-pattern-card" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="10" cy="10" r="2" fill={color} />
</pattern>
</defs>
<rect width="400" height="80" fill="url(#dots-pattern-card)" />
</svg>
{/if}
</div>

View File

@@ -0,0 +1,38 @@
<script lang="ts">
let {
color = 'currentColor',
opacity = 0.1,
pattern = 'waves'
}: {
color?: string;
opacity?: number;
pattern?: 'waves' | 'geometric' | 'dots';
} = $props();
</script>
<div class="absolute top-0 left-0 right-0 pointer-events-none overflow-hidden" style="opacity: {opacity};">
{#if pattern === 'waves'}
<svg class="w-full h-auto" viewBox="0 0 1440 200" xmlns="http://www.w3.org/2000/svg">
<path
fill={color}
d="M0,100 C240,150 480,50 720,100 C960,150 1200,50 1440,100 L1440,0 L0,0 Z"
/>
</svg>
{:else if pattern === 'geometric'}
<svg class="w-full h-auto" viewBox="0 0 1440 200" xmlns="http://www.w3.org/2000/svg">
<path
fill={color}
d="M0,0 L720,150 L1440,0 Z"
/>
</svg>
{:else if pattern === 'dots'}
<svg class="w-full h-auto" viewBox="0 0 1440 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="dots-pattern-top" x="0" y="0" width="40" height="40" patternUnits="userSpaceOnUse">
<circle cx="20" cy="20" r="3" fill={color} />
</pattern>
</defs>
<rect width="1440" height="200" fill="url(#dots-pattern-top)" />
</svg>
{/if}
</div>

View File

@@ -0,0 +1,68 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Palette } from 'lucide-svelte';
import { AVAILABLE_THEMES } from '$lib/utils/themes';
let {
value = 'none',
onValueChange
}: {
value?: string;
onValueChange: (theme: string) => void;
} = $props();
let showMenu = $state(false);
function toggleMenu() {
showMenu = !showMenu;
}
function handleSelect(themeName: string) {
onValueChange(themeName);
showMenu = false;
}
function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLElement;
if (!target.closest('.theme-picker-menu')) {
showMenu = false;
}
}
$effect(() => {
if (showMenu) {
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}
});
</script>
<div class="relative theme-picker-menu">
<Button variant="outline" size="icon" onclick={toggleMenu} aria-label="Select theme pattern">
<Palette class="h-[1.2rem] w-[1.2rem]" />
</Button>
{#if showMenu}
<div
class="absolute left-0 sm:right-0 sm:left-auto mt-2 w-40 rounded-md border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-950 shadow-lg z-50"
>
<div class="py-1">
{#each Object.entries(AVAILABLE_THEMES) as [key, theme]}
<button
type="button"
class="w-full text-left px-4 py-2 text-sm hover:bg-slate-100 dark:hover:bg-slate-900 transition-colors flex items-center justify-between"
class:font-bold={value === key}
class:bg-slate-100={value === key}
class:dark:bg-slate-900={value === key}
onclick={() => handleSelect(key)}
>
<span>{theme.name}</span>
{#if value === key}
<span class="ml-2"></span>
{/if}
</button>
{/each}
</div>
</div>
{/if}
</div>

View File

@@ -12,12 +12,14 @@
items = $bindable([]),
rearranging,
onStartEditing,
onReorder
onReorder,
theme = null
}: {
items: Item[];
rearranging: boolean;
onStartEditing: (item: Item) => void;
onReorder: (items: Item[]) => Promise<void>;
theme?: string | null;
} = $props();
const t = $derived(languageStore.t);
@@ -28,7 +30,7 @@
<div class="space-y-4">
{#each items as item (item.id)}
<div animate:flip={{ duration: 300 }}>
<WishlistItem {item} showDragHandle={false}>
<WishlistItem {item} {theme} showDragHandle={false}>
<div class="flex gap-2">
<Button
type="button"

View File

@@ -5,6 +5,7 @@
import { Textarea } from "$lib/components/ui/textarea";
import { Pencil, Check, X } from "lucide-svelte";
import ColorPicker from "$lib/components/ui/ColorPicker.svelte";
import ThemePicker from "$lib/components/ui/theme-picker.svelte";
import type { Wishlist } from "$lib/server/schema";
import { languageStore } from '$lib/stores/language.svelte';
@@ -13,13 +14,15 @@
onTitleUpdate,
onDescriptionUpdate,
onColorUpdate,
onEndDateUpdate
onEndDateUpdate,
onThemeUpdate
}: {
wishlist: Wishlist;
onTitleUpdate: (title: string) => Promise<boolean>;
onDescriptionUpdate: (description: string | null) => Promise<boolean>;
onColorUpdate: (color: string | null) => void;
onEndDateUpdate: (endDate: string | null) => void;
onThemeUpdate: (theme: string | null) => void;
} = $props();
const t = $derived(languageStore.t);
@@ -29,6 +32,7 @@
let wishlistTitle = $state(wishlist.title);
let wishlistDescription = $state(wishlist.description || "");
let wishlistColor = $state<string | null>(wishlist.color);
let wishlistTheme = $state<string>(wishlist.theme || 'none');
let wishlistEndDate = $state<string | null>(
wishlist.endDate
? new Date(wishlist.endDate).toISOString().split("T")[0]
@@ -112,7 +116,16 @@
{/if}
</button>
</div>
<div class="flex-shrink-0">
<div class="flex items-center gap-2 flex-shrink-0">
<ThemePicker
value={wishlistTheme}
onValueChange={async (theme) => {
wishlistTheme = theme;
await onThemeUpdate(theme);
// Force reactivity by updating the wishlist object
wishlist.theme = theme;
}}
/>
<ColorPicker
bind:color={wishlistColor}
onchange={() => onColorUpdate(wishlistColor)}

View File

@@ -5,12 +5,14 @@
import { getCardStyle } from '$lib/utils/colors';
import { Button } from "$lib/components/ui/button";
import { languageStore } from '$lib/stores/language.svelte';
import ThemeCard from '$lib/components/themes/ThemeCard.svelte';
interface Props {
item: Item;
showImage?: boolean;
children?: any;
showDragHandle?: boolean;
theme?: string | null;
}
let {
@@ -18,6 +20,7 @@
showImage = true,
children,
showDragHandle = false,
theme = null
}: Props = $props();
const t = $derived(languageStore.t);
@@ -51,8 +54,9 @@
const cardStyle = $derived(getCardStyle(item.color));
</script>
<Card style={cardStyle}>
<CardContent class="p-6">
<Card style={cardStyle} class="relative overflow-hidden">
<ThemeCard themeName={theme} color={item.color} />
<CardContent class="p-6 relative z-10">
<div class="flex gap-4">
{#if showDragHandle}
<div