add: better search and better delete locking mechanism
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
18
src/lib/components/ui/SearchBar.svelte
Normal file
18
src/lib/components/ui/SearchBar.svelte
Normal 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
|
||||||
|
/>
|
||||||
31
src/lib/components/ui/UnlockButton.svelte
Normal file
31
src/lib/components/ui/UnlockButton.svelte
Normal 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>
|
||||||
@@ -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,14 +58,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
? (data.savedWishlists || [])
|
||||||
.filter(saved => !saved.wishlist?.ownerToken) // No edit access
|
.filter(saved => !saved.wishlist?.ownerToken) // No edit access
|
||||||
.sort((a, b) => {
|
.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;
|
||||||
if (!a.isFavorite && b.isFavorite) return 1;
|
if (!a.isFavorite && b.isFavorite) return 1;
|
||||||
|
|
||||||
@@ -66,8 +87,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).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()}
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
<Button onclick={() => (window.location.href = '/')}>{t.dashboard.createNew}</Button>
|
<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,6 +259,7 @@
|
|||||||
>
|
>
|
||||||
{t.dashboard.viewWishlist}
|
{t.dashboard.viewWishlist}
|
||||||
</Button>
|
</Button>
|
||||||
|
{#if savedWishlistsUnlocked}
|
||||||
<form method="POST" action="?/unsaveWishlist" use:enhance={() => {
|
<form method="POST" action="?/unsaveWishlist" use:enhance={() => {
|
||||||
return async ({ update }) => {
|
return async ({ update }) => {
|
||||||
await update({ reset: false });
|
await update({ reset: false });
|
||||||
@@ -229,6 +270,7 @@
|
|||||||
{t.dashboard.unsave}
|
{t.dashboard.unsave}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</WishlistCard>
|
</WishlistCard>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user