add: multiple ownership of wishlists
This commit is contained in:
@@ -13,8 +13,23 @@
|
|||||||
|
|
||||||
const t = $derived(languageStore.t);
|
const t = $derived(languageStore.t);
|
||||||
|
|
||||||
|
// Combine owned and claimed wishlists for "My Wishlists"
|
||||||
|
const allMyWishlists = $derived(() => {
|
||||||
|
const owned = data.wishlists || [];
|
||||||
|
// Only include claimed wishlists (those with ownerToken, meaning they were claimed via edit link)
|
||||||
|
const claimed = (data.savedWishlists || [])
|
||||||
|
.filter(saved => saved.wishlist?.ownerToken) // Has edit access
|
||||||
|
.map(saved => ({
|
||||||
|
...saved.wishlist,
|
||||||
|
isFavorite: saved.isFavorite,
|
||||||
|
isClaimed: true,
|
||||||
|
savedId: saved.id
|
||||||
|
}));
|
||||||
|
return [...owned, ...claimed];
|
||||||
|
});
|
||||||
|
|
||||||
const sortedWishlists = $derived(
|
const sortedWishlists = $derived(
|
||||||
[...data.wishlists].sort((a, b) => {
|
[...allMyWishlists()].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;
|
||||||
|
|
||||||
@@ -32,23 +47,26 @@
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Saved wishlists are those WITHOUT ownerToken (saved from public view only)
|
||||||
const sortedSavedWishlists = $derived(
|
const sortedSavedWishlists = $derived(
|
||||||
[...data.savedWishlists].sort((a, b) => {
|
[...(data.savedWishlists || [])]
|
||||||
if (a.isFavorite && !b.isFavorite) return -1;
|
.filter(saved => !saved.wishlist?.ownerToken) // No edit access
|
||||||
if (!a.isFavorite && b.isFavorite) return 1;
|
.sort((a, b) => {
|
||||||
|
if (a.isFavorite && !b.isFavorite) return -1;
|
||||||
|
if (!a.isFavorite && b.isFavorite) return 1;
|
||||||
|
|
||||||
const aHasEndDate = !!a.wishlist?.endDate;
|
const aHasEndDate = !!a.wishlist?.endDate;
|
||||||
const bHasEndDate = !!b.wishlist?.endDate;
|
const bHasEndDate = !!b.wishlist?.endDate;
|
||||||
|
|
||||||
if (aHasEndDate && !bHasEndDate) return -1;
|
if (aHasEndDate && !bHasEndDate) return -1;
|
||||||
if (!aHasEndDate && bHasEndDate) return 1;
|
if (!aHasEndDate && bHasEndDate) return 1;
|
||||||
|
|
||||||
if (aHasEndDate && bHasEndDate) {
|
if (aHasEndDate && bHasEndDate) {
|
||||||
return new Date(a.wishlist.endDate!).getTime() - new Date(b.wishlist.endDate!).getTime();
|
return new Date(a.wishlist.endDate!).getTime() - new Date(b.wishlist.endDate!).getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -107,17 +125,33 @@
|
|||||||
color={wishlist.color}
|
color={wishlist.color}
|
||||||
>
|
>
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex gap-2 flex-wrap">
|
||||||
<form method="POST" action="?/toggleFavorite" use:enhance={() => {
|
{#if wishlist.isClaimed}
|
||||||
return async ({ update }) => {
|
<!-- For claimed wishlists, use saved favorite toggle -->
|
||||||
await update({ reset: false });
|
<form method="POST" action="?/toggleSavedFavorite" 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">
|
<input type="hidden" name="savedWishlistId" value={wishlist.savedId} />
|
||||||
<Star class={wishlist.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
|
<input type="hidden" name="isFavorite" value={wishlist.isFavorite} />
|
||||||
</Button>
|
<Button type="submit" size="sm" variant="outline">
|
||||||
</form>
|
<Star class={wishlist.isFavorite ? "fill-yellow-500 text-yellow-500" : ""} />
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<!-- For owned wishlists, use regular favorite toggle -->
|
||||||
|
<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>
|
||||||
|
{/if}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onclick={() => (window.location.href = `/wishlist/${wishlist.ownerToken}/edit`)}
|
onclick={() => (window.location.href = `/wishlist/${wishlist.ownerToken}/edit`)}
|
||||||
@@ -135,6 +169,19 @@
|
|||||||
>
|
>
|
||||||
{t.dashboard.copyLink}
|
{t.dashboard.copyLink}
|
||||||
</Button>
|
</Button>
|
||||||
|
{#if wishlist.isClaimed}
|
||||||
|
<!-- 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.unsave}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</WishlistCard>
|
</WishlistCard>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { wishlists, items } from '$lib/server/schema';
|
import { wishlists, items, savedWishlists } from '$lib/server/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, locals }) => {
|
export const load: PageServerLoad = async ({ params, locals }) => {
|
||||||
const wishlist = await db.query.wishlists.findFirst({
|
const wishlist = await db.query.wishlists.findFirst({
|
||||||
@@ -19,11 +19,29 @@ export const load: PageServerLoad = async ({ params, locals }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session = await locals.auth();
|
const session = await locals.auth();
|
||||||
|
let hasClaimed = false;
|
||||||
|
let isOwner = false;
|
||||||
|
|
||||||
|
if (session?.user?.id) {
|
||||||
|
// Check if user is the owner
|
||||||
|
isOwner = wishlist.userId === session.user.id;
|
||||||
|
|
||||||
|
// Check if user has claimed this wishlist
|
||||||
|
const savedWishlist = await db.query.savedWishlists.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(savedWishlists.userId, session.user.id),
|
||||||
|
eq(savedWishlists.wishlistId, wishlist.id)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
hasClaimed = !!savedWishlist;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
wishlist,
|
wishlist,
|
||||||
publicUrl: `/wishlist/${wishlist.publicToken}`,
|
publicUrl: `/wishlist/${wishlist.publicToken}`,
|
||||||
isAuthenticated: !!session?.user
|
isAuthenticated: !!session?.user,
|
||||||
|
hasClaimed,
|
||||||
|
isOwner
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -224,5 +242,66 @@ export const actions: Actions = {
|
|||||||
.where(eq(wishlists.id, wishlist.id));
|
.where(eq(wishlists.id, wishlist.id));
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
},
|
||||||
|
|
||||||
|
claimWishlist: async ({ params, locals }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw error(401, 'You must be signed in to claim a wishlist');
|
||||||
|
}
|
||||||
|
|
||||||
|
const wishlist = await db.query.wishlists.findFirst({
|
||||||
|
where: eq(wishlists.ownerToken, params.token)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!wishlist) {
|
||||||
|
throw error(404, 'Wishlist not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already claimed
|
||||||
|
const existing = await db.query.savedWishlists.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(savedWishlists.userId, session.user.id),
|
||||||
|
eq(savedWishlists.wishlistId, wishlist.id)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return { success: true, message: 'Already claimed' };
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(savedWishlists).values({
|
||||||
|
userId: session.user.id,
|
||||||
|
wishlistId: wishlist.id,
|
||||||
|
isFavorite: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, message: 'Wishlist claimed successfully' };
|
||||||
|
},
|
||||||
|
|
||||||
|
unclaimWishlist: async ({ params, locals }) => {
|
||||||
|
const session = await locals.auth();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw error(401, 'You must be signed in');
|
||||||
|
}
|
||||||
|
|
||||||
|
const wishlist = await db.query.wishlists.findFirst({
|
||||||
|
where: eq(wishlists.ownerToken, params.token)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!wishlist) {
|
||||||
|
throw error(404, 'Wishlist not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.delete(savedWishlists).where(
|
||||||
|
and(
|
||||||
|
eq(savedWishlists.userId, session.user.id),
|
||||||
|
eq(savedWishlists.wishlistId, wishlist.id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true, message: 'Wishlist unclaimed' };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -206,6 +206,44 @@
|
|||||||
ownerUrl="/wishlist/{data.wishlist.ownerToken}/edit"
|
ownerUrl="/wishlist/{data.wishlist.ownerToken}/edit"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{#if data.isAuthenticated}
|
||||||
|
<div class="mb-6">
|
||||||
|
{#if data.isOwner}
|
||||||
|
<Button
|
||||||
|
disabled
|
||||||
|
variant="outline"
|
||||||
|
class="w-full md:w-auto opacity-60 cursor-not-allowed"
|
||||||
|
>
|
||||||
|
You Own This Wishlist
|
||||||
|
</Button>
|
||||||
|
<p class="text-sm text-muted-foreground mt-2">
|
||||||
|
This wishlist is already in your dashboard as the owner.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action={data.hasClaimed ? "?/unclaimWishlist" : "?/claimWishlist"}
|
||||||
|
use:enhance
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant={data.hasClaimed ? "outline" : "default"}
|
||||||
|
class="w-full md:w-auto"
|
||||||
|
>
|
||||||
|
{data.hasClaimed ? "Unclaim Wishlist" : "Claim Wishlist"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
<p class="text-sm text-muted-foreground mt-2">
|
||||||
|
{#if data.hasClaimed}
|
||||||
|
You have claimed this wishlist. It will appear in your dashboard.
|
||||||
|
{:else}
|
||||||
|
Claim this wishlist to add it to your dashboard for easy access.
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<WishlistActionButtons
|
<WishlistActionButtons
|
||||||
bind:rearranging={rearranging}
|
bind:rearranging={rearranging}
|
||||||
onToggleAddForm={handleToggleAddForm}
|
onToggleAddForm={handleToggleAddForm}
|
||||||
|
|||||||
Reference in New Issue
Block a user