Compare commits
4 Commits
22f9f8f0c9
...
23ff65f3e7
| Author | SHA1 | Date | |
|---|---|---|---|
| 23ff65f3e7 | |||
|
|
ac81b8175c | ||
|
|
19493b4cd3 | ||
|
|
b80ef2cfea |
@@ -77,6 +77,7 @@ export const user = pgTable("user", {
|
||||
password: text(),
|
||||
username: text(),
|
||||
dashboardTheme: text("dashboard_theme").default('none'),
|
||||
dashboardColor: text("dashboard_color"),
|
||||
}, (table) => [
|
||||
unique("user_email_unique").on(table.email),
|
||||
unique("user_username_unique").on(table.username),
|
||||
|
||||
@@ -7,9 +7,13 @@
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
isAuthenticated = false
|
||||
isAuthenticated = false,
|
||||
fallbackColor = null,
|
||||
fallbackTheme = null
|
||||
}: {
|
||||
isAuthenticated?: boolean;
|
||||
fallbackColor?: string | null;
|
||||
fallbackTheme?: string | null;
|
||||
} = $props();
|
||||
|
||||
const t = $derived(languageStore.t);
|
||||
@@ -114,6 +118,8 @@
|
||||
emptyActionLabel={t.dashboard.createLocalWishlist || "Create local wishlist"}
|
||||
emptyActionHref="/"
|
||||
showCreateButton={true}
|
||||
fallbackColor={fallbackColor}
|
||||
fallbackTheme={fallbackTheme}
|
||||
>
|
||||
{#snippet actions(wishlist, unlocked)}
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
itemCount,
|
||||
color = null,
|
||||
theme = null,
|
||||
fallbackColor = null,
|
||||
fallbackTheme = null,
|
||||
children
|
||||
}: {
|
||||
title: string;
|
||||
@@ -18,14 +20,18 @@
|
||||
itemCount: number;
|
||||
color?: string | null;
|
||||
theme?: string | null;
|
||||
fallbackColor?: string | null;
|
||||
fallbackTheme?: string | null;
|
||||
children?: Snippet;
|
||||
} = $props();
|
||||
|
||||
const cardStyle = $derived(getCardStyle(color));
|
||||
const finalColor = $derived(color || fallbackColor);
|
||||
const finalTheme = $derived(theme || fallbackTheme);
|
||||
const cardStyle = $derived(getCardStyle(color, fallbackColor));
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import EmptyState from '$lib/components/layout/EmptyState.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { flip } from 'svelte/animate';
|
||||
import { getCardStyle } from '$lib/utils/colors';
|
||||
import ThemeCard from '$lib/components/themes/ThemeCard.svelte';
|
||||
|
||||
let {
|
||||
title,
|
||||
@@ -13,6 +15,8 @@
|
||||
emptyDescription,
|
||||
emptyActionLabel,
|
||||
emptyActionHref,
|
||||
fallbackColor = null,
|
||||
fallbackTheme = null,
|
||||
headerAction,
|
||||
searchBar,
|
||||
children
|
||||
@@ -24,11 +28,15 @@
|
||||
emptyDescription?: string;
|
||||
emptyActionLabel?: string;
|
||||
emptyActionHref?: string;
|
||||
fallbackColor?: string | null;
|
||||
fallbackTheme?: string | null;
|
||||
headerAction?: Snippet;
|
||||
searchBar?: Snippet;
|
||||
children: Snippet<[any]>;
|
||||
} = $props();
|
||||
|
||||
const cardStyle = $derived(getCardStyle(fallbackColor, null));
|
||||
|
||||
let scrollContainer: HTMLElement | null = null;
|
||||
|
||||
function handleWheel(event: WheelEvent) {
|
||||
@@ -44,8 +52,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Card style={cardStyle} class="relative overflow-hidden">
|
||||
<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-1 min-w-0">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
@@ -63,7 +72,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent class="relative z-10">
|
||||
{#if items && items.length > 0}
|
||||
<div
|
||||
bind:this={scrollContainer}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
emptyActionHref,
|
||||
showCreateButton = false,
|
||||
hideIfEmpty = false,
|
||||
fallbackColor = null,
|
||||
fallbackTheme = null,
|
||||
actions
|
||||
}: {
|
||||
title: string;
|
||||
@@ -32,6 +34,8 @@
|
||||
emptyActionHref?: string;
|
||||
showCreateButton?: boolean;
|
||||
hideIfEmpty?: boolean;
|
||||
fallbackColor?: string | null;
|
||||
fallbackTheme?: string | null;
|
||||
actions: Snippet<[WishlistItem, boolean]>; // item, unlocked
|
||||
} = $props();
|
||||
|
||||
@@ -126,6 +130,8 @@
|
||||
{emptyDescription}
|
||||
{emptyActionLabel}
|
||||
{emptyActionHref}
|
||||
{fallbackColor}
|
||||
{fallbackTheme}
|
||||
>
|
||||
{#snippet headerAction()}
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
@@ -150,6 +156,8 @@
|
||||
itemCount={wishlist.items?.length || 0}
|
||||
color={wishlist.color}
|
||||
theme={wishlist.theme}
|
||||
fallbackColor={fallbackColor}
|
||||
fallbackTheme={fallbackTheme}
|
||||
>
|
||||
{@render actions(item, unlocked)}
|
||||
</WishlistCard>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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 ColorPicker from '$lib/components/ui/ColorPicker.svelte';
|
||||
import { signOut } from '@auth/sveltekit/client';
|
||||
import { languageStore } from '$lib/stores/language.svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
@@ -11,25 +12,27 @@
|
||||
userName,
|
||||
userEmail,
|
||||
dashboardTheme = 'none',
|
||||
dashboardColor = null,
|
||||
isAuthenticated = false,
|
||||
onThemeUpdate
|
||||
onThemeUpdate,
|
||||
onColorUpdate
|
||||
}: {
|
||||
userName?: string | null;
|
||||
userEmail?: string | null;
|
||||
dashboardTheme?: string;
|
||||
dashboardColor?: string | null;
|
||||
isAuthenticated?: boolean;
|
||||
onThemeUpdate?: (theme: string | null) => void;
|
||||
onColorUpdate?: (color: string | null) => void;
|
||||
} = $props();
|
||||
|
||||
const t = $derived(languageStore.t);
|
||||
|
||||
async function handleThemeChange(theme: string) {
|
||||
// Update theme immediately for instant visual feedback
|
||||
if (onThemeUpdate) {
|
||||
onThemeUpdate(theme);
|
||||
}
|
||||
|
||||
// Only submit to database for authenticated users
|
||||
if (isAuthenticated) {
|
||||
const formData = new FormData();
|
||||
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>
|
||||
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
@@ -52,6 +79,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-1 sm:gap-2 flex-shrink-0">
|
||||
<ColorPicker bind:color={localColor} onchange={handleColorChange} />
|
||||
<ThemePicker value={dashboardTheme} onValueChange={handleThemeChange} />
|
||||
<LanguageToggle />
|
||||
<ThemeToggle />
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
|
||||
let {
|
||||
themeName,
|
||||
color
|
||||
color,
|
||||
showPattern = true
|
||||
}: {
|
||||
themeName?: string;
|
||||
color?: string;
|
||||
themeName?: string | null;
|
||||
color?: string | null;
|
||||
showPattern?: boolean;
|
||||
} = $props();
|
||||
|
||||
const theme = $derived(getTheme(themeName));
|
||||
@@ -18,6 +20,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if theme.pattern !== 'none'}
|
||||
{#if showPattern && theme.pattern !== 'none'}
|
||||
<CardPattern pattern={theme.pattern} color={patternColor} opacity={PATTERN_OPACITY} />
|
||||
{/if}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import { enhance } from "$app/forms";
|
||||
import { flip } from "svelte/animate";
|
||||
import { languageStore } from '$lib/stores/language.svelte';
|
||||
import ThemeCard from "$lib/components/themes/ThemeCard.svelte";
|
||||
import { getCardStyle } from "$lib/utils/colors";
|
||||
|
||||
let {
|
||||
items = $bindable([]),
|
||||
@@ -25,6 +27,7 @@
|
||||
} = $props();
|
||||
|
||||
const t = $derived(languageStore.t);
|
||||
const cardStyle = $derived(getCardStyle(wishlistColor));
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
@@ -68,8 +71,9 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Card>
|
||||
<CardContent class="p-12">
|
||||
<Card style={cardStyle} class="relative overflow-hidden">
|
||||
<ThemeCard themeName={theme} color={wishlistColor} />
|
||||
<CardContent class="p-12 relative z-10">
|
||||
<EmptyState
|
||||
message={t.wishlist.noWishes + ". " + t.wishlist.addFirstWish + "!"}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,8 @@ export const users = pgTable('user', {
|
||||
image: text('image'),
|
||||
password: text('password'),
|
||||
username: text('username').unique(),
|
||||
dashboardTheme: text('dashboard_theme').default('none')
|
||||
dashboardTheme: text('dashboard_theme').default('none'),
|
||||
dashboardColor: text('dashboard_color')
|
||||
});
|
||||
|
||||
export const accounts = pgTable(
|
||||
|
||||
@@ -174,6 +174,22 @@ export const actions: Actions = {
|
||||
.set({ dashboardTheme: theme })
|
||||
.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 };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 currentColor = $state(getInitialColor());
|
||||
|
||||
// Save to localStorage when theme changes for anonymous users
|
||||
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);
|
||||
|
||||
// Only owned wishlists for "My Wishlists"
|
||||
@@ -58,17 +85,23 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<PageContainer theme={currentTheme} themeColor={null}>
|
||||
<PageContainer theme={currentTheme} themeColor={currentColor}>
|
||||
<DashboardHeader
|
||||
userName={data.user?.name}
|
||||
userEmail={data.user?.email}
|
||||
dashboardTheme={currentTheme}
|
||||
dashboardColor={currentColor}
|
||||
isAuthenticated={data.isAuthenticated}
|
||||
onThemeUpdate={handleThemeUpdate}
|
||||
onColorUpdate={handleColorUpdate}
|
||||
/>
|
||||
|
||||
<!-- Local Wishlists Section (for anonymous and authenticated users) -->
|
||||
<LocalWishlistsSection isAuthenticated={data.isAuthenticated} />
|
||||
<LocalWishlistsSection
|
||||
isAuthenticated={data.isAuthenticated}
|
||||
fallbackColor={currentColor}
|
||||
fallbackTheme={currentTheme}
|
||||
/>
|
||||
|
||||
{#if data.isAuthenticated}
|
||||
<!-- My Wishlists Section -->
|
||||
@@ -80,6 +113,8 @@
|
||||
emptyActionLabel={t.dashboard.emptyWishlistsAction}
|
||||
emptyActionHref="/"
|
||||
showCreateButton={true}
|
||||
fallbackColor={currentColor}
|
||||
fallbackTheme={currentTheme}
|
||||
>
|
||||
{#snippet actions(wishlist, unlocked)}
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
@@ -135,6 +170,8 @@
|
||||
emptyMessage={t.dashboard.emptyClaimedWishlists}
|
||||
emptyDescription={t.dashboard.emptyClaimedWishlistsDescription}
|
||||
hideIfEmpty={true}
|
||||
fallbackColor={currentColor}
|
||||
fallbackTheme={currentTheme}
|
||||
>
|
||||
{#snippet actions(wishlist, unlocked)}
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
@@ -189,6 +226,8 @@
|
||||
items={savedWishlists()}
|
||||
emptyMessage={t.dashboard.emptySavedWishlists}
|
||||
emptyDescription={t.dashboard.emptySavedWishlistsDescription}
|
||||
fallbackColor={currentColor}
|
||||
fallbackTheme={currentTheme}
|
||||
>
|
||||
{#snippet actions(saved, unlocked)}
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
CardTitle,
|
||||
} from "$lib/components/ui/card";
|
||||
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 WishlistItem from "$lib/components/wishlist/WishlistItem.svelte";
|
||||
import ReservationButton from "$lib/components/wishlist/ReservationButton.svelte";
|
||||
@@ -19,6 +17,7 @@
|
||||
import { getCardStyle } from "$lib/utils/colors";
|
||||
import { languageStore } from '$lib/stores/language.svelte';
|
||||
import SearchBar from "$lib/components/ui/SearchBar.svelte";
|
||||
import ThemeCard from "$lib/components/themes/ThemeCard.svelte";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
@@ -41,7 +40,6 @@
|
||||
showDashboardLink={true}
|
||||
/>
|
||||
|
||||
<!-- Header -->
|
||||
<Card style={headerCardStyle}>
|
||||
<CardContent class="pt-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
@@ -55,7 +53,6 @@
|
||||
</div>
|
||||
{#if data.isAuthenticated}
|
||||
{#if data.isClaimed}
|
||||
<!-- User has claimed this wishlist - show claimed status -->
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -64,7 +61,6 @@
|
||||
{t.wishlist.youClaimedThis}
|
||||
</Button>
|
||||
{:else if data.isSaved}
|
||||
<!-- User has saved but not claimed - show unsave button -->
|
||||
<form method="POST" action="?/unsaveWishlist" use:enhance>
|
||||
<input
|
||||
type="hidden"
|
||||
@@ -76,7 +72,6 @@
|
||||
</Button>
|
||||
</form>
|
||||
{:else}
|
||||
<!-- Not saved - show save button -->
|
||||
<form method="POST" action="?/saveWishlist" use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
await update({ reset: false });
|
||||
@@ -101,12 +96,10 @@
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Search Bar -->
|
||||
{#if data.wishlist.items && data.wishlist.items.length > 0}
|
||||
<SearchBar bind:value={searchQuery} />
|
||||
{/if}
|
||||
|
||||
<!-- Items List -->
|
||||
<div class="space-y-4">
|
||||
{#if filteredItems.length > 0}
|
||||
{#each filteredItems as item}
|
||||
@@ -121,16 +114,18 @@
|
||||
</WishlistItem>
|
||||
{/each}
|
||||
{:else if data.wishlist.items && data.wishlist.items.length > 0}
|
||||
<Card>
|
||||
<CardContent class="p-12">
|
||||
<Card style={headerCardStyle} class="relative overflow-hidden">
|
||||
<ThemeCard themeName={data.wishlist.theme} color={data.wishlist.color} />
|
||||
<CardContent class="p-12 relative z-10">
|
||||
<EmptyState
|
||||
message="No wishes match your search."
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{:else}
|
||||
<Card>
|
||||
<CardContent class="p-12">
|
||||
<Card style={headerCardStyle} class="relative overflow-hidden">
|
||||
<ThemeCard themeName={data.wishlist.theme} color={data.wishlist.color} />
|
||||
<CardContent class="p-12 relative z-10">
|
||||
<EmptyState
|
||||
message={t.wishlist.emptyWishes}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user