ifixkart-storefront/components/TopDealsMegaMenu.tsx

239 lines
8.5 KiB
TypeScript

"use client";
import React, { useEffect, useState } from 'react';
import Link from 'next/link';
import { Layers } from 'lucide-react';
import {
catalogService,
CategoryResponse,
ProductCardResponse,
} from '@/services/api/catalogService';
import { formatCurrency, getImageUrl } from '@/lib/utils';
import {
CmsMegaMenuPanel,
hasCmsMegaGroups,
} from '@/components/CmsMegaMenuPanel';
import type { MegaMenuData } from '@/services/api/storefrontService';
import {
getMegaMenuProductSlugs,
storefrontService,
} from '@/services/api/storefrontService';
type ShopByItem = { name: string; slug: string; image?: string };
type RatedItem = {
id: string;
name: string;
slug: string;
image: string;
price: number;
oldPrice?: number;
};
function mapCategories(cats: CategoryResponse[]): ShopByItem[] {
return cats.map((c) => ({
name: c.name,
slug: c.slug,
image: c.image_url ? getImageUrl(c.image_url) : undefined,
}));
}
function mapProducts(products: ProductCardResponse[]): RatedItem[] {
return products.slice(0, 6).map((p) => ({
id: p.product_id,
name: p.name,
slug: p.slug,
image: getImageUrl(p.thumbnail_url) || '',
price: Number(p.price || 0),
oldPrice:
p.compare_price && Number(p.compare_price) > Number(p.price)
? Number(p.compare_price)
: undefined,
}));
}
export const TopDealsMegaMenu: React.FC<{ cms?: MegaMenuData | null }> = ({
cms,
}) => {
const [shopBy, setShopBy] = useState<ShopByItem[]>([]);
const [topRated, setTopRated] = useState<RatedItem[]>([]);
const [loading, setLoading] = useState(true);
const [remote, setRemote] = useState<MegaMenuData | null>(cms ?? null);
useEffect(() => {
if (cms) {
setRemote(cms);
return;
}
let cancelled = false;
storefrontService.getMegaMenu().then((menus) => {
if (cancelled) return;
setRemote(menus.deals || null);
});
return () => {
cancelled = true;
};
}, [cms]);
useEffect(() => {
let cancelled = false;
const pinned = getMegaMenuProductSlugs(remote);
const load = async () => {
setLoading(true);
try {
const cats = await catalogService.getCategories().catch(() => []);
if (cancelled) return;
if (cats?.length) {
setShopBy(mapCategories(cats).slice(0, 8));
} else {
setShopBy([]);
}
if (pinned.length) {
const cards = await catalogService.getProductsBySlugs(pinned.slice(0, 6));
if (!cancelled && cards.length) {
setTopRated(mapProducts(cards));
return;
}
}
const productRes = await catalogService
.getProductsPaginated({ sort: 'popular', limit: 6 })
.catch(() => catalogService.getProductsPaginated({ limit: 6 }));
if (!cancelled && productRes?.products?.length) {
setTopRated(mapProducts(productRes.products));
} else if (!cancelled) {
setTopRated([]);
}
} finally {
if (!cancelled) setLoading(false);
}
};
void load();
return () => {
cancelled = true;
};
}, [remote]);
if (hasCmsMegaGroups(remote)) {
return <CmsMegaMenuPanel menu={remote!} />;
}
return (
<div className="w-full max-w-full bg-white border border-t-0 border-gray-200 shadow-[0_16px_40px_rgba(0,0,0,0.12)] rounded-b-md overflow-hidden flex min-h-[320px]">
{/* Shop By — left half */}
<div className="w-1/2 min-w-0 bg-white p-5 lg:p-7 border-r border-gray-100 flex flex-col">
<h4 className="text-center text-[16px] font-bold text-[#222] mb-5">
Shop By
</h4>
{shopBy.length > 0 ? (
<div className="grid grid-cols-4 gap-x-3 gap-y-5 max-w-[440px] mx-auto w-full">
{shopBy.map((item) => (
<Link
key={item.slug}
href={`/shop?category=${encodeURIComponent(item.slug)}`}
className="flex flex-col items-center gap-2 group/item text-center min-w-0"
>
<span className="w-[64px] h-[64px] lg:w-[72px] lg:h-[72px] rounded-full border border-gray-200 bg-white flex items-center justify-center overflow-hidden p-2 group-hover/item:border-primary/40 transition-colors shrink-0">
{item.image ? (
<img
src={item.image}
alt={item.name}
className="max-w-full max-h-full object-contain"
/>
) : (
<Layers className="w-6 h-6 text-gray-400 group-hover/item:text-primary transition-colors" />
)}
</span>
<span className="text-[11px] lg:text-[12px] font-medium text-[#333] group-hover/item:text-primary transition-colors line-clamp-2 leading-snug px-0.5 w-full">
{item.name}
</span>
</Link>
))}
</div>
) : (
<div className="flex-1 flex items-center justify-center text-sm text-gray-400">
No categories available
</div>
)}
</div>
{/* Top Rated — right half */}
<div className="w-1/2 min-w-0 bg-[#F5F6F8] p-5 lg:p-7 flex flex-col">
<h4 className="text-center text-[16px] font-bold text-[#222] mb-5">
Top Rated
</h4>
{loading ? (
<div className="bg-white border border-gray-200 rounded-md overflow-hidden grid grid-cols-2 flex-1">
{Array.from({ length: 4 }).map((_, idx) => (
<div key={idx} className="p-4 animate-pulse flex items-center gap-3">
<div className="w-12 h-12 bg-gray-100 rounded" />
<div className="flex-1 space-y-2">
<div className="h-3 bg-gray-100 rounded w-3/4" />
<div className="h-3 bg-gray-100 rounded w-1/2" />
</div>
</div>
))}
</div>
) : topRated.length > 0 ? (
<div className="bg-white border border-gray-200 rounded-md overflow-hidden grid grid-cols-2">
{topRated.map((product, idx) => (
<Link
key={product.id}
href={`/products/${product.slug}`}
className={`flex items-center gap-2.5 p-3 hover:bg-gray-50 transition-colors border-gray-100 min-w-0 ${
idx % 2 === 0 ? 'border-r' : ''
} ${idx < topRated.length - 2 ? 'border-b' : ''}`}
>
<span className="w-12 h-12 lg:w-14 lg:h-14 shrink-0 flex items-center justify-center overflow-hidden bg-white">
{product.image ? (
<img
src={product.image}
alt={product.name}
className="max-w-full max-h-full object-contain"
/>
) : (
<span className="w-full h-full bg-gray-100 rounded flex items-center justify-center text-[10px] text-gray-400">
No Img
</span>
)}
</span>
<span className="min-w-0 text-left flex-1">
<span className="block text-[12px] font-semibold text-[#222] line-clamp-2 leading-snug">
{product.name}
</span>
<span className="mt-1 flex items-center gap-1.5 flex-wrap">
{product.oldPrice != null && (
<span className="text-[11px] text-gray-400 line-through">
{formatCurrency(product.oldPrice)}
</span>
)}
<span className="text-[13px] font-bold text-[#222]">
{formatCurrency(product.price)}
</span>
</span>
</span>
</Link>
))}
</div>
) : (
<div className="flex-1 flex items-center justify-center text-sm text-gray-400">
No top rated products
</div>
)}
</div>
</div>
);
};
export function isTopDealsNavLabel(label: string): boolean {
const n = label.toLowerCase().replace(/\s+/g, '');
return (
n.includes('topdeal') ||
n === 'deals' ||
n.includes('top-deals') ||
label.toLowerCase().includes('top deals')
);
}