add: better search and better delete locking mechanism

This commit is contained in:
2025-11-25 22:14:27 +01:00
parent ad3634cf98
commit 62ff4826c1
6 changed files with 138 additions and 63 deletions

View File

@@ -14,6 +14,7 @@
emptyActionLabel, emptyActionLabel,
emptyActionHref, emptyActionHref,
headerAction, headerAction,
searchBar,
children children
}: { }: {
title: string; title: string;
@@ -24,6 +25,7 @@
emptyActionLabel?: string; emptyActionLabel?: string;
emptyActionHref?: string; emptyActionHref?: string;
headerAction?: Snippet; headerAction?: Snippet;
searchBar?: Snippet;
children: Snippet<[any]>; children: Snippet<[any]>;
} = $props(); } = $props();
@@ -53,6 +55,11 @@
{@render headerAction()} {@render headerAction()}
{/if} {/if}
</div> </div>
{#if searchBar}
<div class="mt-4">
{@render searchBar()}
</div>
{/if}
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{#if items && items.length > 0} {#if items && items.length > 0}

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { languageStore } from '$lib/stores/language.svelte';
let {
value = $bindable(''),
placeholder = languageStore.t.dashboard.searchPlaceholder
}: {
value: string;
placeholder?: string;
} = $props();
</script>
<Input
type="search"
{placeholder}
bind:value
/>

View File

@@ -0,0 +1,31 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { Lock, LockOpen } from 'lucide-svelte';
import { languageStore } from '$lib/stores/language.svelte';
let {
unlocked = $bindable(false)
}: {
unlocked: boolean;
} = $props();
const t = $derived(languageStore.t);
function handleClick() {
unlocked = !unlocked;
}
</script>
<Button
onclick={handleClick}
variant={unlocked ? "default" : "outline"}
class="w-full md:w-auto"
>
{#if unlocked}
<Lock class="mr-2 h-4 w-4" />
{t.wishlist.lockEditing}
{:else}
<LockOpen class="mr-2 h-4 w-4" />
{t.wishlist.unlockEditing}
{/if}
</Button>

View File

@@ -8,11 +8,18 @@
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);
let savedWishlistsUnlocked = $state(false);
let myWishlistsSearch = $state('');
let savedWishlistsSearch = $state('');
// Combine owned and claimed wishlists for "My Wishlists" // Combine owned and claimed wishlists for "My Wishlists"
const allMyWishlists = $derived(() => { const allMyWishlists = $derived(() => {
const owned = data.wishlists || []; const owned = data.wishlists || [];
@@ -28,8 +35,15 @@
return [...owned, ...claimed]; return [...owned, ...claimed];
}); });
const sortedWishlists = $derived( const sortedWishlists = $derived(() => {
[...allMyWishlists()].sort((a, b) => { 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;
if (!a.isFavorite && b.isFavorite) return 1; if (!a.isFavorite && b.isFavorite) return 1;
@@ -44,30 +58,37 @@
} }
return new Date(b.createdAt).getTime() - new Date(a.createdAt).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 sortedSavedWishlists = $derived(() => {
[...(data.savedWishlists || [])] const filtered = savedWishlistsSearch.trim()
.filter(saved => !saved.wishlist?.ownerToken) // No edit access ? (data.savedWishlists || [])
.sort((a, b) => { .filter(saved => !saved.wishlist?.ownerToken) // No edit access
if (a.isFavorite && !b.isFavorite) return -1; .filter(saved =>
if (!a.isFavorite && b.isFavorite) return 1; saved.wishlist?.title.toLowerCase().includes(savedWishlistsSearch.toLowerCase()) ||
saved.wishlist?.description?.toLowerCase().includes(savedWishlistsSearch.toLowerCase())
)
: (data.savedWishlists || []).filter(saved => !saved.wishlist?.ownerToken);
const aHasEndDate = !!a.wishlist?.endDate; return [...filtered].sort((a, b) => {
const bHasEndDate = !!b.wishlist?.endDate; if (a.isFavorite && !b.isFavorite) return -1;
if (!a.isFavorite && b.isFavorite) return 1;
if (aHasEndDate && !bHasEndDate) return -1; const aHasEndDate = !!a.wishlist?.endDate;
if (!aHasEndDate && bHasEndDate) return 1; const bHasEndDate = !!b.wishlist?.endDate;
if (aHasEndDate && bHasEndDate) { if (aHasEndDate && !bHasEndDate) return -1;
return new Date(a.wishlist.endDate!).getTime() - new Date(b.wishlist.endDate!).getTime(); if (!aHasEndDate && bHasEndDate) return 1;
}
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); 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 { function formatEndDate(date: Date | string | null): string | null {
if (!date) return null; if (!date) return null;
@@ -108,13 +129,22 @@
<WishlistGrid <WishlistGrid
title={t.dashboard.myWishlists} title={t.dashboard.myWishlists}
description={t.dashboard.myWishlistsDescription} description={t.dashboard.myWishlistsDescription}
items={sortedWishlists || []} items={sortedWishlists() || []}
emptyMessage={t.dashboard.emptyWishlists} emptyMessage={t.dashboard.emptyWishlists}
emptyActionLabel={t.dashboard.emptyWishlistsAction} emptyActionLabel={t.dashboard.emptyWishlistsAction}
emptyActionHref="/" emptyActionHref="/"
> >
{#snippet headerAction()} {#snippet headerAction()}
<Button onclick={() => (window.location.href = '/')}>{t.dashboard.createNew}</Button> <div class="flex gap-2 flex-wrap">
<Button onclick={() => (window.location.href = '/')}>{t.dashboard.createNew}</Button>
<UnlockButton bind:unlocked={myWishlistsUnlocked} />
</div>
{/snippet}
{#snippet searchBar()}
{#if allMyWishlists().length > 0}
<SearchBar bind:value={myWishlistsSearch} />
{/if}
{/snippet} {/snippet}
{#snippet children(wishlist)} {#snippet children(wishlist)}
@@ -169,7 +199,7 @@
> >
{t.dashboard.copyLink} {t.dashboard.copyLink}
</Button> </Button>
{#if wishlist.isClaimed} {#if wishlist.isClaimed && myWishlistsUnlocked}
<!-- Add unclaim button for claimed wishlists --> <!-- Add unclaim button for claimed wishlists -->
<form method="POST" action="?/unsaveWishlist" use:enhance={() => { <form method="POST" action="?/unsaveWishlist" use:enhance={() => {
return async ({ update }) => { return async ({ update }) => {
@@ -190,10 +220,20 @@
<WishlistGrid <WishlistGrid
title={t.dashboard.savedWishlists} title={t.dashboard.savedWishlists}
description={t.dashboard.savedWishlistsDescription} description={t.dashboard.savedWishlistsDescription}
items={sortedSavedWishlists || []} items={sortedSavedWishlists() || []}
emptyMessage={t.dashboard.emptySavedWishlists} emptyMessage={t.dashboard.emptySavedWishlists}
emptyDescription={t.dashboard.emptySavedWishlistsDescription} emptyDescription={t.dashboard.emptySavedWishlistsDescription}
> >
{#snippet headerAction()}
<UnlockButton bind:unlocked={savedWishlistsUnlocked} />
{/snippet}
{#snippet searchBar()}
{#if (data.savedWishlists || []).filter(saved => !saved.wishlist?.ownerToken).length > 0}
<SearchBar bind:value={savedWishlistsSearch} />
{/if}
{/snippet}
{#snippet children(saved)} {#snippet children(saved)}
<WishlistCard <WishlistCard
title={saved.wishlist?.title} title={saved.wishlist?.title}
@@ -219,16 +259,18 @@
> >
{t.dashboard.viewWishlist} {t.dashboard.viewWishlist}
</Button> </Button>
<form method="POST" action="?/unsaveWishlist" use:enhance={() => { {#if savedWishlistsUnlocked}
return async ({ update }) => { <form method="POST" action="?/unsaveWishlist" use:enhance={() => {
await update({ reset: false }); return async ({ update }) => {
}; await update({ reset: false });
}}> };
<input type="hidden" name="savedWishlistId" value={saved.id} /> }}>
<Button type="submit" size="sm" variant="destructive"> <input type="hidden" name="savedWishlistId" value={saved.id} />
{t.dashboard.unsave} <Button type="submit" size="sm" variant="destructive">
</Button> {t.dashboard.unsave}
</form> </Button>
</form>
{/if}
</div> </div>
</WishlistCard> </WishlistCard>
{/snippet} {/snippet}

View File

@@ -18,6 +18,7 @@
import { enhance } from "$app/forms"; import { enhance } from "$app/forms";
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";
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
@@ -124,11 +125,7 @@
<!-- Search Bar --> <!-- Search Bar -->
{#if data.wishlist.items && data.wishlist.items.length > 0} {#if data.wishlist.items && data.wishlist.items.length > 0}
<Input <SearchBar bind:value={searchQuery} />
type="search"
placeholder={t.dashboard.searchPlaceholder}
bind:value={searchQuery}
/>
{/if} {/if}
<!-- Items List --> <!-- Items List -->

View File

@@ -9,11 +9,11 @@
import WishlistActionButtons from "$lib/components/wishlist/WishlistActionButtons.svelte"; import WishlistActionButtons from "$lib/components/wishlist/WishlistActionButtons.svelte";
import EditableItemsList from "$lib/components/wishlist/EditableItemsList.svelte"; import EditableItemsList from "$lib/components/wishlist/EditableItemsList.svelte";
import type { Item } from "$lib/server/schema"; import type { Item } from "$lib/server/schema";
import { Input } from "$lib/components/ui/input";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import { Search, Lock, LockOpen } from "lucide-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 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();
@@ -274,15 +274,7 @@
{/if} {/if}
{#if sortedItems.length > 5} {#if sortedItems.length > 5}
<div class="relative"> <SearchBar bind:value={searchQuery} />
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder={t.dashboard.searchPlaceholder}
bind:value={searchQuery}
class="pl-9"
/>
</div>
{/if} {/if}
<EditableItemsList <EditableItemsList
@@ -294,19 +286,7 @@
<div class="mt-12 pt-8 border-t border-border space-y-4"> <div class="mt-12 pt-8 border-t border-border space-y-4">
<div class="flex flex-col md:flex-row gap-4 justify-between items-stretch md:items-center"> <div class="flex flex-col md:flex-row gap-4 justify-between items-stretch md:items-center">
<Button <UnlockButton bind:unlocked={rearranging} />
onclick={() => rearranging = !rearranging}
variant={rearranging ? "default" : "outline"}
class="w-full md:w-auto"
>
{#if rearranging}
<Lock class="mr-2 h-4 w-4" />
{t.wishlist.lockEditing}
{:else}
<LockOpen class="mr-2 h-4 w-4" />
{t.wishlist.unlockEditing}
{/if}
</Button>
{#if rearranging} {#if rearranging}
<form <form