add: local wishlists stored in local storage for anonymous users
This commit is contained in:
93
src/lib/components/dashboard/LocalWishlistsSection.svelte
Normal file
93
src/lib/components/dashboard/LocalWishlistsSection.svelte
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import WishlistSection from '$lib/components/dashboard/WishlistSection.svelte';
|
||||||
|
import { getLocalWishlists, forgetLocalWishlist, toggleLocalFavorite, type LocalWishlist } from '$lib/utils/localWishlists';
|
||||||
|
import { languageStore } from '$lib/stores/language.svelte';
|
||||||
|
import { Star } from 'lucide-svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
let {
|
||||||
|
isAuthenticated = false
|
||||||
|
}: {
|
||||||
|
isAuthenticated?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
const t = $derived(languageStore.t);
|
||||||
|
|
||||||
|
let localWishlists = $state<LocalWishlist[]>([]);
|
||||||
|
|
||||||
|
// Load local wishlists on mount (client-side only)
|
||||||
|
onMount(() => {
|
||||||
|
localWishlists = getLocalWishlists();
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleForget(ownerToken: string) {
|
||||||
|
forgetLocalWishlist(ownerToken);
|
||||||
|
localWishlists = getLocalWishlists();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToggleFavorite(ownerToken: string) {
|
||||||
|
toggleLocalFavorite(ownerToken);
|
||||||
|
localWishlists = getLocalWishlists();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform LocalWishlist to match the format expected by WishlistSection
|
||||||
|
const transformedWishlists = $derived(() => {
|
||||||
|
return localWishlists.map(w => ({
|
||||||
|
id: w.ownerToken,
|
||||||
|
title: w.title,
|
||||||
|
ownerToken: w.ownerToken,
|
||||||
|
publicToken: w.publicToken,
|
||||||
|
createdAt: w.createdAt,
|
||||||
|
isFavorite: w.isFavorite || false,
|
||||||
|
items: [] // We don't have item data in localStorage
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if localWishlists.length > 0}
|
||||||
|
<WishlistSection
|
||||||
|
title={t.dashboard.localWishlists || "Local Wishlists"}
|
||||||
|
description={t.dashboard.localWishlistsDescription || "Wishlists stored in your browser. Sign in to save them permanently."}
|
||||||
|
items={transformedWishlists()}
|
||||||
|
emptyMessage=""
|
||||||
|
>
|
||||||
|
{#snippet actions(wishlist, unlocked)}
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onclick={() => handleToggleFavorite(wishlist.ownerToken)}
|
||||||
|
>
|
||||||
|
<Star class={wishlist.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
|
||||||
|
</Button>
|
||||||
|
<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 unlocked}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onclick={() => handleForget(wishlist.ownerToken)}
|
||||||
|
>
|
||||||
|
{t.dashboard.forget || "Forget"}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</WishlistSection>
|
||||||
|
{/if}
|
||||||
@@ -8,16 +8,25 @@
|
|||||||
let { userName, userEmail }: { userName?: string | null; userEmail?: string | null } = $props();
|
let { userName, userEmail }: { userName?: string | null; userEmail?: string | null } = $props();
|
||||||
|
|
||||||
const t = $derived(languageStore.t);
|
const t = $derived(languageStore.t);
|
||||||
|
const isAuthenticated = $derived(!!userName || !!userEmail);
|
||||||
</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">
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h1 class="text-3xl font-bold">{t.nav.dashboard}</h1>
|
<h1 class="text-3xl font-bold">{t.nav.dashboard}</h1>
|
||||||
|
{#if isAuthenticated}
|
||||||
<p class="text-muted-foreground truncate">{t.dashboard.welcomeBack}, {userName || userEmail}</p>
|
<p class="text-muted-foreground truncate">{t.dashboard.welcomeBack}, {userName || userEmail}</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-muted-foreground">{t.dashboard.anonymousDashboard || "Your local wishlists"}</p>
|
||||||
|
{/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">
|
||||||
<LanguageToggle />
|
<LanguageToggle />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
|
{#if isAuthenticated}
|
||||||
<Button variant="outline" onclick={() => signOut({ callbackUrl: '/' })}>{t.auth.signOut}</Button>
|
<Button variant="outline" onclick={() => signOut({ callbackUrl: '/' })}>{t.auth.signOut}</Button>
|
||||||
|
{:else}
|
||||||
|
<Button variant="outline" onclick={() => (window.location.href = '/signin')}>{t.auth.signIn}</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,18 +2,24 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
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 { isLocalWishlist } from '$lib/utils/localWishlists';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isOwner,
|
isOwner,
|
||||||
hasClaimed
|
hasClaimed,
|
||||||
|
ownerToken
|
||||||
}: {
|
}: {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isOwner: boolean;
|
isOwner: boolean;
|
||||||
hasClaimed: boolean;
|
hasClaimed: boolean;
|
||||||
|
ownerToken: string;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const t = $derived(languageStore.t);
|
const t = $derived(languageStore.t);
|
||||||
|
|
||||||
|
// Check if this wishlist is in localStorage
|
||||||
|
const isLocal = $derived(isLocalWishlist(ownerToken));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isAuthenticated}
|
{#if isAuthenticated}
|
||||||
@@ -33,7 +39,11 @@
|
|||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action={hasClaimed ? "?/unclaimWishlist" : "?/claimWishlist"}
|
action={hasClaimed ? "?/unclaimWishlist" : "?/claimWishlist"}
|
||||||
use:enhance
|
use:enhance={() => {
|
||||||
|
return async ({ update }) => {
|
||||||
|
await update({ reset: false });
|
||||||
|
};
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -48,6 +58,10 @@
|
|||||||
You have claimed this wishlist. It will appear in your dashboard.
|
You have claimed this wishlist. It will appear in your dashboard.
|
||||||
{:else}
|
{:else}
|
||||||
Claim this wishlist to add it to your dashboard for easy access.
|
Claim this wishlist to add it to your dashboard for easy access.
|
||||||
|
{#if isLocal}
|
||||||
|
<br />
|
||||||
|
<span class="text-xs">It will remain in your local wishlists and also appear in your claimed wishlists.</span>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
103
src/lib/utils/localWishlists.ts
Normal file
103
src/lib/utils/localWishlists.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for managing anonymous user's wishlists in localStorage
|
||||||
|
*/
|
||||||
|
|
||||||
|
const LOCAL_WISHLISTS_KEY = 'local_wishlists';
|
||||||
|
|
||||||
|
export interface LocalWishlist {
|
||||||
|
ownerToken: string;
|
||||||
|
publicToken: string;
|
||||||
|
title: string;
|
||||||
|
createdAt: string;
|
||||||
|
isFavorite?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all local wishlists from localStorage
|
||||||
|
*/
|
||||||
|
export function getLocalWishlists(): LocalWishlist[] {
|
||||||
|
if (typeof window === 'undefined') return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(LOCAL_WISHLISTS_KEY);
|
||||||
|
return stored ? JSON.parse(stored) : [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse local wishlists:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a wishlist to localStorage
|
||||||
|
*/
|
||||||
|
export function addLocalWishlist(wishlist: LocalWishlist): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wishlists = getLocalWishlists();
|
||||||
|
|
||||||
|
// Check if already exists
|
||||||
|
const exists = wishlists.some(w => w.ownerToken === wishlist.ownerToken);
|
||||||
|
if (exists) return;
|
||||||
|
|
||||||
|
wishlists.push(wishlist);
|
||||||
|
localStorage.setItem(LOCAL_WISHLISTS_KEY, JSON.stringify(wishlists));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add local wishlist:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a wishlist from localStorage (forget it)
|
||||||
|
*/
|
||||||
|
export function forgetLocalWishlist(ownerToken: string): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wishlists = getLocalWishlists();
|
||||||
|
const filtered = wishlists.filter(w => w.ownerToken !== ownerToken);
|
||||||
|
localStorage.setItem(LOCAL_WISHLISTS_KEY, JSON.stringify(filtered));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to forget local wishlist:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all local wishlists (e.g., when user claims all wishlists)
|
||||||
|
*/
|
||||||
|
export function clearLocalWishlists(): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(LOCAL_WISHLISTS_KEY);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear local wishlists:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a wishlist is in local storage
|
||||||
|
*/
|
||||||
|
export function isLocalWishlist(ownerToken: string): boolean {
|
||||||
|
const wishlists = getLocalWishlists();
|
||||||
|
return wishlists.some(w => w.ownerToken === ownerToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle favorite status for a local wishlist
|
||||||
|
*/
|
||||||
|
export function toggleLocalFavorite(ownerToken: string): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wishlists = getLocalWishlists();
|
||||||
|
const updated = wishlists.map(w =>
|
||||||
|
w.ownerToken === ownerToken
|
||||||
|
? { ...w, isFavorite: !w.isFavorite }
|
||||||
|
: w
|
||||||
|
);
|
||||||
|
localStorage.setItem(LOCAL_WISHLISTS_KEY, JSON.stringify(updated));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to toggle local wishlist favorite:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
import ColorPicker from '$lib/components/ui/ColorPicker.svelte';
|
import ColorPicker from '$lib/components/ui/ColorPicker.svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { languageStore } from '$lib/stores/language.svelte';
|
import { languageStore } from '$lib/stores/language.svelte';
|
||||||
|
import { addLocalWishlist } from '$lib/utils/localWishlists';
|
||||||
|
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
|
|
||||||
@@ -32,7 +33,19 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const { ownerToken } = await response.json();
|
const result = await response.json();
|
||||||
|
const { ownerToken, publicToken, title: wishlistTitle, createdAt } = result;
|
||||||
|
|
||||||
|
// If user is not authenticated, save to localStorage
|
||||||
|
if (!data.session?.user) {
|
||||||
|
addLocalWishlist({
|
||||||
|
ownerToken,
|
||||||
|
publicToken,
|
||||||
|
title: wishlistTitle,
|
||||||
|
createdAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
goto(`/wishlist/${ownerToken}/edit`);
|
goto(`/wishlist/${ownerToken}/edit`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -56,9 +69,8 @@
|
|||||||
<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">
|
||||||
<LanguageToggle />
|
<LanguageToggle />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
{#if data.session?.user}
|
|
||||||
<Button variant="outline" onclick={() => goto('/dashboard')}>{t.nav.dashboard}</Button>
|
<Button variant="outline" onclick={() => goto('/dashboard')}>{t.nav.dashboard}</Button>
|
||||||
{:else}
|
{#if !data.session?.user}
|
||||||
<Button variant="outline" onclick={() => goto('/signin')}>{t.auth.signIn}</Button>
|
<Button variant="outline" onclick={() => goto('/signin')}>{t.auth.signIn}</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,5 +29,11 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
return json({ ownerToken, publicToken, id: wishlist.id });
|
return json({
|
||||||
|
ownerToken,
|
||||||
|
publicToken,
|
||||||
|
id: wishlist.id,
|
||||||
|
title: wishlist.title,
|
||||||
|
createdAt: wishlist.createdAt
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ import { eq, and } from 'drizzle-orm';
|
|||||||
export const load: PageServerLoad = async (event) => {
|
export const load: PageServerLoad = async (event) => {
|
||||||
const session = await event.locals.auth();
|
const session = await event.locals.auth();
|
||||||
|
|
||||||
|
// Allow anonymous users to access dashboard for local wishlists
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
throw redirect(303, '/signin');
|
return {
|
||||||
|
user: null,
|
||||||
|
wishlists: [],
|
||||||
|
savedWishlists: [],
|
||||||
|
isAuthenticated: false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const userWishlists = await db.query.wishlists.findMany({
|
const userWishlists = await db.query.wishlists.findMany({
|
||||||
@@ -53,7 +59,8 @@ export const load: PageServerLoad = async (event) => {
|
|||||||
return {
|
return {
|
||||||
user: session.user,
|
user: session.user,
|
||||||
wishlists: userWishlists,
|
wishlists: userWishlists,
|
||||||
savedWishlists: savedWithAccess
|
savedWishlists: savedWithAccess,
|
||||||
|
isAuthenticated: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
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 WishlistSection from '$lib/components/dashboard/WishlistSection.svelte';
|
import WishlistSection from '$lib/components/dashboard/WishlistSection.svelte';
|
||||||
|
import LocalWishlistsSection from '$lib/components/dashboard/LocalWishlistsSection.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';
|
||||||
@@ -36,6 +37,10 @@
|
|||||||
<PageContainer>
|
<PageContainer>
|
||||||
<DashboardHeader userName={data.user?.name} userEmail={data.user?.email} />
|
<DashboardHeader userName={data.user?.name} userEmail={data.user?.email} />
|
||||||
|
|
||||||
|
<!-- Local Wishlists Section (for anonymous and authenticated users) -->
|
||||||
|
<LocalWishlistsSection isAuthenticated={data.isAuthenticated} />
|
||||||
|
|
||||||
|
{#if data.isAuthenticated}
|
||||||
<!-- My Wishlists Section -->
|
<!-- My Wishlists Section -->
|
||||||
<WishlistSection
|
<WishlistSection
|
||||||
title={t.dashboard.myWishlists}
|
title={t.dashboard.myWishlists}
|
||||||
@@ -189,4 +194,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</WishlistSection>
|
</WishlistSection>
|
||||||
|
{/if}
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|||||||
@@ -130,6 +130,7 @@
|
|||||||
isAuthenticated={data.isAuthenticated}
|
isAuthenticated={data.isAuthenticated}
|
||||||
isOwner={data.isOwner}
|
isOwner={data.isOwner}
|
||||||
hasClaimed={data.hasClaimed}
|
hasClaimed={data.hasClaimed}
|
||||||
|
ownerToken={data.wishlist.ownerToken}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<WishlistActionButtons
|
<WishlistActionButtons
|
||||||
|
|||||||
Reference in New Issue
Block a user