refactor: abstract dashboard lists into components

This commit is contained in:
rasmusq
2025-11-27 21:12:16 +01:00
parent c5ece3d6bb
commit 86c0665aed
2 changed files with 287 additions and 311 deletions

View File

@@ -0,0 +1,157 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
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';
import SearchBar from '$lib/components/ui/SearchBar.svelte';
import UnlockButton from '$lib/components/ui/UnlockButton.svelte';
import type { Snippet } from 'svelte';
type WishlistItem = any; // You can make this more specific based on your types
let {
title,
description,
items,
emptyMessage,
emptyDescription,
emptyActionLabel,
emptyActionHref,
showCreateButton = false,
hideIfEmpty = false,
actions
}: {
title: string;
description: string;
items: WishlistItem[];
emptyMessage: string;
emptyDescription?: string;
emptyActionLabel?: string;
emptyActionHref?: string;
showCreateButton?: boolean;
hideIfEmpty?: boolean;
actions: Snippet<[WishlistItem, boolean]>; // item, unlocked
} = $props();
const t = $derived(languageStore.t);
let unlocked = $state(false);
let searchQuery = $state('');
// Filter items based on search query
const filteredItems = $derived(() => {
if (!searchQuery.trim()) return items;
return items.filter(item => {
const title = item.title || item.wishlist?.title || '';
const description = item.description || item.wishlist?.description || '';
const query = searchQuery.toLowerCase();
return title.toLowerCase().includes(query) || description.toLowerCase().includes(query);
});
});
// Sort items by favorite, end date, then created date
const sortedItems = $derived(() => {
return [...filteredItems()].sort((a, b) => {
// Handle both direct wishlists and saved wishlists
const aItem = a.wishlist || a;
const bItem = b.wishlist || b;
// Sort by favorite first
if (a.isFavorite && !b.isFavorite) return -1;
if (!a.isFavorite && b.isFavorite) return 1;
// Then by end date
const aHasEndDate = !!aItem.endDate;
const bHasEndDate = !!bItem.endDate;
if (aHasEndDate && !bHasEndDate) return -1;
if (!aHasEndDate && bHasEndDate) return 1;
if (aHasEndDate && bHasEndDate) {
return new Date(aItem.endDate!).getTime() - new Date(bItem.endDate!).getTime();
}
// Finally by created date (most recent first)
const aCreatedAt = a.createdAt || aItem.createdAt;
const bCreatedAt = b.createdAt || bItem.createdAt;
return new Date(bCreatedAt).getTime() - new Date(aCreatedAt).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(item: any): string | null {
const wishlist = item.wishlist || item;
if (!wishlist) return null;
const lines: string[] = [];
const topItems = wishlist.items?.slice(0, 3).map((i: any) => i.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;
}
// Hide entire section if hideIfEmpty is true and there are no items
const shouldShow = $derived(() => {
return !hideIfEmpty || items.length > 0;
});
</script>
{#if shouldShow()}
<WishlistGrid
{title}
{description}
items={sortedItems() || []}
{emptyMessage}
{emptyDescription}
{emptyActionLabel}
{emptyActionHref}
>
{#snippet headerAction()}
<div class="flex flex-col sm:flex-row gap-2">
{#if showCreateButton}
<Button onclick={() => (window.location.href = '/')}>{t.dashboard.createNew}</Button>
{/if}
<UnlockButton bind:unlocked />
</div>
{/snippet}
{#snippet searchBar()}
{#if items.length > 0}
<SearchBar bind:value={searchQuery} />
{/if}
{/snippet}
{#snippet children(item)}
{@const wishlist = item.wishlist || item}
<WishlistCard
title={wishlist.title}
description={getWishlistDescription(item)}
itemCount={wishlist.items?.length || 0}
color={wishlist.color}
>
{@render actions(item, unlocked)}
</WishlistCard>
{/snippet}
</WishlistGrid>
{/if}

View File

@@ -3,371 +3,190 @@
import type { PageData } from './$types'; import type { PageData } from './$types';
import PageContainer from '$lib/components/layout/PageContainer.svelte'; import PageContainer from '$lib/components/layout/PageContainer.svelte';
import DashboardHeader from '$lib/components/layout/DashboardHeader.svelte'; import DashboardHeader from '$lib/components/layout/DashboardHeader.svelte';
import WishlistGrid from '$lib/components/dashboard/WishlistGrid.svelte'; import WishlistSection from '$lib/components/dashboard/WishlistSection.svelte';
import WishlistCard from '$lib/components/dashboard/WishlistCard.svelte';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { Star } from 'lucide-svelte'; import { Star } from 'lucide-svelte';
import { languageStore } from '$lib/stores/language.svelte'; import { languageStore } from '$lib/stores/language.svelte';
import SearchBar from '$lib/components/ui/SearchBar.svelte';
import UnlockButton from '$lib/components/ui/UnlockButton.svelte';
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
const t = $derived(languageStore.t); const t = $derived(languageStore.t);
let myWishlistsUnlocked = $state(false); // Only owned wishlists for "My Wishlists"
let claimedWishlistsUnlocked = $state(false); const myWishlists = $derived(() => data.wishlists || []);
let savedWishlistsUnlocked = $state(false);
let myWishlistsSearch = $state('');
let claimedWishlistsSearch = $state('');
let savedWishlistsSearch = $state('');
// Only owned wishlists for "My Wishlists" (exclude claimed)
const allMyWishlists = $derived(() => {
const owned = data.wishlists || [];
return owned;
});
// Claimed wishlists (those with ownerToken, meaning they were claimed via edit link) // Claimed wishlists (those with ownerToken, meaning they were claimed via edit link)
const allClaimedWishlists = $derived(() => { const claimedWishlists = $derived(() => {
const claimed = (data.savedWishlists || []) return (data.savedWishlists || [])
.filter(saved => saved.wishlist?.ownerToken) // Has edit access .filter(saved => saved.wishlist?.ownerToken)
.map(saved => ({ .map(saved => ({
...saved.wishlist, ...saved.wishlist,
isFavorite: saved.isFavorite, isFavorite: saved.isFavorite,
isClaimed: true, isClaimed: true,
savedId: saved.id savedId: saved.id
})); }));
return claimed;
});
const sortedWishlists = $derived(() => {
const filtered = myWishlistsSearch.trim()
? allMyWishlists().filter(w =>
w.title.toLowerCase().includes(myWishlistsSearch.toLowerCase()) ||
w.description?.toLowerCase().includes(myWishlistsSearch.toLowerCase())
)
: allMyWishlists();
return [...filtered].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 sortedClaimedWishlists = $derived(() => {
const filtered = claimedWishlistsSearch.trim()
? allClaimedWishlists().filter(w =>
w.title.toLowerCase().includes(claimedWishlistsSearch.toLowerCase()) ||
w.description?.toLowerCase().includes(claimedWishlistsSearch.toLowerCase())
)
: allClaimedWishlists();
return [...filtered].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();
});
}); });
// Saved wishlists are those WITHOUT ownerToken (saved from public view only) // Saved wishlists are those WITHOUT ownerToken (saved from public view only)
const sortedSavedWishlists = $derived(() => { const savedWishlists = $derived(() => {
const filtered = savedWishlistsSearch.trim() return (data.savedWishlists || []).filter(saved => !saved.wishlist?.ownerToken);
? (data.savedWishlists || [])
.filter(saved => !saved.wishlist?.ownerToken) // No edit access
.filter(saved =>
saved.wishlist?.title.toLowerCase().includes(savedWishlistsSearch.toLowerCase()) ||
saved.wishlist?.description?.toLowerCase().includes(savedWishlistsSearch.toLowerCase())
)
: (data.savedWishlists || []).filter(saved => !saved.wishlist?.ownerToken);
return [...filtered].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> </script>
<PageContainer> <PageContainer>
<DashboardHeader userName={data.user?.name} userEmail={data.user?.email} /> <DashboardHeader userName={data.user?.name} userEmail={data.user?.email} />
<WishlistGrid <!-- My Wishlists Section -->
<WishlistSection
title={t.dashboard.myWishlists} title={t.dashboard.myWishlists}
description={t.dashboard.myWishlistsDescription} description={t.dashboard.myWishlistsDescription}
items={sortedWishlists() || []} items={myWishlists()}
emptyMessage={t.dashboard.emptyWishlists} emptyMessage={t.dashboard.emptyWishlists}
emptyActionLabel={t.dashboard.emptyWishlistsAction} emptyActionLabel={t.dashboard.emptyWishlistsAction}
emptyActionHref="/" emptyActionHref="/"
showCreateButton={true}
> >
{#snippet headerAction()} {#snippet actions(wishlist, unlocked)}
<div class="flex flex-col sm:flex-row gap-2"> <div class="flex gap-2 flex-wrap">
<Button onclick={() => (window.location.href = '/')}>{t.dashboard.createNew}</Button> <form method="POST" action="?/toggleFavorite" use:enhance={() => {
<UnlockButton bind:unlocked={myWishlistsUnlocked} /> return async ({ update }) => {
</div> await update({ reset: false });
{/snippet} };
}}>
{#snippet searchBar()} <input type="hidden" name="wishlistId" value={wishlist.id} />
{#if allMyWishlists().length > 0} <input type="hidden" name="isFavorite" value={wishlist.isFavorite} />
<SearchBar bind:value={myWishlistsSearch} /> <Button type="submit" size="sm" variant="outline">
{/if} <Star class={wishlist.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
{/snippet} </Button>
</form>
{#snippet children(wishlist)} <Button
<WishlistCard size="sm"
title={wishlist.title} onclick={() => (window.location.href = `/wishlist/${wishlist.ownerToken}/edit`)}
description={getWishlistDescription(wishlist)} >
itemCount={wishlist.items?.length || 0} {t.dashboard.manage}
color={wishlist.color} </Button>
> <Button
<div class="flex gap-2 flex-wrap"> size="sm"
<!-- For owned wishlists, use regular favorite toggle --> variant="outline"
<form method="POST" action="?/toggleFavorite" use:enhance={() => { onclick={() => {
navigator.clipboard.writeText(
`${window.location.origin}/wishlist/${wishlist.publicToken}`
);
}}
>
{t.dashboard.copyLink}
</Button>
{#if unlocked}
<form method="POST" action="?/deleteWishlist" use:enhance={() => {
return async ({ update }) => { return async ({ update }) => {
await update({ reset: false }); await update({ reset: false });
}; };
}}> }}>
<input type="hidden" name="wishlistId" value={wishlist.id} /> <input type="hidden" name="wishlistId" value={wishlist.id} />
<input type="hidden" name="isFavorite" value={wishlist.isFavorite} /> <Button type="submit" size="sm" variant="destructive">
<Button type="submit" size="sm" variant="outline"> {t.dashboard.delete}
<Star class={wishlist.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
</Button> </Button>
</form> </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>
{#if myWishlistsUnlocked}
<!-- Add delete button for owned wishlists when unlocked -->
<form method="POST" action="?/deleteWishlist" use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
}}>
<input type="hidden" name="wishlistId" value={wishlist.id} />
<Button type="submit" size="sm" variant="destructive">
{t.dashboard.delete}
</Button>
</form>
{/if}
</div>
</WishlistCard>
{/snippet}
</WishlistGrid>
{#if allClaimedWishlists().length > 0}
<WishlistGrid
title={t.dashboard.claimedWishlists}
description={t.dashboard.claimedWishlistsDescription}
items={sortedClaimedWishlists() || []}
emptyMessage={t.dashboard.emptyClaimedWishlists}
emptyDescription={t.dashboard.emptyClaimedWishlistsDescription}
>
{#snippet headerAction()}
<div class="flex flex-col sm:flex-row gap-2">
<UnlockButton bind:unlocked={claimedWishlistsUnlocked} />
</div>
{/snippet}
{#snippet searchBar()}
{#if allClaimedWishlists().length > 0}
<SearchBar bind:value={claimedWishlistsSearch} />
{/if} {/if}
{/snippet} </div>
{/snippet}
</WishlistSection>
{#snippet children(wishlist)} <!-- Claimed Wishlists Section -->
<WishlistCard <WishlistSection
title={wishlist.title} title={t.dashboard.claimedWishlists}
description={getWishlistDescription(wishlist)} description={t.dashboard.claimedWishlistsDescription}
itemCount={wishlist.items?.length || 0} items={claimedWishlists()}
color={wishlist.color} emptyMessage={t.dashboard.emptyClaimedWishlists}
emptyDescription={t.dashboard.emptyClaimedWishlistsDescription}
hideIfEmpty={true}
>
{#snippet actions(wishlist, unlocked)}
<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={wishlist.savedId} />
<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`)}
> >
<div class="flex gap-2 flex-wrap"> {t.dashboard.manage}
<!-- For claimed wishlists, use saved favorite toggle --> </Button>
<form method="POST" action="?/toggleSavedFavorite" use:enhance={() => { <Button
return async ({ update }) => { size="sm"
await update({ reset: false }); variant="outline"
}; onclick={() => {
}}> navigator.clipboard.writeText(
<input type="hidden" name="savedWishlistId" value={wishlist.savedId} /> `${window.location.origin}/wishlist/${wishlist.publicToken}`
<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> {t.dashboard.copyLink}
</form> </Button>
<Button {#if unlocked}
size="sm" <form method="POST" action="?/unsaveWishlist" use:enhance={() => {
onclick={() => (window.location.href = `/wishlist/${wishlist.ownerToken}/edit`)} return async ({ update }) => {
> await update({ reset: false });
{t.dashboard.manage} };
}}>
<input type="hidden" name="savedWishlistId" value={wishlist.savedId} />
<Button type="submit" size="sm" variant="destructive">
{t.dashboard.unclaim}
</Button> </Button>
<Button </form>
size="sm" {/if}
variant="outline" </div>
onclick={() => { {/snippet}
navigator.clipboard.writeText( </WishlistSection>
`${window.location.origin}/wishlist/${wishlist.publicToken}`
);
}}
>
{t.dashboard.copyLink}
</Button>
{#if claimedWishlistsUnlocked}
<!-- Add unclaim button for claimed wishlists -->
<form method="POST" action="?/unsaveWishlist" use:enhance={() => {
return async ({ update }) => {
await update({ reset: false });
};
}}>
<input type="hidden" name="savedWishlistId" value={wishlist.savedId} />
<Button type="submit" size="sm" variant="destructive">
{t.dashboard.unclaim}
</Button>
</form>
{/if}
</div>
</WishlistCard>
{/snippet}
</WishlistGrid>
{/if}
<WishlistGrid <!-- Saved Wishlists Section -->
<WishlistSection
title={t.dashboard.savedWishlists} title={t.dashboard.savedWishlists}
description={t.dashboard.savedWishlistsDescription} description={t.dashboard.savedWishlistsDescription}
items={sortedSavedWishlists() || []} items={savedWishlists()}
emptyMessage={t.dashboard.emptySavedWishlists} emptyMessage={t.dashboard.emptySavedWishlists}
emptyDescription={t.dashboard.emptySavedWishlistsDescription} emptyDescription={t.dashboard.emptySavedWishlistsDescription}
> >
{#snippet headerAction()} {#snippet actions(saved, unlocked)}
<div class="flex flex-col sm:flex-row gap-2"> <div class="flex gap-2 flex-wrap">
<UnlockButton bind:unlocked={savedWishlistsUnlocked} /> <form method="POST" action="?/toggleSavedFavorite" use:enhance={() => {
</div> return async ({ update }) => {
{/snippet} await update({ reset: false });
};
{#snippet searchBar()} }}>
{#if (data.savedWishlists || []).filter(saved => !saved.wishlist?.ownerToken).length > 0} <input type="hidden" name="savedWishlistId" value={saved.id} />
<SearchBar bind:value={savedWishlistsSearch} /> <input type="hidden" name="isFavorite" value={saved.isFavorite} />
{/if} <Button type="submit" size="sm" variant="outline">
{/snippet} <Star class={saved.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
</Button>
{#snippet children(saved)} </form>
<WishlistCard <Button
title={saved.wishlist?.title} size="sm"
description={getSavedWishlistDescription(saved)} onclick={() => (window.location.href = `/wishlist/${saved.wishlist.publicToken}`)}
itemCount={saved.wishlist?.items?.length || 0} >
color={saved.wishlist?.color} {t.dashboard.viewWishlist}
> </Button>
<div class="flex gap-2 flex-wrap"> {#if unlocked}
<form method="POST" action="?/toggleSavedFavorite" use:enhance={() => { <form method="POST" action="?/unsaveWishlist" use:enhance={() => {
return async ({ update }) => { return async ({ update }) => {
await update({ reset: false }); await update({ reset: false });
}; };
}}> }}>
<input type="hidden" name="savedWishlistId" value={saved.id} /> <input type="hidden" name="savedWishlistId" value={saved.id} />
<input type="hidden" name="isFavorite" value={saved.isFavorite} /> <Button type="submit" size="sm" variant="destructive">
<Button type="submit" size="sm" variant="outline"> {t.dashboard.unsave}
<Star class={saved.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
</Button> </Button>
</form> </form>
<Button {/if}
size="sm" </div>
onclick={() => (window.location.href = `/wishlist/${saved.wishlist.publicToken}`)}
>
{t.dashboard.viewWishlist}
</Button>
{#if savedWishlistsUnlocked}
<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>
{/if}
</div>
</WishlistCard>
{/snippet} {/snippet}
</WishlistGrid> </WishlistSection>
</PageContainer> </PageContainer>