Template
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:
co-authored by
Claude Opus 5
parent
69f7435d6e
commit
e04f1e0405
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,11 +81,19 @@ export interface Product {
|
||||
|
||||
interface UseProductsOptions {
|
||||
first?: number;
|
||||
/** Cursor from a previous page's `endCursor`; omit for the first page. */
|
||||
after?: string | null;
|
||||
query?: string;
|
||||
sortKey?: 'BEST_SELLING' | 'CREATED_AT' | 'PRICE' | 'TITLE';
|
||||
reverse?: boolean;
|
||||
}
|
||||
|
||||
export interface ProductsPage {
|
||||
products: Product[];
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
}
|
||||
|
||||
interface UseProductsReturn {
|
||||
products: Product[];
|
||||
loading: boolean;
|
||||
@@ -94,18 +102,33 @@ interface UseProductsReturn {
|
||||
}
|
||||
|
||||
// Fetch multiple products
|
||||
export async function getProducts({
|
||||
export async function getProducts(
|
||||
options: UseProductsOptions = {}
|
||||
): Promise<Product[]> {
|
||||
const { products } = await getProductsPage(options);
|
||||
return products;
|
||||
}
|
||||
|
||||
// Same fetch, but keeps the cursor so callers can page through the catalogue.
|
||||
export async function getProductsPage({
|
||||
first = 20,
|
||||
after = null,
|
||||
query = '',
|
||||
sortKey = 'BEST_SELLING',
|
||||
reverse = false,
|
||||
}: UseProductsOptions = {}): Promise<Product[]> {
|
||||
}: UseProductsOptions = {}): Promise<ProductsPage> {
|
||||
const response = await shopifyFetch({
|
||||
query: GET_PRODUCTS_QUERY,
|
||||
variables: { first, query, sortKey, reverse },
|
||||
variables: { first, after, query, sortKey, reverse },
|
||||
});
|
||||
|
||||
return response.data.products.edges.map((edge: { node: Product }) => edge.node);
|
||||
const { edges, pageInfo } = response.data.products;
|
||||
|
||||
return {
|
||||
products: edges.map((edge: { node: Product }) => edge.node),
|
||||
hasNextPage: Boolean(pageInfo?.hasNextPage),
|
||||
endCursor: pageInfo?.endCursor ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch a single product by handle
|
||||
|
||||
Reference in New Issue
Block a user