Redesign cart and collections, fix product pagination

- Cart drawer: single-column layout matching the site — no bag icon,
  no dividers, square images, tighter type, discount code form,
  estimated total, and a Go to Checkout button
- Cart: per-line loading state so one row's update no longer disables
  every other row or the checkout button
- Discounts: cartDiscountCodesUpdate mutation plus discountCodes on the
  cart fragment, wired to an applyDiscountCode store action
- Products: fix "load more" returning the same first page — the query
  now takes an `after` cursor and getProductsPage exposes pageInfo
- Collections: cards match the product cards (no border, radius, blurb,
  or CTA row) at 4-up on desktop and 2-up on mobile; section headers
  match the products section
- Shop Pay: cart permalink with payment=shop_pay instead of the hosted
  shop-js element
- Buttons converted to the shadcn Button component throughout
- PDP: swipeable image carousel with dots on mobile, sticky info column,
  colour swatches never fall back to variant photos
- Rename the store from Stride to Shop; larger header wordmark

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jWbNNJLksC1QG8z8845FX
This commit is contained in:
Rami Bitar
2026-08-01 12:05:57 -04:00
co-authored by Claude Opus 5
parent 69f7435d6e
commit e04f1e0405
20 changed files with 546 additions and 466 deletions
+38 -46
View File
@@ -2,7 +2,7 @@
import React, { useState, useEffect } from 'react';
import ProductCard from './product-card';
import { getProducts } from '@/hooks/use-shopify-products';
import { getProductsPage } from '@/hooks/use-shopify-products';
import { Button } from '@/components/ui/button';
import { Loader } from '@/app/components/ui/loader';
@@ -64,11 +64,11 @@ const Products: React.FC<ProductsProps> = ({
const [error, setError] = useState<string | null>(null);
const [loadingMore, setLoadingMore] = useState(false);
const [hasMoreProducts, setHasMoreProducts] = useState(true);
const [cursor, setCursor] = useState<string | null>(null);
const fetchProducts = async (
currentProducts: Product[] = [],
loadMore = false
) => {
// Paging is cursor-based: without `after`, Shopify returns the same first
// page every time and "load more" appends nothing.
const fetchProducts = async (loadMore = false) => {
try {
if (loadMore) {
setLoadingMore(true);
@@ -77,27 +77,25 @@ const Products: React.FC<ProductsProps> = ({
setError(null);
}
const newProducts = await getProducts({
const page = await getProductsPage({
first: limit,
after: loadMore ? cursor : null,
sortKey: 'CREATED_AT',
reverse: true,
});
if (loadMore) {
const existingIds = new Set(currentProducts.map((p) => p.id));
const uniqueNewProducts = newProducts.filter(
(p) => !existingIds.has(p.id)
);
setProducts((prev) => {
if (!loadMore) return page.products;
if (uniqueNewProducts.length === 0) {
setHasMoreProducts(false);
} else {
setProducts((prev) => [...prev, ...uniqueNewProducts]);
}
} else {
setProducts(newProducts);
setHasMoreProducts(newProducts.length === limit);
}
const existingIds = new Set(prev.map((p) => p.id));
return [
...prev,
...page.products.filter((p) => !existingIds.has(p.id)),
];
});
setCursor(page.endCursor);
setHasMoreProducts(page.hasNextPage);
} catch (err) {
console.error('Error fetching products:', err);
setError(err instanceof Error ? err.message : 'Failed to load products');
@@ -113,7 +111,7 @@ const Products: React.FC<ProductsProps> = ({
const handleLoadMore = () => {
if (!loadingMore && hasMoreProducts) {
fetchProducts(products, true);
fetchProducts(true);
}
};
@@ -146,7 +144,7 @@ const Products: React.FC<ProductsProps> = ({
if (error || products.length === 0) {
return (
<div className="py-20 bg-zinc-50">
<div className="py-20 bg-background">
<div className="max-w-screen-2xl mx-auto px-8 text-center">
<h2 className="text-2xl md:text-3xl font-normal max-w-3xl mx-auto text-foreground">
{title}
@@ -154,21 +152,20 @@ const Products: React.FC<ProductsProps> = ({
<p className="text-sm font-normal max-w-xl mx-auto text-muted-foreground mt-2.5 mb-12">
{subtitle}
</p>
<div className="mx-auto max-w-sm bg-white border border-border rounded-3xl p-12">
<i className="ri-inbox-line text-6xl text-muted-foreground mb-6 block"></i>
<h3 className="font-semibold text-2xl mb-3 tracking-tight">
{error ? 'Connection Error' : 'Coming Soon'}
</h3>
<p className="text-muted-foreground mb-8 text-base">
{error ||
'Our curated collection is being prepared. Please check back shortly.'}
</p>
{error && (
<Button onClick={() => fetchProducts()} className="btn-modern">
Try Again
</Button>
)}
</div>
<p className="text-sm text-muted-foreground mb-6">
{error ||
'Our curated collection is being prepared. Please check back shortly.'}
</p>
{error && (
<Button
onClick={() => fetchProducts()}
variant="outline"
size="lg"
className="font-normal text-muted-foreground hover:text-foreground"
>
Try again
</Button>
)}
</div>
</div>
);
@@ -195,17 +192,12 @@ const Products: React.FC<ProductsProps> = ({
<Button
onClick={handleLoadMore}
disabled={loadingMore}
variant="outline"
size="lg"
className="btn-modern rounded-2xl px-14 py-7 text-sm tracking-widest font-medium border border-border"
className="font-normal text-muted-foreground hover:text-foreground"
>
{loadingMore ? (
<>
<Loader size={18} className="mr-3" />
LOADING MORE
</>
) : (
'LOAD MORE PRODUCTS'
)}
{loadingMore && <Loader size={16} />}
{loadingMore ? 'Loading' : 'Load more'}
</Button>
</div>
)}