initial production version

This commit is contained in:
2025-11-25 16:08:50 +01:00
parent 44ce6e38dd
commit 0144e8df1a
108 changed files with 5502 additions and 1780 deletions

View File

@@ -0,0 +1,111 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
import { db } from '$lib/server/db';
import { wishlists, savedWishlists } from '$lib/server/schema';
import { eq, and } from 'drizzle-orm';
export const load: PageServerLoad = async (event) => {
const session = await event.locals.auth();
if (!session?.user?.id) {
throw redirect(303, '/signin');
}
const userWishlists = await db.query.wishlists.findMany({
where: eq(wishlists.userId, session.user.id),
with: {
items: {
orderBy: (items, { asc }) => [asc(items.order)]
},
user: true
},
orderBy: (wishlists, { desc }) => [desc(wishlists.createdAt)]
});
const saved = await db.query.savedWishlists.findMany({
where: eq(savedWishlists.userId, session.user.id),
with: {
wishlist: {
with: {
items: {
orderBy: (items, { asc }) => [asc(items.order)]
},
user: true
}
}
},
orderBy: (savedWishlists, { desc }) => [desc(savedWishlists.createdAt)]
});
return {
user: session.user,
wishlists: userWishlists,
savedWishlists: saved
};
};
export const actions: Actions = {
toggleFavorite: async ({ request, locals }) => {
const session = await locals.auth();
if (!session?.user?.id) {
throw redirect(303, '/signin');
}
const formData = await request.formData();
const wishlistId = formData.get('wishlistId') as string;
const isFavorite = formData.get('isFavorite') === 'true';
if (!wishlistId) {
return { success: false, error: 'Wishlist ID is required' };
}
await db.update(wishlists)
.set({ isFavorite: !isFavorite })
.where(eq(wishlists.id, wishlistId));
return { success: true };
},
toggleSavedFavorite: async ({ request, locals }) => {
const session = await locals.auth();
if (!session?.user?.id) {
throw redirect(303, '/signin');
}
const formData = await request.formData();
const savedWishlistId = formData.get('savedWishlistId') as string;
const isFavorite = formData.get('isFavorite') === 'true';
if (!savedWishlistId) {
return { success: false, error: 'Saved wishlist ID is required' };
}
await db.update(savedWishlists)
.set({ isFavorite: !isFavorite })
.where(eq(savedWishlists.id, savedWishlistId));
return { success: true };
},
unsaveWishlist: async ({ request, locals }) => {
const session = await locals.auth();
if (!session?.user?.id) {
throw redirect(303, '/signin');
}
const formData = await request.formData();
const savedWishlistId = formData.get('savedWishlistId') as string;
if (!savedWishlistId) {
return { success: false, error: 'Saved wishlist ID is required' };
}
await db.delete(savedWishlists)
.where(and(
eq(savedWishlists.id, savedWishlistId),
eq(savedWishlists.userId, session.user.id)
));
return { success: true };
}
};

View File

@@ -0,0 +1,189 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import type { PageData } from './$types';
import PageContainer from '$lib/components/layout/PageContainer.svelte';
import DashboardHeader from '$lib/components/layout/DashboardHeader.svelte';
import WishlistGrid from '$lib/components/dashboard/WishlistGrid.svelte';
import WishlistCard from '$lib/components/dashboard/WishlistCard.svelte';
import { enhance } from '$app/forms';
import { Star } from 'lucide-svelte';
import { languageStore } from '$lib/stores/language.svelte';
let { data }: { data: PageData } = $props();
const t = $derived(languageStore.t);
const sortedWishlists = $derived(
[...data.wishlists].sort((a, b) => {
if (a.isFavorite && !b.isFavorite) return -1;
if (!a.isFavorite && b.isFavorite) return 1;
const aHasEndDate = !!a.endDate;
const bHasEndDate = !!b.endDate;
if (aHasEndDate && !bHasEndDate) return -1;
if (!aHasEndDate && bHasEndDate) return 1;
if (aHasEndDate && bHasEndDate) {
return new Date(a.endDate!).getTime() - new Date(b.endDate!).getTime();
}
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
})
);
const sortedSavedWishlists = $derived(
[...data.savedWishlists].sort((a, b) => {
if (a.isFavorite && !b.isFavorite) return -1;
if (!a.isFavorite && b.isFavorite) return 1;
const aHasEndDate = !!a.wishlist?.endDate;
const bHasEndDate = !!b.wishlist?.endDate;
if (aHasEndDate && !bHasEndDate) return -1;
if (!aHasEndDate && bHasEndDate) return 1;
if (aHasEndDate && bHasEndDate) {
return new Date(a.wishlist.endDate!).getTime() - new Date(b.wishlist.endDate!).getTime();
}
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
})
);
function formatEndDate(date: Date | string | null): string | null {
if (!date) return null;
const d = new Date(date);
return d.toLocaleDateString(languageStore.t.date.format.short, { year: 'numeric', month: 'short', day: 'numeric' });
}
function getWishlistDescription(wishlist: any): string | null {
if (!wishlist) return null;
const lines: string[] = [];
const topItems = wishlist.items?.slice(0, 3).map((item: any) => item.title) || [];
if (topItems.length > 0) {
lines.push(topItems.join(', '));
}
if (wishlist.user?.name || wishlist.user?.username) {
const ownerName = wishlist.user.name || wishlist.user.username;
lines.push(`${t.dashboard.by} ${ownerName}`);
}
if (wishlist.endDate) {
lines.push(`${t.dashboard.ends}: ${formatEndDate(wishlist.endDate)}`);
}
return lines.length > 0 ? lines.join('\n') : null;
}
function getSavedWishlistDescription(saved: any): string | null {
return getWishlistDescription(saved.wishlist);
}
</script>
<PageContainer>
<DashboardHeader userName={data.user?.name} userEmail={data.user?.email} />
<WishlistGrid
title={t.dashboard.myWishlists}
description={t.dashboard.myWishlistsDescription}
items={sortedWishlists || []}
emptyMessage={t.dashboard.emptyWishlists}
emptyActionLabel={t.dashboard.emptyWishlistsAction}
emptyActionHref="/"
>
{#snippet headerAction()}
<Button onclick={() => (window.location.href = '/')}>{t.dashboard.createNew}</Button>
{/snippet}
{#snippet children(wishlist)}
<WishlistCard
title={wishlist.title}
description={getWishlistDescription(wishlist)}
itemCount={wishlist.items?.length || 0}
color={wishlist.color}
>
<div class="flex gap-2 flex-wrap">
<form method="POST" action="?/toggleFavorite" use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
}}>
<input type="hidden" name="wishlistId" value={wishlist.id} />
<input type="hidden" name="isFavorite" value={wishlist.isFavorite} />
<Button type="submit" size="sm" variant="outline">
<Star class={wishlist.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
</Button>
</form>
<Button
size="sm"
onclick={() => (window.location.href = `/wishlist/${wishlist.ownerToken}/edit`)}
>
{t.dashboard.manage}
</Button>
<Button
size="sm"
variant="outline"
onclick={() => {
navigator.clipboard.writeText(
`${window.location.origin}/wishlist/${wishlist.publicToken}`
);
}}
>
{t.dashboard.copyLink}
</Button>
</div>
</WishlistCard>
{/snippet}
</WishlistGrid>
<WishlistGrid
title={t.dashboard.savedWishlists}
description={t.dashboard.savedWishlistsDescription}
items={sortedSavedWishlists || []}
emptyMessage={t.dashboard.emptySavedWishlists}
emptyDescription={t.dashboard.emptySavedWishlistsDescription}
>
{#snippet children(saved)}
<WishlistCard
title={saved.wishlist?.title}
description={getSavedWishlistDescription(saved)}
itemCount={saved.wishlist?.items?.length || 0}
color={saved.wishlist?.color}
>
<div class="flex gap-2 flex-wrap">
<form method="POST" action="?/toggleSavedFavorite" use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
}}>
<input type="hidden" name="savedWishlistId" value={saved.id} />
<input type="hidden" name="isFavorite" value={saved.isFavorite} />
<Button type="submit" size="sm" variant="outline">
<Star class={saved.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
</Button>
</form>
<Button
size="sm"
onclick={() => (window.location.href = `/wishlist/${saved.wishlist.publicToken}`)}
>
{t.dashboard.viewWishlist}
</Button>
<form method="POST" action="?/unsaveWishlist" use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
}}>
<input type="hidden" name="savedWishlistId" value={saved.id} />
<Button type="submit" size="sm" variant="destructive">
{t.dashboard.unsave}
</Button>
</form>
</div>
</WishlistCard>
{/snippet}
</WishlistGrid>
</PageContainer>