Compare commits

15 Commits

Author SHA1 Message Date
Rasmus Q
7453c356bb update: wishlist docker compose file 2025-12-19 23:02:34 +00:00
rasmusq
a5e47cdc1f update: readme with images 2025-12-20 00:02:10 +01:00
rasmusq
b3f123388f update: more visual consistency 2025-12-19 23:48:23 +01:00
rasmusq
8f574380ce update: theme switcher button use new generalized button 2025-12-19 23:35:43 +01:00
rasmusq
cb4539a982 update: generalize button components 2025-12-19 23:23:46 +01:00
rasmusq
466704a23a fix: dropdown theme colors 2025-12-19 23:13:03 +01:00
rasmusq
b381a6d669 update: reduce theme pattern prescence 2025-12-19 22:53:41 +01:00
2b12896374 Merge pull request 'feature/database-timestamps' (#7) from feature/database-timestamps into master
Reviewed-on: #7
2025-12-19 21:34:52 +00:00
rasmusq
bdfcdcc15b add: snow theme 2025-12-19 22:34:23 +01:00
rasmusq
b848477729 update: consistent round button sizing 2025-12-19 21:01:17 +01:00
rasmusq
ed9da14fa5 add: create, update and login dates in database 2025-12-19 20:50:06 +01:00
23ff65f3e7 Merge pull request 'feature/userdefined-dashboard-color' (#6) from feature/userdefined-dashboard-color into master
Reviewed-on: #6
2025-12-19 19:22:35 +00:00
rasmusq
ac81b8175c fix: missing color on empty lists in edit and wishlist pages 2025-12-19 20:20:11 +01:00
rasmusq
19493b4cd3 remove: svg from dashboard lists 2025-12-19 20:09:49 +01:00
rasmusq
b80ef2cfea add: color selection on dashboard 2025-12-19 19:33:43 +01:00
45 changed files with 7778 additions and 232 deletions

View File

@@ -2,6 +2,9 @@
A wishlist application built with SvelteKit, Drizzle ORM, and PostgreSQL. A wishlist application built with SvelteKit, Drizzle ORM, and PostgreSQL.
![Dashboard](readme-assets/dashboard.png)
![Creating a wishlist](readme-assets/create.png)
## Prerequisites ## Prerequisites
- [Bun](https://bun.sh/) - [Bun](https://bun.sh/)

View File

@@ -1,41 +1,61 @@
services: services:
db: database:
image: postgres:16-alpine image: postgres:16-alpine
container_name: wishlist-db container_name: wishlist-postgres
restart: unless-stopped
environment: environment:
POSTGRES_USER: wishlistuser POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: wishlistpassword POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: wishlist POSTGRES_DB: ${POSTGRES_DB}
ports:
- "5432:5432"
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - /mnt/HC_Volume_102830676/wishlist:/var/lib/postgresql/data
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U wishlistuser -d wishlist"] test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
networks:
- wishlist-net
app: app:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: wishlist-app container_name: wishlist-app
restart: unless-stopped
environment: environment:
DATABASE_URL: postgresql://wishlistuser:wishlistpassword@db:5432/wishlist DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@database:5432/${POSTGRES_DB}
NODE_ENV: production NODE_ENV: production
PORT: 3000 PORT: 3000
AUTH_SECRET: ${AUTH_SECRET:-change-me-in-production} AUTH_SECRET: ${AUTH_SECRET}
AUTH_URL: ${AUTH_URL:-http://localhost:3000} AUTH_URL: ${AUTH_URL}
AUTH_TRUST_HOST: true AUTH_TRUST_HOST: "true"
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
ports: AUTHENTIK_CLIENT_ID: ${AUTHENTIK_CLIENT_ID:-}
- "3000:3000" AUTHENTIK_CLIENT_SECRET: ${AUTHENTIK_CLIENT_SECRET:-}
AUTHENTIK_ISSUER: ${AUTHENTIK_ISSUER:-}
depends_on: depends_on:
db: database:
condition: service_healthy condition: service_healthy
restart: unless-stopped networks:
- wishlist-net
- traefik-net
labels:
- traefik.enable=true
- traefik.docker.network=traefik-net
# HTTPS router
- traefik.http.routers.wishlist.rule=Host(`wish.rasmusq.com`)
- traefik.http.routers.wishlist.entrypoints=websecure
- traefik.http.routers.wishlist.tls.certresolver=letsencrypt
# Forward headers for Auth.js
- traefik.http.routers.wishlist.middlewares=wishlist-headers
- traefik.http.middlewares.wishlist-headers.headers.customRequestHeaders.X-Forwarded-Proto=https
- traefik.http.middlewares.wishlist-headers.headers.customRequestHeaders.X-Forwarded-Host=wish.rasmusq.com
- traefik.http.services.wishlist.loadbalancer.server.port=3000
volumes: networks:
postgres_data: wishlist-net:
name: wishlist-net
traefik-net:
external: true

View File

@@ -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),

BIN
readme-assets/create.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
readme-assets/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 KiB

View File

@@ -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;

View File

@@ -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">

View File

@@ -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">

View File

@@ -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}

View File

@@ -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>

View File

@@ -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,8 +79,9 @@
{/if} {/if}
</div> </div>
<div class="flex items-center gap-1 sm:gap-2 flex-shrink-0"> <div class="flex items-center gap-1 sm:gap-2 flex-shrink-0">
<ThemePicker value={dashboardTheme} onValueChange={handleThemeChange} /> <ColorPicker bind:color={localColor} onchange={handleColorChange} size="sm" />
<LanguageToggle /> <ThemePicker value={dashboardTheme} onValueChange={handleThemeChange} color={localColor} />
<LanguageToggle color={localColor} />
<ThemeToggle /> <ThemeToggle />
{#if isAuthenticated} {#if isAuthenticated}
<Button variant="outline" onclick={() => signOut({ callbackUrl: '/' })}>{t.auth.signOut}</Button> <Button variant="outline" onclick={() => signOut({ callbackUrl: '/' })}>{t.auth.signOut}</Button>

View File

@@ -7,10 +7,12 @@
let { let {
isAuthenticated = false, isAuthenticated = false,
showDashboardLink = false showDashboardLink = false,
color = null
}: { }: {
isAuthenticated?: boolean; isAuthenticated?: boolean;
showDashboardLink?: boolean; showDashboardLink?: boolean;
color?: string | null;
} = $props(); } = $props();
const t = $derived(languageStore.t); const t = $derived(languageStore.t);
@@ -28,7 +30,7 @@
</Button> </Button>
{/if} {/if}
<div class="ml-auto flex items-center gap-1 sm:gap-2"> <div class="ml-auto flex items-center gap-1 sm:gap-2">
<LanguageToggle /> <LanguageToggle {color} />
<ThemeToggle /> <ThemeToggle size="sm" {color} />
</div> </div>
</nav> </nav>

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -16,17 +16,15 @@
{#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});
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}

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { X, Pencil } from 'lucide-svelte'; import { X, Pencil } from 'lucide-svelte';
import IconButton from './IconButton.svelte';
let { let {
color = $bindable(null), color = $bindable(null),
@@ -18,7 +19,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'
}; };
@@ -39,17 +40,18 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{#if color} {#if color}
<button <IconButton
type="button"
onclick={clearColor} onclick={clearColor}
class="{buttonSize} flex items-center justify-center rounded-full border border-input hover:bg-accent transition-colors" {color}
{size}
aria-label="Clear color" aria-label="Clear color"
rounded="md"
> >
<X class={iconSize} /> <X class={iconSize} />
</button> </IconButton>
{/if} {/if}
<label <label
class="{buttonSize} flex items-center justify-center rounded-full border border-input hover:opacity-90 transition-opacity cursor-pointer relative overflow-hidden" class="{buttonSize} flex items-center justify-center rounded-md border border-input hover:opacity-90 transition-opacity cursor-pointer relative overflow-hidden"
style={color ? `background-color: ${color};` : ''} style={color ? `background-color: ${color};` : ''}
> >
<Pencil class="{iconSize} relative z-10 pointer-events-none" style={color ? 'color: white; filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));' : ''} /> <Pencil class="{iconSize} relative z-10 pointer-events-none" style={color ? 'color: white; filter: drop-shadow(0 0 2px rgba(0,0,0,0.5));' : ''} />

View File

@@ -0,0 +1,133 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { scale } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import type { Snippet } from 'svelte';
import IconButton from './IconButton.svelte';
let {
items,
selectedValue,
onSelect,
color,
showCheckmark = true,
icon,
ariaLabel
}: {
items: Array<{ value: string; label: string }>;
selectedValue: string;
onSelect: (value: string) => void;
color?: string | null;
showCheckmark?: boolean;
icon: Snippet;
ariaLabel: string;
} = $props();
let showMenu = $state(false);
const menuClasses = $derived(
color
? 'absolute left-0 sm:right-0 sm:left-auto mt-2 w-40 rounded-md border shadow-lg z-50 backdrop-blur-md'
: 'absolute left-0 sm:right-0 sm:left-auto mt-2 w-40 rounded-md border shadow-lg z-50 backdrop-blur-md border-slate-200 dark:border-slate-800 bg-white/90 dark:bg-slate-950/90'
);
const menuStyle = $derived(
color
? `border-color: ${color}; background-color: ${color}20; backdrop-filter: blur(12px);`
: ''
);
function getItemStyle(itemValue: string): string {
if (!color) return '';
return selectedValue === itemValue ? `background-color: ${color}20;` : '';
}
function toggleMenu() {
showMenu = !showMenu;
}
function handleSelect(value: string) {
onSelect(value);
showMenu = false;
}
function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLElement;
if (!target.closest('.dropdown-menu')) {
showMenu = false;
}
}
function handleMouseEnter(e: MouseEvent) {
if (color) {
(e.currentTarget as HTMLElement).style.backgroundColor = `${color}15`;
}
}
function handleMouseLeave(e: MouseEvent, itemValue: string) {
if (color) {
(e.currentTarget as HTMLElement).style.backgroundColor =
selectedValue === itemValue ? `${color}20` : 'transparent';
}
}
$effect(() => {
if (showMenu) {
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}
});
</script>
<div class="relative dropdown-menu">
<IconButton
size="sm"
rounded="md"
onclick={toggleMenu}
aria-label={ariaLabel}
class={color ? 'hover-themed' : ''}
style={color ? `--hover-bg: ${color}20;` : ''}
>
{@render icon()}
</IconButton>
{#if showMenu}
<div
class={menuClasses}
style={menuStyle}
transition:scale={{ duration: 150, start: 0.95, opacity: 0, easing: cubicOut }}
>
<div class="py-1">
{#each items as item}
<button
type="button"
class="w-full text-left px-4 py-2 text-sm transition-colors"
class:hover:bg-slate-100={!color}
class:dark:hover:bg-slate-900={!color}
class:font-bold={selectedValue === item.value}
class:bg-slate-100={selectedValue === item.value && !color}
class:dark:bg-slate-900={selectedValue === item.value && !color}
class:flex={showCheckmark}
class:items-center={showCheckmark}
class:justify-between={showCheckmark}
style={getItemStyle(item.value)}
onmouseenter={handleMouseEnter}
onmouseleave={(e) => handleMouseLeave(e, item.value)}
onclick={() => handleSelect(item.value)}
>
<span>{item.label}</span>
{#if showCheckmark && selectedValue === item.value}
<span class="ml-2"></span>
{/if}
</button>
{/each}
</div>
</div>
{/if}
</div>
<style>
:global(.hover-themed:hover) {
background-color: var(--hover-bg) !important;
}
</style>

View File

@@ -0,0 +1,52 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { HTMLButtonAttributes } from 'svelte/elements';
interface Props extends HTMLButtonAttributes {
color?: string | null;
rounded?: 'full' | 'md' | 'lg';
size?: 'sm' | 'md' | 'lg';
children: Snippet;
}
let {
color = null,
rounded = 'full',
size = 'md',
class: className = '',
children,
...restProps
}: Props = $props();
const sizeClasses = {
sm: 'w-8 h-8',
md: 'w-10 h-10',
lg: 'w-12 h-12'
};
const roundedClasses = {
full: 'rounded-full',
md: 'rounded-md',
lg: 'rounded-lg'
};
const baseClasses = 'flex items-center justify-center border border-input transition-colors backdrop-blur';
const sizeClass = sizeClasses[size];
const roundedClass = roundedClasses[rounded];
</script>
<button
type="button"
class="{baseClasses} {sizeClass} {roundedClass} {className} backdrop-blur-sm"
class:hover:bg-accent={!color}
style={color ? `--hover-bg: ${color}20;` : ''}
{...restProps}
>
{@render children()}
</button>
<style>
button[style*='--hover-bg']:hover {
background-color: var(--hover-bg);
}
</style>

View File

@@ -1,58 +1,32 @@
<script lang="ts"> <script lang="ts">
import { languageStore } from '$lib/stores/language.svelte'; import { languageStore } from '$lib/stores/language.svelte';
import { languages } from '$lib/i18n/translations'; import { languages } from '$lib/i18n/translations';
import { Button } from '$lib/components/ui/button'; import Dropdown from '$lib/components/ui/Dropdown.svelte';
import { Languages } from 'lucide-svelte'; import { Languages } from 'lucide-svelte';
let showMenu = $state(false); let { color }: { color?: string | null } = $props();
function toggleMenu() { const languageItems = $derived(
showMenu = !showMenu; languages.map((lang) => ({
} value: lang.code,
label: lang.name
}))
);
function setLanguage(code: 'en' | 'da') { function setLanguage(code: string) {
languageStore.setLanguage(code); languageStore.setLanguage(code as 'en' | 'da');
showMenu = false;
} }
function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLElement;
if (!target.closest('.language-toggle-menu')) {
showMenu = false;
}
}
$effect(() => {
if (showMenu) {
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}
});
</script> </script>
<div class="relative language-toggle-menu"> <Dropdown
<Button variant="outline" size="icon" onclick={toggleMenu} aria-label="Toggle language"> items={languageItems}
selectedValue={languageStore.current}
onSelect={setLanguage}
{color}
showCheckmark={false}
ariaLabel="Toggle language"
>
{#snippet icon()}
<Languages class="h-[1.2rem] w-[1.2rem]" /> <Languages class="h-[1.2rem] w-[1.2rem]" />
</Button> {/snippet}
</Dropdown>
{#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 languages as lang}
<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"
class:font-bold={languageStore.current === lang.code}
class:bg-slate-100={languageStore.current === lang.code}
class:dark:bg-slate-900={languageStore.current === lang.code}
onclick={() => setLanguage(lang.code)}
>
{lang.name}
</button>
{/each}
</div>
</div>
{/if}
</div>

View File

@@ -1,68 +1,35 @@
<script lang="ts"> <script lang="ts">
import { Button } from '$lib/components/ui/button'; import Dropdown from '$lib/components/ui/Dropdown.svelte';
import { Palette } from 'lucide-svelte'; import { Palette } from 'lucide-svelte';
import { AVAILABLE_THEMES } from '$lib/utils/themes'; import { AVAILABLE_THEMES } from '$lib/utils/themes';
let { let {
value = 'none', value = 'none',
onValueChange onValueChange,
color
}: { }: {
value?: string; value?: string;
onValueChange: (theme: string) => void; onValueChange: (theme: string) => void;
color?: string | null;
} = $props(); } = $props();
let showMenu = $state(false); const themeItems = $derived(
Object.entries(AVAILABLE_THEMES).map(([key, theme]) => ({
function toggleMenu() { value: key,
showMenu = !showMenu; label: theme.name
} }))
);
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> </script>
<div class="relative theme-picker-menu"> <Dropdown
<Button variant="outline" size="icon" onclick={toggleMenu} aria-label="Select theme pattern"> items={themeItems}
selectedValue={value}
onSelect={onValueChange}
{color}
showCheckmark={true}
ariaLabel="Select theme pattern"
>
{#snippet icon()}
<Palette class="h-[1.2rem] w-[1.2rem]" /> <Palette class="h-[1.2rem] w-[1.2rem]" />
</Button> {/snippet}
</Dropdown>
{#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>

View File

@@ -1,14 +1,22 @@
<script lang="ts"> <script lang="ts">
import { themeStore } from '$lib/stores/theme.svelte'; import { themeStore } from '$lib/stores/theme.svelte';
import { Button } from '$lib/components/ui/button';
import { Sun, Moon, Monitor } from 'lucide-svelte'; import { Sun, Moon, Monitor } from 'lucide-svelte';
import IconButton from '../IconButton.svelte';
let {
color = $bindable(null),
size = 'sm',
}: {
color: string | null;
size?: 'sm' | 'md' | 'lg';
} = $props();
function toggle() { function toggle() {
themeStore.toggle(); themeStore.toggle();
} }
</script> </script>
<Button onclick={toggle} variant="ghost" size="icon" class="rounded-full"> <IconButton onclick={toggle} {size} {color} rounded="md">
{#if themeStore.current === 'light'} {#if themeStore.current === 'light'}
<Sun size={20} /> <Sun size={20} />
<span class="sr-only">Light mode (click for dark)</span> <span class="sr-only">Light mode (click for dark)</span>
@@ -19,4 +27,4 @@
<Monitor size={20} /> <Monitor size={20} />
<span class="sr-only">System mode (click for light)</span> <span class="sr-only">System mode (click for light)</span>
{/if} {/if}
</Button> </IconButton>

View File

@@ -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"

View File

@@ -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"

View File

@@ -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} showPattern={false} />
<CardContent class="p-12 relative z-10">
<EmptyState <EmptyState
message={t.wishlist.noWishes + ". " + t.wishlist.addFirstWish + "!"} message={t.wishlist.noWishes + ". " + t.wishlist.addFirstWish + "!"}
/> />

View File

@@ -6,6 +6,7 @@
import { Pencil, Check, X } from "lucide-svelte"; import { Pencil, Check, X } from "lucide-svelte";
import ColorPicker from "$lib/components/ui/ColorPicker.svelte"; import ColorPicker from "$lib/components/ui/ColorPicker.svelte";
import ThemePicker from "$lib/components/ui/theme-picker.svelte"; import ThemePicker from "$lib/components/ui/theme-picker.svelte";
import IconButton from "$lib/components/ui/IconButton.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'; import { getCardStyle } from '$lib/utils/colors';
@@ -80,7 +81,6 @@
} }
</script> </script>
<!-- Title Header -->
<div class="flex items-center justify-between gap-4 mb-6"> <div class="flex items-center justify-between gap-4 mb-6">
<div class="flex items-center gap-2 flex-1 min-w-0"> <div class="flex items-center gap-2 flex-1 min-w-0">
{#if editingTitle} {#if editingTitle}
@@ -100,8 +100,7 @@
{:else} {:else}
<h1 class="text-3xl font-bold leading-[2.25rem]">{wishlistTitle}</h1> <h1 class="text-3xl font-bold leading-[2.25rem]">{wishlistTitle}</h1>
{/if} {/if}
<button <IconButton
type="button"
onclick={() => { onclick={() => {
if (editingTitle) { if (editingTitle) {
saveTitle(); saveTitle();
@@ -109,7 +108,9 @@
editingTitle = true; editingTitle = true;
} }
}} }}
class="shrink-0 w-8 h-8 flex items-center justify-center rounded-full border border-input hover:bg-accent transition-colors" color={wishlistColor}
size="sm"
class="shrink-0"
aria-label={editingTitle ? "Save title" : "Edit title"} aria-label={editingTitle ? "Save title" : "Edit title"}
> >
{#if editingTitle} {#if editingTitle}
@@ -117,7 +118,7 @@
{:else} {:else}
<Pencil class="w-4 h-4" /> <Pencil class="w-4 h-4" />
{/if} {/if}
</button> </IconButton>
</div> </div>
<div class="flex items-center gap-2 shrink-0"> <div class="flex items-center gap-2 shrink-0">
<ThemePicker <ThemePicker
@@ -128,23 +129,22 @@
// Force reactivity by updating the wishlist object // Force reactivity by updating the wishlist object
wishlist.theme = theme; wishlist.theme = theme;
}} }}
color={wishlistColor}
/> />
<ColorPicker <ColorPicker
bind:color={wishlistColor} bind:color={wishlistColor}
onchange={() => onColorUpdate(wishlistColor)} onchange={() => onColorUpdate(wishlistColor)}
size="sm"
/> />
</div> </div>
</div> </div>
<!-- Settings Card -->
<Card style={cardStyle}> <Card style={cardStyle}>
<CardContent class="pt-6 space-y-4"> <CardContent class="pt-6 space-y-4">
<!-- Description -->
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center justify-between gap-2"> <div class="flex items-center justify-between gap-2">
<Label for="wishlist-description">{t.form.descriptionOptional}</Label> <Label for="wishlist-description">{t.form.descriptionOptional}</Label>
<button <IconButton
type="button"
onclick={() => { onclick={() => {
if (editingDescription) { if (editingDescription) {
saveDescription(); saveDescription();
@@ -152,7 +152,9 @@
editingDescription = true; editingDescription = true;
} }
}} }}
class="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-full border border-input hover:bg-accent transition-colors" color={wishlistColor}
size="sm"
class="flex-shrink-0"
aria-label={editingDescription ? "Save description" : "Edit description"} aria-label={editingDescription ? "Save description" : "Edit description"}
> >
{#if editingDescription} {#if editingDescription}
@@ -160,7 +162,7 @@
{:else} {:else}
<Pencil class="w-4 h-4" /> <Pencil class="w-4 h-4" />
{/if} {/if}
</button> </IconButton>
</div> </div>
{#if editingDescription} {#if editingDescription}
<Textarea <Textarea
@@ -183,19 +185,19 @@
{/if} {/if}
</div> </div>
<!-- End Date -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-4"> <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-4">
<Label for="wishlist-end-date">{t.form.endDateOptional}</Label> <Label for="wishlist-end-date">{t.form.endDateOptional}</Label>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{#if wishlistEndDate} {#if wishlistEndDate}
<button <IconButton
type="button"
onclick={clearEndDate} onclick={clearEndDate}
class="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-full border border-input hover:bg-accent transition-colors" color={wishlistColor}
size="sm"
class="flex-shrink-0"
aria-label="Clear end date" aria-label="Clear end date"
> >
<X class="w-4 h-4" /> <X class="w-4 h-4" />
</button> </IconButton>
{/if} {/if}
<Input <Input
id="wishlist-end-date" id="wishlist-end-date"

View File

@@ -57,7 +57,7 @@
</script> </script>
<Card style={cardStyle} class="relative overflow-hidden"> <Card style={cardStyle} class="relative overflow-hidden">
<ThemeCard themeName={theme} color={item.color} /> <ThemeCard themeName={theme} color={item.color} showPattern={false} />
<CardContent class="p-6 relative z-10"> <CardContent class="p-6 relative z-10">
<div class="flex gap-4"> <div class="flex gap-4">
{#if showDragHandle} {#if showDragHandle}

View File

@@ -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(

View File

@@ -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'
} }
}; };

View File

@@ -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 };

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">

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();
@@ -39,9 +38,9 @@
<Navigation <Navigation
isAuthenticated={data.isAuthenticated} isAuthenticated={data.isAuthenticated}
showDashboardLink={true} showDashboardLink={true}
color={data.wishlist.color}
/> />
<!-- 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 +54,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 +62,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 +73,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 +97,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 +115,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} showPattern={false} />
<CardContent class="p-12 relative z-10">
<EmptyState <EmptyState
message={t.wishlist.emptyWishes} message={t.wishlist.emptyWishes}
/> />

View File

@@ -123,6 +123,7 @@
<Navigation <Navigation
isAuthenticated={data.isAuthenticated} isAuthenticated={data.isAuthenticated}
showDashboardLink={true} showDashboardLink={true}
color={currentColor}
/> />
<WishlistHeader <WishlistHeader
@@ -155,7 +156,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 +170,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}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 130 KiB

2783
static/themes/snow/bgtop.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 130 KiB

1695
static/themes/snow/item.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -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

View File

@@ -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

View File

@@ -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