Compare commits

...
21 changed files with 210 additions and 53 deletions
+1
View File
@@ -77,6 +77,7 @@ export const user = pgTable("user", {
password: text(), password: text(),
username: text(), username: text(),
dashboardTheme: text("dashboard_theme").default('none'), dashboardTheme: text("dashboard_theme").default('none'),
dashboardColor: text("dashboard_color"),
}, (table) => [ }, (table) => [
unique("user_email_unique").on(table.email), unique("user_email_unique").on(table.email),
unique("user_username_unique").on(table.username), unique("user_username_unique").on(table.username),
+2
View File
@@ -13,6 +13,7 @@
} }
:root { :root {
color-scheme: light;
--radius: 0.625rem; --radius: 0.625rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695); --foreground: oklch(0.129 0.042 264.695);
@@ -48,6 +49,7 @@
} }
.dark { .dark {
color-scheme: dark;
--background: oklch(0.129 0.042 264.695); --background: oklch(0.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858); --foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.208 0.042 265.755); --card: oklch(0.208 0.042 265.755);
@@ -7,9 +7,13 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
let { let {
isAuthenticated = false isAuthenticated = false,
fallbackColor = null,
fallbackTheme = null
}: { }: {
isAuthenticated?: boolean; isAuthenticated?: boolean;
fallbackColor?: string | null;
fallbackTheme?: string | null;
} = $props(); } = $props();
const t = $derived(languageStore.t); const t = $derived(languageStore.t);
@@ -114,6 +118,8 @@
emptyActionLabel={t.dashboard.createLocalWishlist || "Create local wishlist"} emptyActionLabel={t.dashboard.createLocalWishlist || "Create local wishlist"}
emptyActionHref="/" emptyActionHref="/"
showCreateButton={true} showCreateButton={true}
fallbackColor={fallbackColor}
fallbackTheme={fallbackTheme}
> >
{#snippet actions(wishlist, unlocked)} {#snippet actions(wishlist, unlocked)}
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
@@ -11,6 +11,8 @@
itemCount, itemCount,
color = null, color = null,
theme = null, theme = null,
fallbackColor = null,
fallbackTheme = null,
children children
}: { }: {
title: string; title: string;
@@ -18,14 +20,18 @@
itemCount: number; itemCount: number;
color?: string | null; color?: string | null;
theme?: string | null; theme?: string | null;
fallbackColor?: string | null;
fallbackTheme?: string | null;
children?: Snippet; children?: Snippet;
} = $props(); } = $props();
const cardStyle = $derived(getCardStyle(color)); const finalColor = $derived(color || fallbackColor);
const finalTheme = $derived(theme || fallbackTheme);
const cardStyle = $derived(getCardStyle(color, fallbackColor));
</script> </script>
<Card style={cardStyle} class="h-full flex flex-col relative overflow-hidden"> <Card style={cardStyle} class="h-full flex flex-col relative overflow-hidden">
<ThemeCard themeName={theme} color={color} /> <ThemeCard themeName={finalTheme} color={finalColor} />
<CardHeader class="flex-shrink-0 relative z-10"> <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">
@@ -4,6 +4,8 @@
import EmptyState from '$lib/components/layout/EmptyState.svelte'; import EmptyState from '$lib/components/layout/EmptyState.svelte';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { getCardStyle } from '$lib/utils/colors';
import ThemeCard from '$lib/components/themes/ThemeCard.svelte';
let { let {
title, title,
@@ -13,6 +15,8 @@
emptyDescription, emptyDescription,
emptyActionLabel, emptyActionLabel,
emptyActionHref, emptyActionHref,
fallbackColor = null,
fallbackTheme = null,
headerAction, headerAction,
searchBar, searchBar,
children children
@@ -24,11 +28,15 @@
emptyDescription?: string; emptyDescription?: string;
emptyActionLabel?: string; emptyActionLabel?: string;
emptyActionHref?: string; emptyActionHref?: string;
fallbackColor?: string | null;
fallbackTheme?: string | null;
headerAction?: Snippet; headerAction?: Snippet;
searchBar?: Snippet; searchBar?: Snippet;
children: Snippet<[any]>; children: Snippet<[any]>;
} = $props(); } = $props();
const cardStyle = $derived(getCardStyle(fallbackColor, null));
let scrollContainer: HTMLElement | null = null; let scrollContainer: HTMLElement | null = null;
function handleWheel(event: WheelEvent) { function handleWheel(event: WheelEvent) {
@@ -44,8 +52,9 @@
} }
</script> </script>
<Card> <Card style={cardStyle} class="relative overflow-hidden">
<CardHeader> <ThemeCard themeName={fallbackTheme} color={fallbackColor} showPattern={false} />
<CardHeader class="relative z-10">
<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">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<CardTitle>{title}</CardTitle> <CardTitle>{title}</CardTitle>
@@ -63,7 +72,7 @@
</div> </div>
{/if} {/if}
</CardHeader> </CardHeader>
<CardContent> <CardContent class="relative z-10">
{#if items && items.length > 0} {#if items && items.length > 0}
<div <div
bind:this={scrollContainer} bind:this={scrollContainer}
@@ -21,6 +21,8 @@
emptyActionHref, emptyActionHref,
showCreateButton = false, showCreateButton = false,
hideIfEmpty = false, hideIfEmpty = false,
fallbackColor = null,
fallbackTheme = null,
actions actions
}: { }: {
title: string; title: string;
@@ -32,6 +34,8 @@
emptyActionHref?: string; emptyActionHref?: string;
showCreateButton?: boolean; showCreateButton?: boolean;
hideIfEmpty?: boolean; hideIfEmpty?: boolean;
fallbackColor?: string | null;
fallbackTheme?: string | null;
actions: Snippet<[WishlistItem, boolean]>; // item, unlocked actions: Snippet<[WishlistItem, boolean]>; // item, unlocked
} = $props(); } = $props();
@@ -126,6 +130,8 @@
{emptyDescription} {emptyDescription}
{emptyActionLabel} {emptyActionLabel}
{emptyActionHref} {emptyActionHref}
{fallbackColor}
{fallbackTheme}
> >
{#snippet headerAction()} {#snippet headerAction()}
<div class="flex flex-col sm:flex-row gap-2"> <div class="flex flex-col sm:flex-row gap-2">
@@ -150,6 +156,8 @@
itemCount={wishlist.items?.length || 0} itemCount={wishlist.items?.length || 0}
color={wishlist.color} color={wishlist.color}
theme={wishlist.theme} theme={wishlist.theme}
fallbackColor={fallbackColor}
fallbackTheme={fallbackTheme}
> >
{@render actions(item, unlocked)} {@render actions(item, unlocked)}
</WishlistCard> </WishlistCard>
@@ -3,6 +3,7 @@
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 ThemePicker from '$lib/components/ui/theme-picker.svelte';
import ColorPicker from '$lib/components/ui/ColorPicker.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'; import { enhance } from '$app/forms';
@@ -11,25 +12,27 @@
userName, userName,
userEmail, userEmail,
dashboardTheme = 'none', dashboardTheme = 'none',
dashboardColor = null,
isAuthenticated = false, isAuthenticated = false,
onThemeUpdate onThemeUpdate,
onColorUpdate
}: { }: {
userName?: string | null; userName?: string | null;
userEmail?: string | null; userEmail?: string | null;
dashboardTheme?: string; dashboardTheme?: string;
dashboardColor?: string | null;
isAuthenticated?: boolean; isAuthenticated?: boolean;
onThemeUpdate?: (theme: string | null) => void; onThemeUpdate?: (theme: string | null) => void;
onColorUpdate?: (color: string | null) => void;
} = $props(); } = $props();
const t = $derived(languageStore.t); const t = $derived(languageStore.t);
async function handleThemeChange(theme: string) { async function handleThemeChange(theme: string) {
// Update theme immediately for instant visual feedback
if (onThemeUpdate) { if (onThemeUpdate) {
onThemeUpdate(theme); onThemeUpdate(theme);
} }
// Only submit to database for authenticated users
if (isAuthenticated) { if (isAuthenticated) {
const formData = new FormData(); const formData = new FormData();
formData.append('theme', theme); formData.append('theme', theme);
@@ -40,6 +43,30 @@
}); });
} }
} }
let localColor = $state(dashboardColor);
$effect(() => {
localColor = dashboardColor;
});
async function handleColorChange() {
if (onColorUpdate) {
onColorUpdate(localColor);
}
if (isAuthenticated) {
const formData = new FormData();
if (localColor) {
formData.append('color', localColor);
}
await fetch('?/updateDashboardColor', {
method: 'POST',
body: formData
});
}
}
</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">
@@ -52,6 +79,7 @@
{/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">
<ColorPicker bind:color={localColor} onchange={handleColorChange} />
<ThemePicker value={dashboardTheme} onValueChange={handleThemeChange} /> <ThemePicker value={dashboardTheme} onValueChange={handleThemeChange} />
<LanguageToggle /> <LanguageToggle />
<ThemeToggle /> <ThemeToggle />
+14 -1
View File
@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import ThemeBackground from '$lib/components/themes/ThemeBackground.svelte'; import ThemeBackground from '$lib/components/themes/ThemeBackground.svelte';
import { hexToRgba } from '$lib/utils/colors';
import { themeStore } from '$lib/stores/theme.svelte';
let { let {
children, children,
@@ -13,9 +15,20 @@
theme?: string | null; theme?: string | null;
themeColor?: string | null; themeColor?: string | null;
} = $props(); } = $props();
const backgroundStyle = $derived.by(() => {
if (!themeColor) return '';
const isDark = themeStore.getResolvedTheme() === 'dark';
const tintedColor = hexToRgba(themeColor, 0.15);
return isDark
? `background: linear-gradient(${tintedColor}, ${tintedColor}), #000000;`
: `background-color: ${tintedColor};`;
});
</script> </script>
<div class="min-h-screen p-4 md:p-8 relative overflow-hidden"> <div class="min-h-screen p-4 md:p-8 relative overflow-hidden" style={backgroundStyle}>
<ThemeBackground themeName={theme} color={themeColor} /> <ThemeBackground themeName={theme} color={themeColor} />
<div class="max-w-{maxWidth} mx-auto space-y-6 relative z-10"> <div class="max-w-{maxWidth} mx-auto space-y-6 relative z-10">
{@render children()} {@render children()}
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import TopPattern from './svgs/TopPattern.svelte'; import TopPattern from './svgs/TopPattern.svelte';
import BottomPattern from './svgs/BottomPattern.svelte'; import BottomPattern from './svgs/BottomPattern.svelte';
import { getTheme, getPatternColor, PATTERN_OPACITY } from '$lib/utils/themes'; import { getTheme, PATTERN_OPACITY } from '$lib/utils/themes';
import { themeStore } from '$lib/stores/theme.svelte';
let { let {
themeName, themeName,
@@ -16,7 +17,10 @@
} = $props(); } = $props();
const theme = $derived(getTheme(themeName)); const theme = $derived(getTheme(themeName));
const patternColor = $derived(getPatternColor(color)); const patternColor = $derived.by(() => {
const isDark = themeStore.getResolvedTheme() === 'dark';
return isDark ? '#FFFFFF' : '#000000';
});
</script> </script>
{#if theme.pattern !== 'none'} {#if theme.pattern !== 'none'}
+12 -6
View File
@@ -1,19 +1,25 @@
<script lang="ts"> <script lang="ts">
import CardPattern from './svgs/CardPattern.svelte'; import CardPattern from './svgs/CardPattern.svelte';
import { getTheme, getPatternColor, PATTERN_OPACITY } from '$lib/utils/themes'; import { getTheme, PATTERN_OPACITY } from '$lib/utils/themes';
import { themeStore } from '$lib/stores/theme.svelte';
let { let {
themeName, themeName,
color color,
showPattern = true
}: { }: {
themeName?: string; themeName?: string | null;
color?: string; color?: string | null;
showPattern?: boolean;
} = $props(); } = $props();
const theme = $derived(getTheme(themeName)); const theme = $derived(getTheme(themeName));
const patternColor = $derived(getPatternColor(color)); const patternColor = $derived.by(() => {
const isDark = themeStore.getResolvedTheme() === 'dark';
return isDark ? '#FFFFFF' : '#000000';
});
</script> </script>
{#if theme.pattern !== 'none'} {#if showPattern && theme.pattern !== 'none'}
<CardPattern pattern={theme.pattern} color={patternColor} opacity={PATTERN_OPACITY} /> <CardPattern pattern={theme.pattern} color={patternColor} opacity={PATTERN_OPACITY} />
{/if} {/if}
@@ -7,22 +7,27 @@
import { enhance } from "$app/forms"; import { enhance } from "$app/forms";
import { flip } from "svelte/animate"; import { flip } from "svelte/animate";
import { languageStore } from '$lib/stores/language.svelte'; import { languageStore } from '$lib/stores/language.svelte';
import ThemeCard from "$lib/components/themes/ThemeCard.svelte";
import { getCardStyle } from "$lib/utils/colors";
let { let {
items = $bindable([]), items = $bindable([]),
rearranging, rearranging,
onStartEditing, onStartEditing,
onReorder, onReorder,
theme = null theme = null,
wishlistColor = 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; theme?: string | null;
wishlistColor?: string | null;
} = $props(); } = $props();
const t = $derived(languageStore.t); const t = $derived(languageStore.t);
const cardStyle = $derived(getCardStyle(wishlistColor));
</script> </script>
<div class="space-y-4"> <div class="space-y-4">
@@ -30,7 +35,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} {theme} showDragHandle={false}> <WishlistItem {item} {theme} {wishlistColor} showDragHandle={false}>
<div class="flex gap-2"> <div class="flex gap-2">
<Button <Button
type="button" type="button"
@@ -66,8 +71,9 @@
{/each} {/each}
</div> </div>
{:else} {:else}
<Card> <Card style={cardStyle} class="relative overflow-hidden">
<CardContent class="p-12"> <ThemeCard themeName={theme} color={wishlistColor} />
<CardContent class="p-12 relative z-10">
<EmptyState <EmptyState
message={t.wishlist.noWishes + ". " + t.wishlist.addFirstWish + "!"} message={t.wishlist.noWishes + ". " + t.wishlist.addFirstWish + "!"}
/> />
@@ -4,15 +4,18 @@
import { Label } from '$lib/components/ui/label'; import { Label } from '$lib/components/ui/label';
import { Card, CardContent } from '$lib/components/ui/card'; import { Card, CardContent } from '$lib/components/ui/card';
import { languageStore } from '$lib/stores/language.svelte'; import { languageStore } from '$lib/stores/language.svelte';
import { getCardStyle } from '$lib/utils/colors';
interface Props { interface Props {
publicUrl: string; publicUrl: string;
ownerUrl?: string; ownerUrl?: string;
wishlistColor?: string | null;
} }
let { publicUrl, ownerUrl }: Props = $props(); let { publicUrl, ownerUrl, wishlistColor = null }: Props = $props();
const t = $derived(languageStore.t); const t = $derived(languageStore.t);
const cardStyle = $derived(getCardStyle(null, wishlistColor));
let copiedPublic = $state(false); let copiedPublic = $state(false);
let copiedOwner = $state(false); let copiedOwner = $state(false);
@@ -34,7 +37,7 @@
} }
</script> </script>
<Card> <Card style={cardStyle}>
<CardContent class="space-y-4 pt-6"> <CardContent class="space-y-4 pt-6">
<div class="space-y-2"> <div class="space-y-2">
<Label>{t.wishlist.shareViewOnly}</Label> <Label>{t.wishlist.shareViewOnly}</Label>
@@ -8,6 +8,7 @@
import ThemePicker from "$lib/components/ui/theme-picker.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';
import { getCardStyle } from '$lib/utils/colors';
let { let {
wishlist, wishlist,
@@ -39,6 +40,8 @@
: null, : null,
); );
const cardStyle = $derived(getCardStyle(null, wishlistColor));
async function saveTitle() { async function saveTitle() {
if (!wishlistTitle.trim()) { if (!wishlistTitle.trim()) {
wishlistTitle = wishlist.title; wishlistTitle = wishlist.title;
@@ -134,7 +137,7 @@
</div> </div>
<!-- Settings Card --> <!-- Settings Card -->
<Card> <Card style={cardStyle}>
<CardContent class="pt-6 space-y-4"> <CardContent class="pt-6 space-y-4">
<!-- Description --> <!-- Description -->
<div class="space-y-2"> <div class="space-y-2">
@@ -13,6 +13,7 @@
children?: any; children?: any;
showDragHandle?: boolean; showDragHandle?: boolean;
theme?: string | null; theme?: string | null;
wishlistColor?: string | null;
} }
let { let {
@@ -20,7 +21,8 @@
showImage = true, showImage = true,
children, children,
showDragHandle = false, showDragHandle = false,
theme = null theme = null,
wishlistColor = null
}: Props = $props(); }: Props = $props();
const t = $derived(languageStore.t); const t = $derived(languageStore.t);
@@ -51,7 +53,7 @@
return `${symbol}${amount}`; return `${symbol}${amount}`;
} }
const cardStyle = $derived(getCardStyle(item.color)); const cardStyle = $derived(getCardStyle(item.color, wishlistColor));
</script> </script>
<Card style={cardStyle} class="relative overflow-hidden"> <Card style={cardStyle} class="relative overflow-hidden">
+2 -1
View File
@@ -13,7 +13,8 @@ export const users = pgTable('user', {
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') dashboardTheme: text('dashboard_theme').default('none'),
dashboardColor: text('dashboard_color')
}); });
export const accounts = pgTable( export const accounts = pgTable(
+4 -5
View File
@@ -5,6 +5,7 @@ type ResolvedTheme = 'light' | 'dark';
class ThemeStore { class ThemeStore {
current = $state<Theme>('system'); current = $state<Theme>('system');
resolved = $state<ResolvedTheme>('light');
constructor() { constructor() {
if (browser) { if (browser) {
@@ -27,6 +28,8 @@ class ThemeStore {
const isDark = this.current === 'dark' || const isDark = this.current === 'dark' ||
(this.current === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches); (this.current === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
this.resolved = isDark ? 'dark' : 'light';
if (isDark) { if (isDark) {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
} else { } else {
@@ -35,11 +38,7 @@ class ThemeStore {
} }
getResolvedTheme(): ResolvedTheme { getResolvedTheme(): ResolvedTheme {
if (!browser) return 'light'; return this.resolved;
const isDark = this.current === 'dark' ||
(this.current === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
return isDark ? 'dark' : 'light';
} }
toggle() { toggle() {
+5 -3
View File
@@ -11,8 +11,10 @@ export function hexToRgba(hex: string, alpha: number): string {
/** /**
* Generate card style string with color, transparency, and blur * Generate card style string with color, transparency, and blur
*/ */
export function getCardStyle(color: string | null): string { export function getCardStyle(color: string | null, fallbackColor?: string | null): string {
if (!color) return ''; const activeColor = color || fallbackColor;
if (!activeColor) return '';
return `background-color: ${hexToRgba(color, 0.2)} !important; backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important;`; const opacity = color ? 0.2 : 0.15;
return `background-color: ${hexToRgba(activeColor, opacity)} !important; backdrop-filter: blur(10px) !important; -webkit-backdrop-filter: blur(10px) !important;`;
} }
+16
View File
@@ -174,6 +174,22 @@ export const actions: Actions = {
.set({ dashboardTheme: theme }) .set({ dashboardTheme: theme })
.where(eq(users.id, session.user.id)); .where(eq(users.id, session.user.id));
return { success: true };
},
updateDashboardColor: async ({ request, locals }) => {
const session = await locals.auth();
if (!session?.user?.id) {
throw redirect(303, '/signin');
}
const formData = await request.formData();
const color = formData.get('color') as string | null;
await db.update(users)
.set({ dashboardColor: color })
.where(eq(users.id, session.user.id));
return { success: true }; return { success: true };
} }
}; };
+41 -2
View File
@@ -24,7 +24,21 @@
} }
} }
// For anonymous users, get color from localStorage
function getInitialColor() {
if (data.isAuthenticated) {
return data.user?.dashboardColor || null;
} else {
// Anonymous user - get from localStorage
if (typeof window !== 'undefined') {
return localStorage.getItem('dashboardColor') || null;
}
return null;
}
}
let currentTheme = $state(getInitialTheme()); let currentTheme = $state(getInitialTheme());
let currentColor = $state(getInitialColor());
// Save to localStorage when theme changes for anonymous users // Save to localStorage when theme changes for anonymous users
function handleThemeUpdate(theme: string | null) { function handleThemeUpdate(theme: string | null) {
@@ -35,6 +49,19 @@
} }
} }
// Save to localStorage when color changes for anonymous users
function handleColorUpdate(color: string | null) {
currentColor = color;
if (!data.isAuthenticated && typeof window !== 'undefined') {
if (color) {
localStorage.setItem('dashboardColor', color);
} else {
localStorage.removeItem('dashboardColor');
}
}
}
const t = $derived(languageStore.t); const t = $derived(languageStore.t);
// Only owned wishlists for "My Wishlists" // Only owned wishlists for "My Wishlists"
@@ -58,17 +85,23 @@
}); });
</script> </script>
<PageContainer theme={currentTheme} themeColor={null}> <PageContainer theme={currentTheme} themeColor={currentColor}>
<DashboardHeader <DashboardHeader
userName={data.user?.name} userName={data.user?.name}
userEmail={data.user?.email} userEmail={data.user?.email}
dashboardTheme={currentTheme} dashboardTheme={currentTheme}
dashboardColor={currentColor}
isAuthenticated={data.isAuthenticated} isAuthenticated={data.isAuthenticated}
onThemeUpdate={handleThemeUpdate} onThemeUpdate={handleThemeUpdate}
onColorUpdate={handleColorUpdate}
/> />
<!-- Local Wishlists Section (for anonymous and authenticated users) --> <!-- Local Wishlists Section (for anonymous and authenticated users) -->
<LocalWishlistsSection isAuthenticated={data.isAuthenticated} /> <LocalWishlistsSection
isAuthenticated={data.isAuthenticated}
fallbackColor={currentColor}
fallbackTheme={currentTheme}
/>
{#if data.isAuthenticated} {#if data.isAuthenticated}
<!-- My Wishlists Section --> <!-- My Wishlists Section -->
@@ -80,6 +113,8 @@
emptyActionLabel={t.dashboard.emptyWishlistsAction} emptyActionLabel={t.dashboard.emptyWishlistsAction}
emptyActionHref="/" emptyActionHref="/"
showCreateButton={true} showCreateButton={true}
fallbackColor={currentColor}
fallbackTheme={currentTheme}
> >
{#snippet actions(wishlist, unlocked)} {#snippet actions(wishlist, unlocked)}
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
@@ -135,6 +170,8 @@
emptyMessage={t.dashboard.emptyClaimedWishlists} emptyMessage={t.dashboard.emptyClaimedWishlists}
emptyDescription={t.dashboard.emptyClaimedWishlistsDescription} emptyDescription={t.dashboard.emptyClaimedWishlistsDescription}
hideIfEmpty={true} hideIfEmpty={true}
fallbackColor={currentColor}
fallbackTheme={currentTheme}
> >
{#snippet actions(wishlist, unlocked)} {#snippet actions(wishlist, unlocked)}
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
@@ -189,6 +226,8 @@
items={savedWishlists()} items={savedWishlists()}
emptyMessage={t.dashboard.emptySavedWishlists} emptyMessage={t.dashboard.emptySavedWishlists}
emptyDescription={t.dashboard.emptySavedWishlistsDescription} emptyDescription={t.dashboard.emptySavedWishlistsDescription}
fallbackColor={currentColor}
fallbackTheme={currentTheme}
> >
{#snippet actions(saved, unlocked)} {#snippet actions(saved, unlocked)}
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
+8 -13
View File
@@ -7,8 +7,6 @@
CardTitle, CardTitle,
} from "$lib/components/ui/card"; } from "$lib/components/ui/card";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import type { PageData } from "./$types"; import type { PageData } from "./$types";
import WishlistItem from "$lib/components/wishlist/WishlistItem.svelte"; import WishlistItem from "$lib/components/wishlist/WishlistItem.svelte";
import ReservationButton from "$lib/components/wishlist/ReservationButton.svelte"; import ReservationButton from "$lib/components/wishlist/ReservationButton.svelte";
@@ -19,6 +17,7 @@
import { getCardStyle } from "$lib/utils/colors"; import { getCardStyle } from "$lib/utils/colors";
import { languageStore } from '$lib/stores/language.svelte'; import { languageStore } from '$lib/stores/language.svelte';
import SearchBar from "$lib/components/ui/SearchBar.svelte"; import SearchBar from "$lib/components/ui/SearchBar.svelte";
import ThemeCard from "$lib/components/themes/ThemeCard.svelte";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@@ -41,7 +40,6 @@
showDashboardLink={true} showDashboardLink={true}
/> />
<!-- Header -->
<Card style={headerCardStyle}> <Card style={headerCardStyle}>
<CardContent class="pt-6"> <CardContent class="pt-6">
<div class="flex flex-wrap items-start justify-between gap-4"> <div class="flex flex-wrap items-start justify-between gap-4">
@@ -55,7 +53,6 @@
</div> </div>
{#if data.isAuthenticated} {#if data.isAuthenticated}
{#if data.isClaimed} {#if data.isClaimed}
<!-- User has claimed this wishlist - show claimed status -->
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
@@ -64,7 +61,6 @@
{t.wishlist.youClaimedThis} {t.wishlist.youClaimedThis}
</Button> </Button>
{:else if data.isSaved} {:else if data.isSaved}
<!-- User has saved but not claimed - show unsave button -->
<form method="POST" action="?/unsaveWishlist" use:enhance> <form method="POST" action="?/unsaveWishlist" use:enhance>
<input <input
type="hidden" type="hidden"
@@ -76,7 +72,6 @@
</Button> </Button>
</form> </form>
{:else} {:else}
<!-- Not saved - show save button -->
<form method="POST" action="?/saveWishlist" use:enhance={() => { <form method="POST" action="?/saveWishlist" use:enhance={() => {
return async ({ update }) => { return async ({ update }) => {
await update({ reset: false }); await update({ reset: false });
@@ -101,16 +96,14 @@
</CardContent> </CardContent>
</Card> </Card>
<!-- Search Bar -->
{#if data.wishlist.items && data.wishlist.items.length > 0} {#if data.wishlist.items && data.wishlist.items.length > 0}
<SearchBar bind:value={searchQuery} /> <SearchBar bind:value={searchQuery} />
{/if} {/if}
<!-- Items List -->
<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} theme={data.wishlist.theme}> <WishlistItem {item} theme={data.wishlist.theme} wishlistColor={data.wishlist.color}>
<ReservationButton <ReservationButton
itemId={item.id} itemId={item.id}
isReserved={item.isReserved} isReserved={item.isReserved}
@@ -121,16 +114,18 @@
</WishlistItem> </WishlistItem>
{/each} {/each}
{:else if data.wishlist.items && data.wishlist.items.length > 0} {:else if data.wishlist.items && data.wishlist.items.length > 0}
<Card> <Card style={headerCardStyle} class="relative overflow-hidden">
<CardContent class="p-12"> <ThemeCard themeName={data.wishlist.theme} color={data.wishlist.color} />
<CardContent class="p-12 relative z-10">
<EmptyState <EmptyState
message="No wishes match your search." message="No wishes match your search."
/> />
</CardContent> </CardContent>
</Card> </Card>
{:else} {:else}
<Card> <Card style={headerCardStyle} class="relative overflow-hidden">
<CardContent class="p-12"> <ThemeCard themeName={data.wishlist.theme} color={data.wishlist.color} />
<CardContent class="p-12 relative z-10">
<EmptyState <EmptyState
message={t.wishlist.emptyWishes} message={t.wishlist.emptyWishes}
/> />
+10 -2
View File
@@ -23,6 +23,7 @@
let editFormElement = $state<HTMLElement | null>(null); let editFormElement = $state<HTMLElement | null>(null);
let searchQuery = $state(""); let searchQuery = $state("");
let currentTheme = $state(data.wishlist.theme || 'none'); let currentTheme = $state(data.wishlist.theme || 'none');
let currentColor = $state(data.wishlist.color);
let items = $state<Item[]>([]); let items = $state<Item[]>([]);
@@ -111,9 +112,14 @@
currentTheme = theme || 'none'; currentTheme = theme || 'none';
await wishlistUpdates.updateTheme(theme); await wishlistUpdates.updateTheme(theme);
} }
async function handleColorUpdate(color: string | null) {
currentColor = color;
await wishlistUpdates.updateColor(color);
}
</script> </script>
<PageContainer maxWidth="4xl" theme={currentTheme} themeColor={data.wishlist.color}> <PageContainer maxWidth="4xl" theme={currentTheme} themeColor={currentColor}>
<Navigation <Navigation
isAuthenticated={data.isAuthenticated} isAuthenticated={data.isAuthenticated}
showDashboardLink={true} showDashboardLink={true}
@@ -123,7 +129,7 @@
wishlist={data.wishlist} wishlist={data.wishlist}
onTitleUpdate={wishlistUpdates.updateTitle} onTitleUpdate={wishlistUpdates.updateTitle}
onDescriptionUpdate={wishlistUpdates.updateDescription} onDescriptionUpdate={wishlistUpdates.updateDescription}
onColorUpdate={wishlistUpdates.updateColor} onColorUpdate={handleColorUpdate}
onEndDateUpdate={wishlistUpdates.updateEndDate} onEndDateUpdate={wishlistUpdates.updateEndDate}
onThemeUpdate={handleThemeUpdate} onThemeUpdate={handleThemeUpdate}
/> />
@@ -131,6 +137,7 @@
<ShareLinks <ShareLinks
publicUrl={data.publicUrl} publicUrl={data.publicUrl}
ownerUrl="/wishlist/{data.wishlist.ownerToken}/edit" ownerUrl="/wishlist/{data.wishlist.ownerToken}/edit"
wishlistColor={currentColor}
/> />
<ClaimWishlistSection <ClaimWishlistSection
@@ -176,6 +183,7 @@
onStartEditing={startEditing} onStartEditing={startEditing}
onReorder={handleReorder} onReorder={handleReorder}
theme={currentTheme} theme={currentTheme}
wishlistColor={currentColor}
/> />
<DangerZone bind:unlocked={rearranging} /> <DangerZone bind:unlocked={rearranging} />