Update React Editor integration and clean up template
This commit is contained in:
@@ -12,8 +12,6 @@ import {
|
||||
} from '@/graphql/cart';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface CartLineInput {
|
||||
merchandiseId: string;
|
||||
quantity: number;
|
||||
@@ -91,13 +89,7 @@ export interface Cart {
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Shopify API functions ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Every cart mutation returns the same `{ cart, userErrors }` payload, and both
|
||||
* the payload and the cart inside it are nullable — Shopify returns no cart when
|
||||
* the mutation could not be applied. Callers want a cart or an exception.
|
||||
*/
|
||||
/** Normalizes nullable Shopify cart mutation payloads. */
|
||||
function unwrapCartPayload<T>(
|
||||
payload:
|
||||
| {
|
||||
@@ -200,12 +192,9 @@ export function redirectToCheckout(checkoutUrl: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Zustand Store ───────────────────────────────────────────────────
|
||||
|
||||
const CART_ID_KEY = 'cartId';
|
||||
|
||||
interface CartState {
|
||||
// State
|
||||
isOpen: boolean;
|
||||
cartId: string | null;
|
||||
cart: Cart | null;
|
||||
@@ -213,10 +202,6 @@ interface CartState {
|
||||
error: string | null;
|
||||
_initialized: boolean;
|
||||
|
||||
// Computed (derived in the hook)
|
||||
// items, itemCount, totalAmount, checkoutUrl
|
||||
|
||||
// Actions
|
||||
openCart: () => void;
|
||||
closeCart: () => void;
|
||||
toggleCart: () => void;
|
||||
@@ -229,7 +214,6 @@ interface CartState {
|
||||
}
|
||||
|
||||
export const useCartStore = create<CartState>((set, get) => ({
|
||||
// Initial state
|
||||
isOpen: false,
|
||||
cartId: null,
|
||||
cart: null,
|
||||
@@ -237,12 +221,10 @@ export const useCartStore = create<CartState>((set, get) => ({
|
||||
error: null,
|
||||
_initialized: false,
|
||||
|
||||
// UI actions
|
||||
openCart: () => set({ isOpen: true }),
|
||||
closeCart: () => set({ isOpen: false }),
|
||||
toggleCart: () => set((s) => ({ isOpen: !s.isOpen })),
|
||||
|
||||
// Initialize cart from localStorage
|
||||
initCart: () => {
|
||||
if (get()._initialized) return;
|
||||
set({ _initialized: true });
|
||||
@@ -276,12 +258,10 @@ export const useCartStore = create<CartState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
// Add item to cart (creates cart if needed)
|
||||
addItem: async (variantId: string, quantity: number = 1) => {
|
||||
try {
|
||||
set({ loading: true, error: null });
|
||||
|
||||
// Get or create cart
|
||||
let currentCartId = get().cartId;
|
||||
if (!currentCartId) {
|
||||
const storedCartId = localStorage.getItem(CART_ID_KEY);
|
||||
@@ -309,7 +289,6 @@ export const useCartStore = create<CartState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
// Remove item from cart
|
||||
removeItem: async (lineId: string) => {
|
||||
const { cartId } = get();
|
||||
if (!cartId) throw new Error('No cart exists');
|
||||
@@ -327,7 +306,6 @@ export const useCartStore = create<CartState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
// Update item quantity
|
||||
updateItemQuantity: async (lineId: string, quantity: number) => {
|
||||
const { cartId, removeItem } = get();
|
||||
if (!cartId) throw new Error('No cart exists');
|
||||
@@ -351,8 +329,7 @@ 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.
|
||||
// Invalid discount codes return `applicable: false` instead of an error.
|
||||
applyDiscountCode: async (code: string) => {
|
||||
const { cartId } = get();
|
||||
if (!cartId) throw new Error('No cart exists');
|
||||
@@ -374,7 +351,6 @@ export const useCartStore = create<CartState>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
// Refresh cart from Shopify
|
||||
refreshCart: async () => {
|
||||
const storedCartId = localStorage.getItem(CART_ID_KEY);
|
||||
if (!storedCartId) {
|
||||
@@ -403,12 +379,9 @@ export const useCartStore = create<CartState>((set, get) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// ─── Hook (backwards-compatible API) ─────────────────────────────────
|
||||
|
||||
export function useShopifyCart() {
|
||||
const store = useCartStore();
|
||||
|
||||
// Initialize cart safely in an effect, not during render
|
||||
useEffect(() => {
|
||||
if (!store._initialized) {
|
||||
store.initCart();
|
||||
|
||||
@@ -4,26 +4,26 @@ import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
getCollections,
|
||||
getCollectionProducts,
|
||||
} from '@/services/shopify/catalog';
|
||||
} from '@/services/shopify/shop';
|
||||
|
||||
export {
|
||||
getCollections,
|
||||
getCollectionProducts,
|
||||
getCollectionProductsPage,
|
||||
} from '@/services/shopify/catalog';
|
||||
} from '@/services/shopify/shop';
|
||||
export type {
|
||||
Collection,
|
||||
CollectionWithProducts,
|
||||
CollectionSortKey,
|
||||
CollectionProductsPage,
|
||||
ProductFilterFacet,
|
||||
} from '@/services/shopify/catalog';
|
||||
} from '@/services/shopify/shop';
|
||||
|
||||
import type {
|
||||
Collection,
|
||||
CollectionWithProducts,
|
||||
CollectionSortKey,
|
||||
} from '@/services/shopify/catalog';
|
||||
} from '@/services/shopify/shop';
|
||||
|
||||
interface UseCollectionProductsOptions {
|
||||
first?: number;
|
||||
@@ -33,7 +33,6 @@ interface UseCollectionProductsOptions {
|
||||
filterInputs?: string[];
|
||||
}
|
||||
|
||||
// Hook for fetching all collections
|
||||
export function useCollections(first = 50) {
|
||||
const [collections, setCollections] = useState<Collection[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -60,9 +59,7 @@ export function useCollections(first = 50) {
|
||||
return { collections, loading, error, refetch: fetchCollections };
|
||||
}
|
||||
|
||||
// Deferred variant of useCollections: nothing is requested until `load` runs,
|
||||
// so a menu can hold off until it's actually opened. The fetch happens once —
|
||||
// re-opening reuses what's already in state.
|
||||
// Fetch once on demand for menus that may never open.
|
||||
export function useCollectionsOnDemand(first = 50) {
|
||||
const [collections, setCollections] = useState<Collection[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -79,7 +76,6 @@ export function useCollectionsOnDemand(first = 50) {
|
||||
setCollections(await getCollections(first));
|
||||
} catch (err) {
|
||||
console.error('Error fetching collections:', err);
|
||||
// Let the next open (or a retry) try again.
|
||||
requested.current = false;
|
||||
setError(err instanceof Error ? err.message : 'Failed to load collections');
|
||||
} finally {
|
||||
@@ -90,7 +86,6 @@ export function useCollectionsOnDemand(first = 50) {
|
||||
return { collections, loading, error, load };
|
||||
}
|
||||
|
||||
// Hook for fetching products in a collection
|
||||
export function useCollectionProducts(
|
||||
handle: string | null,
|
||||
options: UseCollectionProductsOptions = {}
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface ShopPolicy {
|
||||
url: string;
|
||||
}
|
||||
|
||||
// Handles Shopify uses for each policy — also the routes under /policies/[handle].
|
||||
export const POLICY_HANDLES = [
|
||||
'terms-of-service',
|
||||
'privacy-policy',
|
||||
@@ -21,8 +20,6 @@ export const POLICY_HANDLES = [
|
||||
'subscription-policy',
|
||||
] as const;
|
||||
|
||||
// Policies change rarely, so this reads through the cached client (revalidated
|
||||
// hourly) rather than the no-store one used for carts and products.
|
||||
export async function getShopPolicies(): Promise<ShopPolicy[]> {
|
||||
try {
|
||||
const data = unwrapStorefrontResult(
|
||||
@@ -34,7 +31,6 @@ export async function getShopPolicies(): Promise<ShopPolicy[]> {
|
||||
(policy): policy is ShopPolicy => Boolean(policy?.handle)
|
||||
);
|
||||
} catch (err) {
|
||||
// A storefront without policies configured shouldn't break the footer.
|
||||
console.error('Failed to load shop policies:', err);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -5,24 +5,22 @@ import {
|
||||
getProducts,
|
||||
getProduct,
|
||||
getProductRecommendations,
|
||||
} from '@/services/shopify/catalog';
|
||||
} from '@/services/shopify/shop';
|
||||
|
||||
// Pure fetchers live in services/shopify/catalog so server code can use them
|
||||
// too; re-exported here so existing imports keep working.
|
||||
export {
|
||||
getProducts,
|
||||
getProductsPage,
|
||||
getProduct,
|
||||
getProductRecommendations,
|
||||
} from '@/services/shopify/catalog';
|
||||
} from '@/services/shopify/shop';
|
||||
export type {
|
||||
Product,
|
||||
ProductOption,
|
||||
ProductOptionValue,
|
||||
ProductsPage,
|
||||
} from '@/services/shopify/catalog';
|
||||
} from '@/services/shopify/shop';
|
||||
|
||||
import type { Product } from '@/services/shopify/catalog';
|
||||
import type { Product } from '@/services/shopify/shop';
|
||||
|
||||
interface UseProductsOptions {
|
||||
first?: number;
|
||||
@@ -39,7 +37,6 @@ interface UseProductsReturn {
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Hook for fetching multiple products
|
||||
export function useProducts(options: UseProductsOptions = {}): UseProductsReturn {
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -66,7 +63,6 @@ export function useProducts(options: UseProductsOptions = {}): UseProductsReturn
|
||||
return { products, loading, error, refetch: fetchProducts };
|
||||
}
|
||||
|
||||
// Hook for fetching a single product
|
||||
export function useProduct(handle: string | null) {
|
||||
const [product, setProduct] = useState<Product | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -101,7 +97,6 @@ export function useProduct(handle: string | null) {
|
||||
return { product, loading, error, refetch: fetchProduct };
|
||||
}
|
||||
|
||||
// Hook for fetching product recommendations
|
||||
export function useProductRecommendations(productId: string | null) {
|
||||
const [recommendations, setRecommendations] = useState<Product[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// Re-exported from the server-safe catalogue module so both client components
|
||||
// and Route Handlers can search the storefront.
|
||||
export { searchProducts, searchSuggestions } from '@/services/shopify/catalog';
|
||||
export { searchProducts, searchSuggestions } from '@/services/shopify/shop';
|
||||
export type {
|
||||
SearchSortKey,
|
||||
SearchFilter,
|
||||
SearchFilterValue,
|
||||
SearchProductsResult,
|
||||
SearchSuggestion,
|
||||
} from '@/services/shopify/catalog';
|
||||
} from '@/services/shopify/shop';
|
||||
|
||||
Reference in New Issue
Block a user