Compare commits
7 Commits
22f9f8f0c9
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdfcdcc15b | ||
|
|
b848477729 | ||
|
|
ed9da14fa5 | ||
| 23ff65f3e7 | |||
|
|
ac81b8175c | ||
|
|
19493b4cd3 | ||
|
|
b80ef2cfea |
@@ -77,6 +77,10 @@ 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"),
|
||||||
|
lastLogin: timestamp("last_login", { mode: 'string' }),
|
||||||
|
createdAt: timestamp("created_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp("updated_at", { mode: 'string' }).defaultNow().notNull(),
|
||||||
}, (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),
|
||||||
|
|||||||
@@ -103,6 +103,14 @@ const authConfig: SvelteKitAuthConfig = {
|
|||||||
signIn: '/signin'
|
signIn: '/signin'
|
||||||
},
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
async signIn({ user }) {
|
||||||
|
if (user?.id) {
|
||||||
|
await db.update(users)
|
||||||
|
.set({ lastLogin: new Date() })
|
||||||
|
.where(eq(users.id, user.id));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
async jwt({ token, user }) {
|
async jwt({ token, user }) {
|
||||||
if (user) {
|
if (user) {
|
||||||
token.id = user.id;
|
token.id = user.id;
|
||||||
|
|||||||
@@ -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} size="sm" />
|
||||||
<ThemePicker value={dashboardTheme} onValueChange={handleThemeChange} />
|
<ThemePicker value={dashboardTheme} onValueChange={handleThemeChange} />
|
||||||
<LanguageToggle />
|
<LanguageToggle />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
|
|||||||
@@ -5,10 +5,12 @@
|
|||||||
|
|
||||||
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));
|
||||||
@@ -18,6 +20,6 @@
|
|||||||
});
|
});
|
||||||
</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}
|
||||||
|
|||||||
@@ -16,17 +16,15 @@
|
|||||||
|
|
||||||
{#if pattern !== 'none'}
|
{#if pattern !== 'none'}
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-0 left-0 right-0 pointer-events-none overflow-hidden"
|
class="fixed bottom-0 left-0 right-0 pointer-events-none overflow-hidden z-0"
|
||||||
style="
|
style="
|
||||||
mask-image: url({patternPath});
|
mask-image: url({patternPath});
|
||||||
-webkit-mask-image: url({patternPath});
|
|
||||||
mask-size: cover;
|
mask-size: cover;
|
||||||
-webkit-mask-size: cover;
|
mask-repeat: no-repeat;
|
||||||
mask-repeat: repeat;
|
mask-position: left bottom;
|
||||||
-webkit-mask-repeat: repeat;
|
|
||||||
background-color: {color};
|
background-color: {color};
|
||||||
opacity: {opacity};
|
opacity: {opacity};
|
||||||
height: 200px;
|
height: 100vh;
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -16,18 +16,15 @@
|
|||||||
|
|
||||||
{#if pattern !== 'none'}
|
{#if pattern !== 'none'}
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-0 right-0 pointer-events-none overflow-hidden rounded-b-lg"
|
class="absolute bottom-0 top-0 right-0 pointer-events-none overflow-hidden rounded-b-lg"
|
||||||
style="
|
style="
|
||||||
mask-image: url({patternPath});
|
mask-image: url({patternPath});
|
||||||
-webkit-mask-image: url({patternPath});
|
|
||||||
mask-size: cover;
|
mask-size: cover;
|
||||||
-webkit-mask-size: cover;
|
mask-repeat: no-repeat;
|
||||||
mask-repeat: repeat;
|
mask-position: right bottom;
|
||||||
-webkit-mask-repeat: repeat;
|
|
||||||
background-color: {color};
|
background-color: {color};
|
||||||
opacity: {opacity};
|
opacity: {opacity};
|
||||||
width: 200px;
|
width: 100%;
|
||||||
height: 200px;
|
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -16,17 +16,16 @@
|
|||||||
|
|
||||||
{#if pattern !== 'none'}
|
{#if pattern !== 'none'}
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 left-0 right-0 pointer-events-none overflow-hidden"
|
class="fixed top-0 right-0 left-0 pointer-events-none z-0"
|
||||||
style="
|
style="
|
||||||
mask-image: url({patternPath});
|
mask-image: url({patternPath});
|
||||||
-webkit-mask-image: url({patternPath});
|
-webkit-mask-image: url({patternPath});
|
||||||
mask-size: cover;
|
mask-size: cover;
|
||||||
-webkit-mask-size: cover;
|
mask-repeat: no-repeat;
|
||||||
mask-repeat: repeat;
|
mask-position: right top;
|
||||||
-webkit-mask-repeat: repeat;
|
|
||||||
background-color: {color};
|
background-color: {color};
|
||||||
opacity: {opacity};
|
opacity: {opacity};
|
||||||
height: 200px;
|
height: 100vh;
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const iconSizeClasses = {
|
const iconSizeClasses = {
|
||||||
sm: 'w-3 h-3',
|
sm: 'w-4 h-4',
|
||||||
md: 'w-4 h-4',
|
md: 'w-4 h-4',
|
||||||
lg: 'w-5 h-5'
|
lg: 'w-5 h-5'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,12 +8,18 @@
|
|||||||
import ColorPicker from '$lib/components/ui/ColorPicker.svelte';
|
import ColorPicker from '$lib/components/ui/ColorPicker.svelte';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
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';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
|
wishlistColor?: string | null;
|
||||||
|
wishlistTheme?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { onSuccess }: Props = $props();
|
let { onSuccess, wishlistColor = null, wishlistTheme = null }: Props = $props();
|
||||||
|
|
||||||
|
const cardStyle = $derived(getCardStyle(wishlistColor, null));
|
||||||
|
|
||||||
const t = $derived(languageStore.t);
|
const t = $derived(languageStore.t);
|
||||||
|
|
||||||
@@ -53,11 +59,12 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card>
|
<Card style={cardStyle} class="relative overflow-hidden">
|
||||||
<CardHeader>
|
<ThemeCard themeName={wishlistTheme} color={wishlistColor} showPattern={false} />
|
||||||
|
<CardHeader class="relative z-10">
|
||||||
<CardTitle>{t.form.addNewWish}</CardTitle>
|
<CardTitle>{t.form.addNewWish}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent class="relative z-10">
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/addItem"
|
action="?/addItem"
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import type { Item } from '$lib/server/schema';
|
import type { Item } from '$lib/server/schema';
|
||||||
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';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: Item;
|
item: Item;
|
||||||
@@ -18,9 +20,13 @@
|
|||||||
currentPosition?: number;
|
currentPosition?: number;
|
||||||
totalItems?: number;
|
totalItems?: number;
|
||||||
onPositionChange?: (newPosition: number) => void;
|
onPositionChange?: (newPosition: number) => void;
|
||||||
|
wishlistColor?: string | null;
|
||||||
|
wishlistTheme?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { item, onSuccess, onCancel, onColorChange, currentPosition = 1, totalItems = 1, onPositionChange }: Props = $props();
|
let { item, onSuccess, onCancel, onColorChange, currentPosition = 1, totalItems = 1, onPositionChange, wishlistColor = null, wishlistTheme = null }: Props = $props();
|
||||||
|
|
||||||
|
const cardStyle = $derived(getCardStyle(wishlistColor, null));
|
||||||
|
|
||||||
const t = $derived(languageStore.t);
|
const t = $derived(languageStore.t);
|
||||||
|
|
||||||
@@ -60,11 +66,12 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card>
|
<Card style={cardStyle} class="relative overflow-hidden">
|
||||||
<CardHeader>
|
<ThemeCard themeName={wishlistTheme} color={wishlistColor} showPattern={false} />
|
||||||
|
<CardHeader class="relative z-10">
|
||||||
<CardTitle>{t.wishlist.editWish}</CardTitle>
|
<CardTitle>{t.wishlist.editWish}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent class="relative z-10">
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/updateItem"
|
action="?/updateItem"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
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([]),
|
||||||
@@ -25,6 +27,7 @@
|
|||||||
} = $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">
|
||||||
@@ -68,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 + "!"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -132,6 +132,7 @@
|
|||||||
<ColorPicker
|
<ColorPicker
|
||||||
bind:color={wishlistColor}
|
bind:color={wishlistColor}
|
||||||
onchange={() => onColorUpdate(wishlistColor)}
|
onchange={() => onColorUpdate(wishlistColor)}
|
||||||
|
size="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ 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'),
|
||||||
|
lastLogin: timestamp('last_login', { mode: 'date' }),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const accounts = pgTable(
|
export const accounts = pgTable(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { themeStore } from '$lib/stores/theme.svelte';
|
import { themeStore } from '$lib/stores/theme.svelte';
|
||||||
|
|
||||||
export type ThemePattern = 'waves' | 'geometric' | 'dots' | 'none';
|
export type ThemePattern = 'snow' | 'none';
|
||||||
|
|
||||||
export interface Theme {
|
export interface Theme {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -13,16 +13,8 @@ export const AVAILABLE_THEMES: Record<string, Theme> = {
|
|||||||
pattern: 'none'
|
pattern: 'none'
|
||||||
},
|
},
|
||||||
waves: {
|
waves: {
|
||||||
name: 'Waves',
|
name: 'Snow',
|
||||||
pattern: 'waves'
|
pattern: 'snow'
|
||||||
},
|
|
||||||
geometric: {
|
|
||||||
name: 'Geometric',
|
|
||||||
pattern: 'geometric'
|
|
||||||
},
|
|
||||||
dots: {
|
|
||||||
name: 'Dots',
|
|
||||||
pattern: 'dots'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const actions: Actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db.update(wishlists)
|
await db.update(wishlists)
|
||||||
.set({ isFavorite: !isFavorite })
|
.set({ isFavorite: !isFavorite, updatedAt: new Date() })
|
||||||
.where(eq(wishlists.id, wishlistId));
|
.where(eq(wishlists.id, wishlistId));
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -171,7 +171,23 @@ export const actions: Actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await db.update(users)
|
await db.update(users)
|
||||||
.set({ dashboardTheme: theme })
|
.set({ dashboardTheme: theme, updatedAt: new Date() })
|
||||||
|
.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, updatedAt: new Date() })
|
||||||
.where(eq(users.id, session.user.id));
|
.where(eq(users.id, session.user.id));
|
||||||
|
|
||||||
return { success: true };
|
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 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">
|
||||||
|
|||||||
@@ -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,12 +96,10 @@
|
|||||||
</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}
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -155,7 +155,7 @@
|
|||||||
|
|
||||||
{#if showAddForm}
|
{#if showAddForm}
|
||||||
<div bind:this={addFormElement}>
|
<div bind:this={addFormElement}>
|
||||||
<AddItemForm onSuccess={handleItemAdded} />
|
<AddItemForm onSuccess={handleItemAdded} wishlistColor={currentColor} wishlistTheme={currentTheme} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -169,6 +169,8 @@
|
|||||||
currentPosition={items.findIndex(item => item.id === editingItem.id) + 1}
|
currentPosition={items.findIndex(item => item.id === editingItem.id) + 1}
|
||||||
totalItems={items.length}
|
totalItems={items.length}
|
||||||
onPositionChange={handlePositionChange}
|
onPositionChange={handlePositionChange}
|
||||||
|
wishlistColor={currentColor}
|
||||||
|
wishlistTheme={currentTheme}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
2782
static/themes/snow/bgbottom.svg
Normal file
|
After Width: | Height: | Size: 130 KiB |
2783
static/themes/snow/bgtop.svg
Normal file
|
After Width: | Height: | Size: 130 KiB |
1695
static/themes/snow/item.svg
Normal file
|
After Width: | Height: | Size: 78 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 52 52"><path fill="#000000" d="M0 17.83V0h17.83a3 3 0 0 1-5.66 2H5.9A5 5 0 0 1 2 5.9v6.27a3 3 0 0 1-2 5.66zm0 18.34a3 3 0 0 1 2 5.66v6.27A5 5 0 0 1 5.9 52h6.27a3 3 0 0 1 5.66 0H0V36.17zM36.17 52a3 3 0 0 1 5.66 0h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 0 1 0-5.66V52H36.17zM0 31.93v-9.78a5 5 0 0 1 3.8.72l4.43-4.43a3 3 0 1 1 1.42 1.41L5.2 24.28a5 5 0 0 1 0 5.52l4.44 4.43a3 3 0 1 1-1.42 1.42L3.8 31.2a5 5 0 0 1-3.8.72zm52-14.1a3 3 0 0 1 0-5.66V5.9A5 5 0 0 1 48.1 2h-6.27a3 3 0 0 1-5.66-2H52v17.83zm0 14.1a4.97 4.97 0 0 1-1.72-.72l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1 0-5.52l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43c.53-.35 1.12-.6 1.72-.72v9.78zM22.15 0h9.78a5 5 0 0 1-.72 3.8l4.44 4.43a3 3 0 1 1-1.42 1.42L29.8 5.2a5 5 0 0 1-5.52 0l-4.43 4.44a3 3 0 1 1-1.41-1.42l4.43-4.43a5 5 0 0 1-.72-3.8zm0 52c.13-.6.37-1.19.72-1.72l-4.43-4.43a3 3 0 1 1 1.41-1.41l4.43 4.43a5 5 0 0 1 5.52 0l4.43-4.43a3 3 0 1 1 1.42 1.41l-4.44 4.43c.36.53.6 1.12.72 1.72h-9.78zm9.75-24a5 5 0 0 1-3.9 3.9v6.27a3 3 0 1 1-2 0V31.9a5 5 0 0 1-3.9-3.9h-6.27a3 3 0 1 1 0-2h6.27a5 5 0 0 1 3.9-3.9v-6.27a3 3 0 1 1 2 0v6.27a5 5 0 0 1 3.9 3.9h6.27a3 3 0 1 1 0 2H31.9z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |