- 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>
185 lines
6.3 KiB
TypeScript
185 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 { 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,
|
|
mergeFilterStateIntoSearchParams,
|
|
shopFilterStateToApiArgs,
|
|
} from "@/lib/shop/filterState";
|
|
import {
|
|
enrichedProductToCardProps,
|
|
type EnrichedProduct,
|
|
} from "@/lib/shop/productMapper";
|
|
|
|
const SORT_OPTIONS: SortOption[] = [
|
|
{ value: "newest", label: "Newest" },
|
|
{ value: "price-asc", label: "Price: Low to High" },
|
|
{ value: "price-desc", label: "Price: High to Low" },
|
|
];
|
|
|
|
export function RecentlyAddedPage() {
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const searchParams = useSearchParams();
|
|
|
|
const filterState = useMemo(
|
|
() => filterStateFromSearchParams(searchParams),
|
|
[searchParams],
|
|
);
|
|
const filterArgs = useMemo(
|
|
() => shopFilterStateToApiArgs(filterState),
|
|
[filterState],
|
|
);
|
|
|
|
const [sort, setSort] = useState<string>("newest");
|
|
const [filterModalOpen, setFilterModalOpen] = useState(false);
|
|
|
|
const filterOptions = useQuery(api.products.getFilterOptions, {
|
|
recentlyAdded: true,
|
|
});
|
|
const products = useQuery(api.products.listRecentlyAdded, { ...filterArgs });
|
|
|
|
const setFilterStateToUrl = useCallback(
|
|
(state: typeof filterState) => {
|
|
const next = mergeFilterStateIntoSearchParams(
|
|
new URLSearchParams(searchParams.toString()),
|
|
state,
|
|
);
|
|
const q = next.toString();
|
|
router.replace(q ? `${pathname}?${q}` : pathname);
|
|
},
|
|
[pathname, router, searchParams],
|
|
);
|
|
|
|
const handleApplyFiltersFromModal = useCallback(
|
|
(state: typeof filterState) => {
|
|
const next = mergeFilterStateIntoSearchParams(
|
|
new URLSearchParams(searchParams.toString()),
|
|
state,
|
|
);
|
|
const q = next.toString();
|
|
router.replace(q ? `${pathname}?${q}` : pathname);
|
|
setFilterModalOpen(false);
|
|
},
|
|
[pathname, router, searchParams],
|
|
);
|
|
|
|
const handleApplyFilter = useCallback(() => {
|
|
const next = mergeFilterStateIntoSearchParams(
|
|
new URLSearchParams(searchParams.toString()),
|
|
filterState,
|
|
);
|
|
const q = next.toString();
|
|
router.push(q ? `${pathname}?${q}` : pathname);
|
|
setFilterModalOpen(false);
|
|
}, [router, pathname, searchParams, filterState]);
|
|
|
|
const sortedProducts = useMemo(() => {
|
|
if (!products || !Array.isArray(products)) return [];
|
|
const list = [...products] as EnrichedProduct[];
|
|
if (sort === "newest") {
|
|
// Data is already ordered by _creationTime desc from the query
|
|
list.sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0));
|
|
} else if (sort === "price-asc") {
|
|
list.sort((a, b) => (a.variants?.[0]?.price ?? 0) - (b.variants?.[0]?.price ?? 0));
|
|
} else if (sort === "price-desc") {
|
|
list.sort((a, b) => (b.variants?.[0]?.price ?? 0) - (a.variants?.[0]?.price ?? 0));
|
|
}
|
|
return list;
|
|
}, [products, sort]);
|
|
|
|
const handleRetry = useCallback(() => {
|
|
window.location.reload();
|
|
}, []);
|
|
|
|
const isLoading = products === undefined;
|
|
const hasError = products === null;
|
|
const isEmpty = Array.isArray(products) && products.length === 0;
|
|
|
|
return (
|
|
<main className="mx-auto w-full max-w-7xl min-w-0 px-4 py-6 md:px-6 md:py-8">
|
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
<ShopBreadcrumbBar
|
|
items={[
|
|
{ label: "Home", href: "/" },
|
|
{ label: "Shop", href: "/shop" },
|
|
{ label: "Recently Added" },
|
|
]}
|
|
/>
|
|
<ShopToolbar
|
|
sortOptions={SORT_OPTIONS}
|
|
currentSort={sort}
|
|
onSortChange={setSort}
|
|
onOpenFilter={() => setFilterModalOpen(true)}
|
|
resultCount={Array.isArray(products) ? products.length : undefined}
|
|
/>
|
|
</div>
|
|
|
|
<ShopPageBanner
|
|
title="Recently Added"
|
|
subtitle="Explore the latest products added in the last 30 days"
|
|
/>
|
|
|
|
<div className="mt-6 flex flex-col gap-6 lg:flex-row lg:items-start">
|
|
<ShopFilterSidebar
|
|
filterOptions={filterOptions ?? null}
|
|
filterState={filterState}
|
|
onFilterChange={setFilterStateToUrl}
|
|
filterOptionsLoading={filterOptions === undefined}
|
|
/>
|
|
|
|
<div className="min-w-0 flex-1">
|
|
<div className="mt-6">
|
|
{isLoading ? (
|
|
<ShopProductGridSkeleton />
|
|
) : hasError ? (
|
|
<ShopErrorState
|
|
message="Failed to load products."
|
|
onRetry={handleRetry}
|
|
/>
|
|
) : isEmpty ? (
|
|
<ShopEmptyState
|
|
title="No new products yet"
|
|
description="Nothing has been added in the last 30 days. Check back soon!"
|
|
/>
|
|
) : (
|
|
<ShopProductGrid
|
|
products={sortedProducts.map((p) => ({
|
|
...enrichedProductToCardProps(p as EnrichedProduct),
|
|
id: (p as { _id: string })._id,
|
|
}))}
|
|
/>
|
|
)}
|
|
<div className="mt-8">
|
|
<CustomerConfidenceBooster />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<ShopFilterModal
|
|
isOpen={filterModalOpen}
|
|
onClose={() => setFilterModalOpen(false)}
|
|
onApply={handleApplyFilter}
|
|
filterOptions={filterOptions ?? null}
|
|
filterState={filterState}
|
|
onApplyFilters={handleApplyFiltersFromModal}
|
|
filterOptionsLoading={filterOptions === undefined}
|
|
/>
|
|
</main>
|
|
);
|
|
}
|