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
+50
View File
@@ -7,6 +7,7 @@ import {
ADD_CART_LINES_MUTATION,
UPDATE_CART_LINES_MUTATION,
REMOVE_CART_LINES_MUTATION,
UPDATE_CART_DISCOUNT_CODES_MUTATION,
GET_CART_QUERY,
} from '@/graphql/cart';
import { useEffect } from 'react';
@@ -59,10 +60,16 @@ interface CartLine {
};
}
export interface CartDiscountCode {
code: string;
applicable: boolean;
}
export interface Cart {
id: string;
checkoutUrl: string;
totalQuantity: number;
discountCodes?: CartDiscountCode[];
cost: {
subtotalAmount: {
amount: string;
@@ -147,6 +154,24 @@ async function removeCartLinesApi(
return response.data.cartLinesRemove.cart;
}
async function updateCartDiscountCodesApi(
cartId: string,
discountCodes: string[]
): Promise<Cart> {
const response = await shopifyFetch({
query: UPDATE_CART_DISCOUNT_CODES_MUTATION,
variables: { cartId, discountCodes },
});
if (response.data.cartDiscountCodesUpdate.userErrors.length > 0) {
throw new Error(
response.data.cartDiscountCodesUpdate.userErrors[0].message
);
}
return response.data.cartDiscountCodesUpdate.cart;
}
async function getCartApi(cartId: string): Promise<Cart | null> {
const response = await shopifyFetch({
query: GET_CART_QUERY,
@@ -186,6 +211,7 @@ interface CartState {
addItem: (variantId: string, quantity?: number) => Promise<Cart>;
removeItem: (lineId: string) => Promise<Cart>;
updateItemQuantity: (lineId: string, quantity: number) => Promise<Cart>;
applyDiscountCode: (code: string) => Promise<Cart>;
refreshCart: () => Promise<void>;
}
@@ -312,6 +338,29 @@ export const useCartStore = create<CartState>((set, get) => ({
}
},
// Apply a discount code. Shopify accepts unknown codes and reports them back
// as `applicable: false`, so callers should check the returned cart.
applyDiscountCode: async (code: string) => {
const { cartId } = get();
if (!cartId) throw new Error('No cart exists');
try {
set({ loading: true, error: null });
const trimmed = code.trim();
const updatedCart = await updateCartDiscountCodesApi(
cartId,
trimmed ? [trimmed] : []
);
set({ cart: updatedCart, loading: false });
return updatedCart;
} catch (err) {
const errorMessage =
err instanceof Error ? err.message : 'Failed to apply discount code';
set({ error: errorMessage, loading: false });
throw err;
}
},
// Refresh cart from Shopify
refreshCart: async () => {
const storedCartId = localStorage.getItem(CART_ID_KEY);
@@ -374,6 +423,7 @@ export function useShopifyCart() {
addItem: store.addItem,
removeItem: store.removeItem,
updateItemQuantity: store.updateItemQuantity,
applyDiscountCode: store.applyDiscountCode,
refreshCart: store.refreshCart,
};
}