feature/builtin-themes #4
@@ -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()}
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
description={getWishlistDescription(item)}
|
||||
itemCount={wishlist.items?.length || 0}
|
||||
color={wishlist.color}
|
||||
theme={wishlist.theme}
|
||||
>
|
||||
{@render actions(item, unlocked)}
|
||||
</WishlistCard>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
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([]),
|
||||
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"
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,8 @@ export const users = pgTable('user', {
|
||||
emailVerified: timestamp('emailVerified', { mode: 'date' }),
|
||||
image: text('image'),
|
||||
password: text('password'),
|
||||
username: text('username').unique()
|
||||
username: text('username').unique(),
|
||||
dashboardTheme: text('dashboard_theme').default('none')
|
||||
});
|
||||
|
||||
export const accounts = pgTable(
|
||||
@@ -70,6 +71,7 @@ export const wishlists = pgTable('wishlists', {
|
||||
publicToken: text('public_token').notNull().unique(),
|
||||
isFavorite: boolean('is_favorite').default(false).notNull(),
|
||||
color: text('color'),
|
||||
theme: text('theme').default('none'),
|
||||
endDate: timestamp('end_date', { mode: 'date' }),
|
||||
createdAt: timestamp('created_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 || "" });
|
||||
}
|
||||
|
||||
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> {
|
||||
const response = await fetch("?/reorderItems", {
|
||||
method: "POST",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
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';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
@@ -149,6 +149,26 @@ export const actions: Actions = {
|
||||
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 };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,8 +34,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<PageContainer>
|
||||
<DashboardHeader userName={data.user?.name} userEmail={data.user?.email} />
|
||||
<PageContainer theme={data.user?.dashboardTheme} themeColor="hsl(var(--primary))">
|
||||
<DashboardHeader
|
||||
userName={data.user?.name}
|
||||
userEmail={data.user?.email}
|
||||
dashboardTheme={data.user?.dashboardTheme || 'none'}
|
||||
/>
|
||||
|
||||
<!-- Local Wishlists Section (for anonymous and authenticated users) -->
|
||||
<LocalWishlistsSection isAuthenticated={data.isAuthenticated} />
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<PageContainer maxWidth="4xl">
|
||||
<PageContainer maxWidth="4xl" theme={data.wishlist.theme} themeColor={data.wishlist.color}>
|
||||
<Navigation
|
||||
isAuthenticated={data.isAuthenticated}
|
||||
showDashboardLink={true}
|
||||
@@ -110,7 +110,7 @@
|
||||
<div class="space-y-4">
|
||||
{#if filteredItems.length > 0}
|
||||
{#each filteredItems as item}
|
||||
<WishlistItem {item}>
|
||||
<WishlistItem {item} theme={data.wishlist.theme}>
|
||||
<ReservationButton
|
||||
itemId={item.id}
|
||||
isReserved={item.isReserved}
|
||||
|
||||
@@ -203,6 +203,7 @@ export const actions: Actions = {
|
||||
const title = formData.get('title');
|
||||
const description = formData.get('description');
|
||||
const endDate = formData.get('endDate');
|
||||
const theme = formData.get('theme');
|
||||
|
||||
const wishlist = await db.query.wishlists.findFirst({
|
||||
where: eq(wishlists.ownerToken, params.token)
|
||||
@@ -237,6 +238,10 @@ export const actions: Actions = {
|
||||
updates.endDate = endDateStr ? new Date(endDateStr) : null;
|
||||
}
|
||||
|
||||
if (theme !== null) {
|
||||
updates.theme = theme?.toString().trim() || 'none';
|
||||
}
|
||||
|
||||
await db.update(wishlists)
|
||||
.set(updates)
|
||||
.where(eq(wishlists.id, wishlist.id));
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<PageContainer maxWidth="4xl">
|
||||
<PageContainer maxWidth="4xl" theme={data.wishlist.theme} themeColor={data.wishlist.color}>
|
||||
<Navigation
|
||||
isAuthenticated={data.isAuthenticated}
|
||||
showDashboardLink={true}
|
||||
@@ -119,6 +119,7 @@
|
||||
onDescriptionUpdate={wishlistUpdates.updateDescription}
|
||||
onColorUpdate={wishlistUpdates.updateColor}
|
||||
onEndDateUpdate={wishlistUpdates.updateEndDate}
|
||||
onThemeUpdate={wishlistUpdates.updateTheme}
|
||||
/>
|
||||
|
||||
<ShareLinks
|
||||
@@ -168,6 +169,7 @@
|
||||
{rearranging}
|
||||
onStartEditing={startEditing}
|
||||
onReorder={handleReorder}
|
||||
theme={data.wishlist.theme}
|
||||
/>
|
||||
|
||||
<DangerZone bind:unlocked={rearranging} />
|
||||
|
||||
Reference in New Issue
Block a user