Files
the-pet-loft/apps/storefront/src/components/shop/ShopIndexContent.tsx
ianshaloom a5e61d02fd feat(storefront): add payment/carrier assets, CustomerConfidenceBooster, and footer enhancements
- Add payment method SVGs (Visa, Mastercard, Apple Pay, Google Pay, Klarna, Link, Revolut Pay, Billie, Cartes, Discover)
- Add carrier images (DPD, Evri)
- Add CustomerConfidenceBooster section component
- Enhance Footer with payment methods and carrier display
- Wire CustomerConfidenceBooster into shop pages (PetCategory, RecentlyAdded, ShopIndex, SubCategory, Tag, TopCategory) and home page
- Update tsconfig.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 14:48:02 +03:00

186 lines
6.3 KiB
TypeScript

"use client";
import { useQuery } from "convex/react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import { api } from "../../../../../convex/_generated/api";
import { ShopBreadcrumbBar } from "@/components/shop/ShopBreadcrumbBar";
import { ShopCategoryStrip } from "@/components/shop/ShopCategoryStrip";
import { ShopFilterModal } from "@/components/shop/ShopFilterModal";
import { ShopFilterSidebar } from "@/components/shop/ShopFilterSidebar";
import { ShopPageBanner } from "@/components/shop/ShopPageBanner";
import { ShopProductGrid } from "@/components/shop/ShopProductGrid";
import { ShopToolbar, type SortOption } from "@/components/shop/ShopToolbar";
import { ShopEmptyState } from "@/components/shop/state/ShopEmptyState";
import { ShopErrorState } from "@/components/shop/state/ShopErrorState";
import { ShopProductGridSkeleton } from "@/components/shop/state/ShopProductGridSkeleton";
import { CustomerConfidenceBooster } from "@/components/sections/CustomerConfidenceBooster";
import {
filterStateFromSearchParams,
filterStateToSearchParams,
shopFilterStateToApiArgs,
} from "@/lib/shop/filterState";
import {
enrichedProductToCardProps,
type EnrichedProduct,
} from "@/lib/shop/productMapper";
import {
PET_CATEGORY_SLUGS,
TOP_CATEGORY_SLUGS,
} from "@/lib/shop/constants";
const SORT_OPTIONS: SortOption[] = [
{ value: "newest", label: "Newest" },
{ value: "price-asc", label: "Price: Low to High" },
{ value: "price-desc", label: "Price: High to Low" },
];
function formatCategoryLabel(slug: string): string {
if (slug === "other-pets") return "Other Pets";
return slug.charAt(0).toUpperCase() + slug.slice(1);
}
export function ShopIndexContent() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const [sort, setSort] = useState<string>("newest");
const [filterModalOpen, setFilterModalOpen] = useState(false);
const filterState = useMemo(
() => filterStateFromSearchParams(searchParams),
[searchParams],
);
const filterArgs = useMemo(() => shopFilterStateToApiArgs(filterState), [filterState]);
const filterOptions = useQuery(api.products.getFilterOptions, {});
const products = useQuery(api.products.listActive, filterArgs);
const setFilterStateToUrl = useCallback(
(state: typeof filterState) => {
const params = filterStateToSearchParams(state);
const q = params.toString();
router.replace(q ? `${pathname}?${q}` : pathname);
},
[pathname, router],
);
const stripLinks = useMemo(
() => [
...PET_CATEGORY_SLUGS.map((slug) => ({
label: formatCategoryLabel(slug),
href: `/shop/${slug}`,
})),
...TOP_CATEGORY_SLUGS.map((slug) => ({
label: formatCategoryLabel(slug),
href: `/shop/${slug}`,
})),
],
[],
);
const sortedProducts = useMemo(() => {
if (!products || !Array.isArray(products)) return [];
const list = [...products] as EnrichedProduct[];
if (sort === "newest") {
list.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
} else if (sort === "price-asc") {
list.sort((a, b) => {
const pa = a.variants?.[0]?.price ?? 0;
const pb = b.variants?.[0]?.price ?? 0;
return pa - pb;
});
} else if (sort === "price-desc") {
list.sort((a, b) => {
const pa = a.variants?.[0]?.price ?? 0;
const pb = b.variants?.[0]?.price ?? 0;
return pb - pa;
});
}
return list;
}, [products, sort]);
const handleRetry = useCallback(() => {
window.location.reload();
}, []);
return (
<main className="mx-auto w-full max-w-7xl min-w-0 px-4 py-6 md:px-6 md:py-8">
{/* Top row: breadcrumb + toolbar (same row) */}
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<ShopBreadcrumbBar
items={[
{ label: "Home", href: "/" },
{ label: "Shop" },
]}
/>
<ShopToolbar
sortOptions={SORT_OPTIONS}
currentSort={sort}
onSortChange={setSort}
onOpenFilter={() => setFilterModalOpen(true)}
resultCount={Array.isArray(products) ? products.length : undefined}
/>
</div>
<ShopPageBanner
title="Shop"
subtitle="Browse all products"
/>
{/* Two-column layout on lg: sidebar | main */}
<div className="mt-6 flex flex-col gap-6 lg:flex-row lg:items-start">
{/* Left: filter sidebar (desktop only) */}
<ShopFilterSidebar
filterOptions={filterOptions ?? null}
filterState={filterState}
onFilterChange={setFilterStateToUrl}
filterOptionsLoading={filterOptions === undefined}
/>
{/* Right: category strip + product area */}
<div className="min-w-0 flex-1">
<ShopCategoryStrip links={stripLinks} />
<div className="mt-6">
{products === undefined ? (
<ShopProductGridSkeleton />
) : products === null ? (
<ShopErrorState
message="Failed to load products."
onRetry={handleRetry}
/>
) : Array.isArray(products) && products.length === 0 ? (
<ShopEmptyState
title="No products found"
description="There are no products available right now."
/>
) : (
<ShopProductGrid
products={sortedProducts.map((p) => ({
...enrichedProductToCardProps(p as EnrichedProduct),
id: (p as { _id: string })._id,
}))}
/>
)}
<div className="mt-8">
<CustomerConfidenceBooster />
</div>
</div>
</div>
</div>
{/* Filter modal (mobile/tablet only); filters apply on "Apply filter" */}
<ShopFilterModal
isOpen={filterModalOpen}
onClose={() => setFilterModalOpen(false)}
onApply={() => setFilterModalOpen(false)}
filterOptions={filterOptions ?? null}
filterState={filterState}
onApplyFilters={setFilterStateToUrl}
filterOptionsLoading={filterOptions === undefined}
/>
</main>
);
}