ifixkart-storefront/app/wishlist/page.tsx

195 lines
7.7 KiB
TypeScript

/**
* @page Wishlist Page (`app/wishlist/page.tsx`)
* @purpose TechShop-style wishlist list with row layout and View Products CTA linking to reusable PDP.
*/
'use client';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { Heart, X } from 'lucide-react';
import { AnnouncementBar } from '@/layout/AnnouncementBar';
import { Header } from '@/layout/Header';
import { Navbar } from '@/layout/Navbar';
import { Footer } from '@/layout/Footer';
import { StickyHeaderSpacer } from '@/components/StickyHeaderSpacer';
import { FloatingButtons } from '@/components/FloatingButtons';
import { useCartStore } from '@/store/cartStore';
import { ProductResponse } from '@/services/api/catalogService';
import { formatCatalogPriceLabel, getImageUrl } from '@/lib/utils';
function getProductImage(product: ProductResponse): string {
const fromProduct = product.images?.[0]?.image_url;
const fromVariant = product.variants?.[0]?.images?.[0]?.image_url;
return getImageUrl(fromProduct || fromVariant || '');
}
function getPriceLabel(product: ProductResponse): string {
return formatCatalogPriceLabel(product);
}
function getActionLabel(product: ProductResponse): string {
const variantCount = product.variants?.length || 0;
return variantCount > 1 ? 'Select Options' : 'View Products';
}
function formatAddedDate(product: ProductResponse): string {
const raw = product.created_at;
if (!raw) {
return new Date().toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
}
const date = new Date(raw);
if (Number.isNaN(date.getTime())) {
return new Date().toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
}
return date.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
}
export default function WishlistPage() {
const wishlist = useCartStore((state) => state.wishlist);
const toggleWishlist = useCartStore((state) => state.toggleWishlist);
const hasHydrated = useCartStore((state) => state._hasHydrated);
const [ready, setReady] = useState(false);
useEffect(() => {
if (hasHydrated) {
setReady(true);
return;
}
// Fallback if persist rehydrate event is slow
const t = setTimeout(() => setReady(true), 250);
return () => clearTimeout(t);
}, [hasHydrated]);
return (
<div className="min-h-screen bg-[#F8F9FB] text-[#1E293B] flex flex-col font-sans">
<AnnouncementBar />
<Header />
<StickyHeaderSpacer />
<Navbar />
<main className="flex-grow pb-24 antialiased">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
{!ready ? (
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
{[1, 2, 3].map((i) => (
<div
key={i}
className={`flex items-center gap-5 px-5 py-5 animate-pulse ${
i % 2 === 0 ? 'bg-[#f8f9fb]' : 'bg-white'
}`}
>
<div className="w-[90px] h-[90px] rounded-md bg-gray-100 shrink-0" />
<div className="flex-1 space-y-2">
<div className="h-4 bg-gray-100 rounded w-2/3" />
<div className="h-3 bg-gray-100 rounded w-1/4" />
<div className="h-3 bg-gray-100 rounded w-1/5" />
</div>
<div className="h-10 w-32 bg-gray-100 rounded-md" />
</div>
))}
</div>
) : wishlist.length === 0 ? (
<div className="bg-white border border-gray-200 rounded-lg p-12 text-center max-w-lg mx-auto shadow-xs">
<Heart className="w-14 h-14 text-gray-300 mx-auto mb-4 stroke-1" />
<h2 className="text-lg font-bold text-gray-900 font-outfit">Your Wishlist is Empty</h2>
<p className="text-sm text-gray-500 mt-2 mb-6">
Explore products and click the heart icon to save items for later.
</p>
<Link
href="/shop"
className="inline-flex bg-primary hover:bg-primary-hover text-white text-sm font-semibold px-6 py-3 rounded-md transition-colors"
>
Browse Products
</Link>
</div>
) : (
<div className="bg-white border border-gray-200 rounded-lg overflow-hidden shadow-xs">
{wishlist.map((product, index) => {
const imageUrl = getProductImage(product);
const priceLabel = getPriceLabel(product);
const actionLabel = getActionLabel(product);
const addedDate = formatAddedDate(product);
const href = `/products/${product.slug || product.product_id}`;
return (
<div
key={product.product_id}
className={`relative flex flex-col sm:flex-row sm:items-center gap-4 sm:gap-6 px-4 sm:px-6 py-5 border-b border-gray-100 last:border-b-0 ${
index % 2 === 1 ? 'bg-[#f8f9fb]' : 'bg-white'
}`}
>
{/* Remove from wishlist */}
<button
type="button"
onClick={() => toggleWishlist(product)}
aria-label={`Remove ${product.name} from wishlist`}
className="absolute top-3 right-3 sm:static sm:order-4 p-1.5 text-gray-300 hover:text-[#e4382f] transition-colors cursor-pointer shrink-0"
>
<X size={16} strokeWidth={2.5} />
</button>
{/* Image */}
<Link
href={href}
className="w-[88px] h-[88px] sm:w-[100px] sm:h-[100px] rounded-md border border-gray-150 bg-white p-2 flex items-center justify-center shrink-0 overflow-hidden"
>
{imageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={imageUrl}
alt={product.name}
className="w-full h-full object-contain"
/>
) : (
<span className="text-[10px] text-gray-400 font-medium">No Image</span>
)}
</Link>
{/* Details */}
<div className="flex-1 min-w-0 text-left pr-8 sm:pr-0">
<Link
href={href}
className="block text-[15px] sm:text-[16px] font-semibold text-primary hover:underline leading-snug"
>
{product.name}
</Link>
<p className="mt-1.5 text-[15px] font-bold text-gray-900">{priceLabel}</p>
<p className="mt-1 text-[12px] text-gray-400 font-medium">{addedDate}</p>
</div>
{/* CTA — opens reusable product detail page */}
<div className="sm:ml-auto shrink-0">
<Link
href={href}
className="inline-flex items-center justify-center min-w-[140px] bg-primary hover:bg-primary-hover text-white text-[13px] font-semibold px-5 py-2.5 rounded-md transition-colors shadow-xs"
>
{actionLabel}
</Link>
</div>
</div>
);
})}
</div>
)}
</div>
</main>
<Footer />
<FloatingButtons />
</div>
);
}