wip: not loading themes until reload, missing in dashboard, bad alignment and scaling
This commit is contained in:
@@ -3,26 +3,30 @@
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import { getCardStyle } from '$lib/utils/colors';
|
import { getCardStyle } from '$lib/utils/colors';
|
||||||
|
import ThemeCard from '$lib/components/themes/ThemeCard.svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
itemCount,
|
itemCount,
|
||||||
color = null,
|
color = null,
|
||||||
|
theme = null,
|
||||||
children
|
children
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
itemCount: number;
|
itemCount: number;
|
||||||
color?: string | null;
|
color?: string | null;
|
||||||
|
theme?: string | null;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const cardStyle = $derived(getCardStyle(color));
|
const cardStyle = $derived(getCardStyle(color));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card style={cardStyle} class="h-full flex flex-col">
|
<Card style={cardStyle} class="h-full flex flex-col relative overflow-hidden">
|
||||||
<CardHeader class="flex-shrink-0">
|
<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">
|
<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">
|
<CardTitle class="text-lg flex items-center gap-2 flex-1 min-w-0">
|
||||||
<span class="truncate">{title}</span>
|
<span class="truncate">{title}</span>
|
||||||
@@ -35,7 +39,7 @@
|
|||||||
<CardDescription class="line-clamp-3 whitespace-pre-line">{description}</CardDescription>
|
<CardDescription class="line-clamp-3 whitespace-pre-line">{description}</CardDescription>
|
||||||
{/if}
|
{/if}
|
||||||
</CardHeader>
|
</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}
|
{#if children}
|
||||||
<div>
|
<div>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
description={getWishlistDescription(item)}
|
description={getWishlistDescription(item)}
|
||||||
itemCount={wishlist.items?.length || 0}
|
itemCount={wishlist.items?.length || 0}
|
||||||
color={wishlist.color}
|
color={wishlist.color}
|
||||||
|
theme={wishlist.theme}
|
||||||
>
|
>
|
||||||
{@render actions(item, unlocked)}
|
{@render actions(item, unlocked)}
|
||||||
</WishlistCard>
|
</WishlistCard>
|
||||||
|
|||||||
@@ -2,13 +2,41 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { ThemeToggle } from '$lib/components/ui/theme-toggle';
|
import { ThemeToggle } from '$lib/components/ui/theme-toggle';
|
||||||
import { LanguageToggle } from '$lib/components/ui/language-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 { signOut } from '@auth/sveltekit/client';
|
||||||
import { languageStore } from '$lib/stores/language.svelte';
|
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 t = $derived(languageStore.t);
|
||||||
const isAuthenticated = $derived(!!userName || !!userEmail);
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
@@ -21,6 +49,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 sm:gap-2 flex-shrink-0">
|
<div class="flex items-center gap-1 sm:gap-2 flex-shrink-0">
|
||||||
|
{#if isAuthenticated}
|
||||||
|
<ThemePicker value={currentTheme} onValueChange={handleThemeChange} />
|
||||||
|
{/if}
|
||||||
<LanguageToggle />
|
<LanguageToggle />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
{#if isAuthenticated}
|
{#if isAuthenticated}
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte';
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="min-h-screen p-4 md:p-8">
|
<div class="min-h-screen p-4 md:p-8 relative overflow-hidden">
|
||||||
<div class="max-w-{maxWidth} mx-auto space-y-6">
|
<ThemeBackground themeName={theme} color={themeColor} />
|
||||||
|
<div class="max-w-{maxWidth} mx-auto space-y-6 relative z-10">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
37
src/lib/components/themes/ThemeBackground.svelte
Normal file
37
src/lib/components/themes/ThemeBackground.svelte
Normal 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}
|
||||||
23
src/lib/components/themes/ThemeCard.svelte
Normal file
23
src/lib/components/themes/ThemeCard.svelte
Normal 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}
|
||||||
38
src/lib/components/themes/svgs/BottomPattern.svelte
Normal file
38
src/lib/components/themes/svgs/BottomPattern.svelte
Normal 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>
|
||||||
38
src/lib/components/themes/svgs/CardPattern.svelte
Normal file
38
src/lib/components/themes/svgs/CardPattern.svelte
Normal 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>
|
||||||
38
src/lib/components/themes/svgs/TopPattern.svelte
Normal file
38
src/lib/components/themes/svgs/TopPattern.svelte
Normal 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>
|
||||||
68
src/lib/components/ui/theme-picker.svelte
Normal file
68
src/lib/components/ui/theme-picker.svelte
Normal 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>
|
||||||
@@ -12,12 +12,14 @@
|
|||||||
items = $bindable([]),
|
items = $bindable([]),
|
||||||
rearranging,
|
rearranging,
|
||||||
onStartEditing,
|
onStartEditing,
|
||||||
onReorder
|
onReorder,
|
||||||
|
theme = null
|
||||||
}: {
|
}: {
|
||||||
items: Item[];
|
items: Item[];
|
||||||
rearranging: boolean;
|
rearranging: boolean;
|
||||||
onStartEditing: (item: Item) => void;
|
onStartEditing: (item: Item) => void;
|
||||||
onReorder: (items: Item[]) => Promise<void>;
|
onReorder: (items: Item[]) => Promise<void>;
|
||||||
|
theme?: string | null;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const t = $derived(languageStore.t);
|
const t = $derived(languageStore.t);
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{#each items as item (item.id)}
|
{#each items as item (item.id)}
|
||||||
<div animate:flip={{ duration: 300 }}>
|
<div animate:flip={{ duration: 300 }}>
|
||||||
<WishlistItem {item} showDragHandle={false}>
|
<WishlistItem {item} {theme} showDragHandle={false}>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { Textarea } from "$lib/components/ui/textarea";
|
import { Textarea } from "$lib/components/ui/textarea";
|
||||||
import { Pencil, Check, X } from "lucide-svelte";
|
import { Pencil, Check, X } from "lucide-svelte";
|
||||||
import ColorPicker from "$lib/components/ui/ColorPicker.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 type { Wishlist } from "$lib/server/schema";
|
||||||
import { languageStore } from '$lib/stores/language.svelte';
|
import { languageStore } from '$lib/stores/language.svelte';
|
||||||
|
|
||||||
@@ -13,13 +14,15 @@
|
|||||||
onTitleUpdate,
|
onTitleUpdate,
|
||||||
onDescriptionUpdate,
|
onDescriptionUpdate,
|
||||||
onColorUpdate,
|
onColorUpdate,
|
||||||
onEndDateUpdate
|
onEndDateUpdate,
|
||||||
|
onThemeUpdate
|
||||||
}: {
|
}: {
|
||||||
wishlist: Wishlist;
|
wishlist: Wishlist;
|
||||||
onTitleUpdate: (title: string) => Promise<boolean>;
|
onTitleUpdate: (title: string) => Promise<boolean>;
|
||||||
onDescriptionUpdate: (description: string | null) => Promise<boolean>;
|
onDescriptionUpdate: (description: string | null) => Promise<boolean>;
|
||||||
onColorUpdate: (color: string | null) => void;
|
onColorUpdate: (color: string | null) => void;
|
||||||
onEndDateUpdate: (endDate: string | null) => void;
|
onEndDateUpdate: (endDate: string | null) => void;
|
||||||
|
onThemeUpdate: (theme: string | null) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const t = $derived(languageStore.t);
|
const t = $derived(languageStore.t);
|
||||||
@@ -29,6 +32,7 @@
|
|||||||
let wishlistTitle = $state(wishlist.title);
|
let wishlistTitle = $state(wishlist.title);
|
||||||
let wishlistDescription = $state(wishlist.description || "");
|
let wishlistDescription = $state(wishlist.description || "");
|
||||||
let wishlistColor = $state<string | null>(wishlist.color);
|
let wishlistColor = $state<string | null>(wishlist.color);
|
||||||
|
let wishlistTheme = $state<string>(wishlist.theme || 'none');
|
||||||
let wishlistEndDate = $state<string | null>(
|
let wishlistEndDate = $state<string | null>(
|
||||||
wishlist.endDate
|
wishlist.endDate
|
||||||
? new Date(wishlist.endDate).toISOString().split("T")[0]
|
? new Date(wishlist.endDate).toISOString().split("T")[0]
|
||||||
@@ -112,7 +116,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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
|
<ColorPicker
|
||||||
bind:color={wishlistColor}
|
bind:color={wishlistColor}
|
||||||
onchange={() => onColorUpdate(wishlistColor)}
|
onchange={() => onColorUpdate(wishlistColor)}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
import { getCardStyle } from '$lib/utils/colors';
|
import { getCardStyle } from '$lib/utils/colors';
|
||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from "$lib/components/ui/button";
|
||||||
import { languageStore } from '$lib/stores/language.svelte';
|
import { languageStore } from '$lib/stores/language.svelte';
|
||||||
|
import ThemeCard from '$lib/components/themes/ThemeCard.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: Item;
|
item: Item;
|
||||||
showImage?: boolean;
|
showImage?: boolean;
|
||||||
children?: any;
|
children?: any;
|
||||||
showDragHandle?: boolean;
|
showDragHandle?: boolean;
|
||||||
|
theme?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -18,6 +20,7 @@
|
|||||||
showImage = true,
|
showImage = true,
|
||||||
children,
|
children,
|
||||||
showDragHandle = false,
|
showDragHandle = false,
|
||||||
|
theme = null
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const t = $derived(languageStore.t);
|
const t = $derived(languageStore.t);
|
||||||
@@ -51,8 +54,9 @@
|
|||||||
const cardStyle = $derived(getCardStyle(item.color));
|
const cardStyle = $derived(getCardStyle(item.color));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card style={cardStyle}>
|
<Card style={cardStyle} class="relative overflow-hidden">
|
||||||
<CardContent class="p-6">
|
<ThemeCard themeName={theme} color={item.color} />
|
||||||
|
<CardContent class="p-6 relative z-10">
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
{#if showDragHandle}
|
{#if showDragHandle}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export const users = pgTable('user', {
|
|||||||
emailVerified: timestamp('emailVerified', { mode: 'date' }),
|
emailVerified: timestamp('emailVerified', { mode: 'date' }),
|
||||||
image: text('image'),
|
image: text('image'),
|
||||||
password: text('password'),
|
password: text('password'),
|
||||||
username: text('username').unique()
|
username: text('username').unique(),
|
||||||
|
dashboardTheme: text('dashboard_theme').default('none')
|
||||||
});
|
});
|
||||||
|
|
||||||
export const accounts = pgTable(
|
export const accounts = pgTable(
|
||||||
@@ -70,6 +71,7 @@ export const wishlists = pgTable('wishlists', {
|
|||||||
publicToken: text('public_token').notNull().unique(),
|
publicToken: text('public_token').notNull().unique(),
|
||||||
isFavorite: boolean('is_favorite').default(false).notNull(),
|
isFavorite: boolean('is_favorite').default(false).notNull(),
|
||||||
color: text('color'),
|
color: text('color'),
|
||||||
|
theme: text('theme').default('none'),
|
||||||
endDate: timestamp('end_date', { mode: 'date' }),
|
endDate: timestamp('end_date', { mode: 'date' }),
|
||||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
updatedAt: timestamp('updated_at').defaultNow().notNull()
|
updatedAt: timestamp('updated_at').defaultNow().notNull()
|
||||||
|
|||||||
51
src/lib/utils/themes.ts
Normal file
51
src/lib/utils/themes.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Theme configuration for SVG overlays
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ThemePattern = 'waves' | 'geometric' | 'dots' | 'none';
|
||||||
|
|
||||||
|
export interface Theme {
|
||||||
|
name: string;
|
||||||
|
pattern: ThemePattern;
|
||||||
|
opacity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AVAILABLE_THEMES: Record<string, Theme> = {
|
||||||
|
none: {
|
||||||
|
name: 'None',
|
||||||
|
pattern: 'none',
|
||||||
|
opacity: 0
|
||||||
|
},
|
||||||
|
waves: {
|
||||||
|
name: 'Waves',
|
||||||
|
pattern: 'waves',
|
||||||
|
opacity: 0.1
|
||||||
|
},
|
||||||
|
geometric: {
|
||||||
|
name: 'Geometric',
|
||||||
|
pattern: 'geometric',
|
||||||
|
opacity: 0.1
|
||||||
|
},
|
||||||
|
dots: {
|
||||||
|
name: 'Dots',
|
||||||
|
pattern: 'dots',
|
||||||
|
opacity: 0.15
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_THEME = 'none';
|
||||||
|
|
||||||
|
export function getTheme(themeName?: string | null): Theme {
|
||||||
|
if (!themeName || !AVAILABLE_THEMES[themeName]) {
|
||||||
|
return AVAILABLE_THEMES[DEFAULT_THEME];
|
||||||
|
}
|
||||||
|
return AVAILABLE_THEMES[themeName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get color from a CSS color string or class
|
||||||
|
* For now, we'll use currentColor which inherits from the parent
|
||||||
|
*/
|
||||||
|
export function getThemeColor(color?: string | null): string {
|
||||||
|
return color || 'currentColor';
|
||||||
|
}
|
||||||
@@ -38,6 +38,10 @@ export async function updateEndDate(endDate: string | null): Promise<boolean> {
|
|||||||
return updateWishlist({ endDate: endDate || "" });
|
return updateWishlist({ endDate: endDate || "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateTheme(theme: string | null): Promise<boolean> {
|
||||||
|
return updateWishlist({ theme: theme || "none" });
|
||||||
|
}
|
||||||
|
|
||||||
export async function reorderItems(items: Array<{ id: string; order: number }>): Promise<boolean> {
|
export async function reorderItems(items: Array<{ id: string; order: number }>): Promise<boolean> {
|
||||||
const response = await fetch("?/reorderItems", {
|
const response = await fetch("?/reorderItems", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { wishlists, savedWishlists } from '$lib/server/schema';
|
import { wishlists, savedWishlists, users } from '$lib/server/schema';
|
||||||
import { eq, and } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
|
||||||
export const load: PageServerLoad = async (event) => {
|
export const load: PageServerLoad = async (event) => {
|
||||||
@@ -149,6 +149,26 @@ export const actions: Actions = {
|
|||||||
eq(wishlists.userId, session.user.id)
|
eq(wishlists.userId, session.user.id)
|
||||||
));
|
));
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
updateDashboardTheme: async ({ request, locals }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw redirect(303, '/signin');
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
const theme = formData.get('theme') as string;
|
||||||
|
|
||||||
|
if (!theme) {
|
||||||
|
return { success: false, error: 'Theme is required' };
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.update(users)
|
||||||
|
.set({ dashboardTheme: theme })
|
||||||
|
.where(eq(users.id, session.user.id));
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,8 +34,12 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PageContainer>
|
<PageContainer theme={data.user?.dashboardTheme} themeColor="hsl(var(--primary))">
|
||||||
<DashboardHeader userName={data.user?.name} userEmail={data.user?.email} />
|
<DashboardHeader
|
||||||
|
userName={data.user?.name}
|
||||||
|
userEmail={data.user?.email}
|
||||||
|
dashboardTheme={data.user?.dashboardTheme || 'none'}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Local Wishlists Section (for anonymous and authenticated users) -->
|
<!-- Local Wishlists Section (for anonymous and authenticated users) -->
|
||||||
<LocalWishlistsSection isAuthenticated={data.isAuthenticated} />
|
<LocalWishlistsSection isAuthenticated={data.isAuthenticated} />
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PageContainer maxWidth="4xl">
|
<PageContainer maxWidth="4xl" theme={data.wishlist.theme} themeColor={data.wishlist.color}>
|
||||||
<Navigation
|
<Navigation
|
||||||
isAuthenticated={data.isAuthenticated}
|
isAuthenticated={data.isAuthenticated}
|
||||||
showDashboardLink={true}
|
showDashboardLink={true}
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{#if filteredItems.length > 0}
|
{#if filteredItems.length > 0}
|
||||||
{#each filteredItems as item}
|
{#each filteredItems as item}
|
||||||
<WishlistItem {item}>
|
<WishlistItem {item} theme={data.wishlist.theme}>
|
||||||
<ReservationButton
|
<ReservationButton
|
||||||
itemId={item.id}
|
itemId={item.id}
|
||||||
isReserved={item.isReserved}
|
isReserved={item.isReserved}
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ export const actions: Actions = {
|
|||||||
const title = formData.get('title');
|
const title = formData.get('title');
|
||||||
const description = formData.get('description');
|
const description = formData.get('description');
|
||||||
const endDate = formData.get('endDate');
|
const endDate = formData.get('endDate');
|
||||||
|
const theme = formData.get('theme');
|
||||||
|
|
||||||
const wishlist = await db.query.wishlists.findFirst({
|
const wishlist = await db.query.wishlists.findFirst({
|
||||||
where: eq(wishlists.ownerToken, params.token)
|
where: eq(wishlists.ownerToken, params.token)
|
||||||
@@ -237,6 +238,10 @@ export const actions: Actions = {
|
|||||||
updates.endDate = endDateStr ? new Date(endDateStr) : null;
|
updates.endDate = endDateStr ? new Date(endDateStr) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (theme !== null) {
|
||||||
|
updates.theme = theme?.toString().trim() || 'none';
|
||||||
|
}
|
||||||
|
|
||||||
await db.update(wishlists)
|
await db.update(wishlists)
|
||||||
.set(updates)
|
.set(updates)
|
||||||
.where(eq(wishlists.id, wishlist.id));
|
.where(eq(wishlists.id, wishlist.id));
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PageContainer maxWidth="4xl">
|
<PageContainer maxWidth="4xl" theme={data.wishlist.theme} themeColor={data.wishlist.color}>
|
||||||
<Navigation
|
<Navigation
|
||||||
isAuthenticated={data.isAuthenticated}
|
isAuthenticated={data.isAuthenticated}
|
||||||
showDashboardLink={true}
|
showDashboardLink={true}
|
||||||
@@ -119,6 +119,7 @@
|
|||||||
onDescriptionUpdate={wishlistUpdates.updateDescription}
|
onDescriptionUpdate={wishlistUpdates.updateDescription}
|
||||||
onColorUpdate={wishlistUpdates.updateColor}
|
onColorUpdate={wishlistUpdates.updateColor}
|
||||||
onEndDateUpdate={wishlistUpdates.updateEndDate}
|
onEndDateUpdate={wishlistUpdates.updateEndDate}
|
||||||
|
onThemeUpdate={wishlistUpdates.updateTheme}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ShareLinks
|
<ShareLinks
|
||||||
@@ -168,6 +169,7 @@
|
|||||||
{rearranging}
|
{rearranging}
|
||||||
onStartEditing={startEditing}
|
onStartEditing={startEditing}
|
||||||
onReorder={handleReorder}
|
onReorder={handleReorder}
|
||||||
|
theme={data.wishlist.theme}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DangerZone bind:unlocked={rearranging} />
|
<DangerZone bind:unlocked={rearranging} />
|
||||||
|
|||||||
Reference in New Issue
Block a user