diff --git a/app/[slug]/page.tsx b/app/[slug]/page.tsx index 45b5d3d..f2d82cb 100644 --- a/app/[slug]/page.tsx +++ b/app/[slug]/page.tsx @@ -3,11 +3,19 @@ import { useEffect, useState } from 'react'; import { useParams } from 'next/navigation'; import DOMPurify from 'dompurify'; -import { wishlistsApi, itemsApi, claimingApi, type Wishlist, type Item } from '@/lib/api'; +import { authApi, wishlistsApi, itemsApi, claimingApi, type Wishlist, type Item } from '@/lib/api'; import Header from '@/components/header'; -import PasswordLockGuard from '@/components/password-lock-guard'; +import GuestGuard from '@/components/guest-guard'; export default function PublicWishlistPage() { + return ( + + + + ); +} + +function PublicWishlistContent() { const params = useParams(); const [wishlist, setWishlist] = useState(null); const [items, setItems] = useState([]); @@ -15,17 +23,36 @@ export default function PublicWishlistPage() { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(''); + // Current viewer + const [currentGuestId, setCurrentGuestId] = useState(null); + const [isAdmin, setIsAdmin] = useState(false); + // Claim form state const [claimingItemId, setClaimingItemId] = useState(null); const [claimNote, setClaimNote] = useState(''); + const [claimQty, setClaimQty] = useState(1); const [isClaiming, setIsClaiming] = useState(false); const [claimError, setClaimError] = useState(''); const [justClaimedItemId, setJustClaimedItemId] = useState(null); - const [justClaimedNote, setJustClaimedNote] = useState(''); // Unclaim state const [isUnclaiming, setIsUnclaiming] = useState(false); - const [unclaimError, setUnclaimError] = useState(''); + + useEffect(() => { + (async () => { + try { + const who = await authApi.whoami(); + if (who.role === 'admin') { + setIsAdmin(true); + if (who.guest) setCurrentGuestId(who.guest.id); + } else if (who.role === 'guest') { + setCurrentGuestId(who.guest.id); + } + } catch { + /* ignore */ + } + })(); + }, []); useEffect(() => { fetchWishlist(); @@ -47,10 +74,14 @@ export default function PublicWishlistPage() { } }; - const handleClaimItem = (itemId: string) => { - setClaimingItemId(itemId); + const myClaimFor = (item: Item) => item.claims.find((c) => c.guest.id === currentGuestId); + + const handleStartClaim = (item: Item) => { + const my = myClaimFor(item); + setClaimingItemId(item.id); setClaimError(''); - setClaimNote(''); + setClaimNote(my?.note ?? ''); + setClaimQty(my?.quantity ?? 1); setJustClaimedItemId(null); }; @@ -61,12 +92,12 @@ export default function PublicWishlistPage() { setClaimError(''); try { - await claimingApi.claim(itemId, undefined, claimNote); + await claimingApi.claim(itemId, { quantity: claimQty, note: claimNote }); setJustClaimedItemId(itemId); - setJustClaimedNote(claimNote); setClaimingItemId(null); setClaimNote(''); + setClaimQty(1); fetchWishlist(); } catch (err: any) { setClaimError(err.message || 'Erro ao reservar item'); @@ -75,19 +106,16 @@ export default function PublicWishlistPage() { } }; - const handleUnclaim = async (itemId: string) => { - if (!confirm('Tem certeza que deseja cancelar a reserva deste item?')) { - return; - } + const handleUnclaim = async (itemId: string, guestId?: string) => { + if (!confirm('Cancelar a reserva?')) return; setIsUnclaiming(true); - setUnclaimError(''); try { - await claimingApi.unclaim(itemId); + await claimingApi.unclaim(itemId, guestId ? { guestId } : {}); fetchWishlist(); } catch (err: any) { - setUnclaimError(err.message || 'Erro ao cancelar reserva'); + alert(err.message || 'Erro ao cancelar reserva'); } finally { setIsUnclaiming(false); } @@ -95,7 +123,10 @@ export default function PublicWishlistPage() { const filteredItems = showClaimed ? items - : items.filter((item) => !item.claimedAt || item.id === justClaimedItemId); + : items.filter((item) => { + const my = myClaimFor(item); + return item.remainingQuantity > 0 || my || item.id === justClaimedItemId; + }); const formatPrice = (price: number | null, currency: string) => { if (!price) return null; @@ -125,14 +156,13 @@ export default function PublicWishlistPage() { } return ( - -
-
+
+
{/* Main Content */}
@@ -167,7 +197,6 @@ export default function PublicWishlistPage() { className="prose prose-indigo dark:prose-invert max-w-none text-gray-700 dark:text-gray-300 [&_a]:text-indigo-600 [&_a]:dark:text-indigo-400 [&_a]:hover:underline" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(wishlist.preferences) }} onClick={(e) => { - // Make all links open in new tab const target = e.target as HTMLElement; if (target.tagName === 'A') { e.preventDefault(); @@ -188,7 +217,7 @@ export default function PublicWishlistPage() { onChange={(e) => setShowClaimed(e.target.checked)} className="h-4 w-4 text-blue-600 border-gray-300 rounded" /> - Mostrar itens reservados + Mostrar itens esgotados
@@ -205,155 +234,179 @@ export default function PublicWishlistPage() {
) : (
- {filteredItems.map((item) => ( -
-
- {/* Left: Image */} - {item.imageUrl && ( -
- {item.name} -
- )} + {filteredItems.map((item) => { + const myClaim = myClaimFor(item); + const maxForMe = item.remainingQuantity + (myClaim?.quantity ?? 0); + const showQuantitySummary = item.quantity > 1; + const sold = item.remainingQuantity === 0 && !myClaim; - {/* Middle: Item Details */} -
-

- {item.name} -

- {item.description && ( -

- {item.description} -

+ return ( +
+
+ {/* Left: Image */} + {item.imageUrl && ( +
+ {item.name} +
)} -
- {/* Right: Action Area */} -
-
- {item.purchaseUrls && item.purchaseUrls.length > 0 && ( - + {/* Middle: Item Details */} +
+

+ {item.name} +

+ {showQuantitySummary && ( +

+ {item.claimedQuantity} de {item.quantity} reservados +

+ )} + {item.description && ( +

+ {item.description} +

+ )} + + {/* Existing claims list */} + {item.claims.length > 0 && ( +
    + {item.claims.map((c) => { + const isMine = c.guest.id === currentGuestId; + const canCancel = isMine || isAdmin; + return ( +
  • +
    + {c.guest.name} + · {c.quantity} un. + {c.note && ( + + "{c.note}" + + )} +
    + {canCancel && ( + + )} +
  • + ); + })} +
)}
- {/* Claimed Badge, Success Message, or Claim Button/Form */} -
- {justClaimedItemId === item.id ? ( -
-
-
- - - + {/* Right: Action Area */} +
+
+ {item.purchaseUrls && item.purchaseUrls.length > 0 && ( + -
-

- Item reservado! -

-

- O status está confirmado. -

- {justClaimedNote && ( -

- Sua nota: "{justClaimedNote}" -

)}
- ) : item.claimedAt ? ( -
- {item.claimedByNote && ( -

- Nota: {item.claimedByNote} -

- )} - {item.isPurchased && ( -

- ✓ Comprado -

- )} - {showClaimed && ( - - )} -
- ) : claimingItemId === item.id ? ( -
-
handleSubmitClaim(e, item.id)} className="space-y-3"> - {claimError && ( -
- {claimError} + +
+ {sold ? ( +
+ Esgotado +
+ ) : claimingItemId === item.id ? ( + handleSubmitClaim(e, item.id)} className="space-y-3"> + {claimError && ( +
+ {claimError} +
+ )} + + {item.quantity > 1 && ( +
+ + setClaimQty(Math.max(1, Math.min(maxForMe, Number(e.target.value) || 1)))} + className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 dark:bg-gray-700 dark:text-white" + /> +
+ )} + +
+ +