Changes
This commit is contained in:
+16
-7
@@ -5,21 +5,30 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import Index from "./pages/Index";
|
||||
import NotFound from "./pages/NotFound";
|
||||
import { useCartSync } from "./hooks/useCartSync";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function AppContent() {
|
||||
useCartSync();
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
const App = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<Sonner />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<AppContent />
|
||||
</TooltipProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { Check } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Check, Loader2 } from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import AbonnemangForm from "./AbonnemangForm";
|
||||
import { useShopifyProducts } from "@/hooks/useShopifyProducts";
|
||||
import { useCartStore } from "@/stores/cartStore";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const plans = [
|
||||
const planDetails = [
|
||||
{
|
||||
name: "Liten",
|
||||
kgPerWeek: "3 kg",
|
||||
@@ -18,6 +19,7 @@ const plans = [
|
||||
"Veckoleverans",
|
||||
"Betala direkt eller med Klarna",
|
||||
],
|
||||
shopifyHandle: "liten-kakabonnemang-3-kg-vecka",
|
||||
},
|
||||
{
|
||||
name: "Mellan",
|
||||
@@ -29,6 +31,7 @@ const plans = [
|
||||
"Veckoleverans",
|
||||
"Betala direkt eller med Klarna",
|
||||
],
|
||||
shopifyHandle: "mellan-kakabonnemang-5-kg-vecka",
|
||||
},
|
||||
{
|
||||
name: "Stor",
|
||||
@@ -40,18 +43,66 @@ const plans = [
|
||||
"Veckoleverans",
|
||||
"Betala direkt eller med Klarna",
|
||||
],
|
||||
shopifyHandle: "stor-kakabonnemang-10-kg-vecka",
|
||||
},
|
||||
];
|
||||
|
||||
const PlansSection = () => {
|
||||
const [selectedPlan, setSelectedPlan] = useState<string>("");
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [checkoutLoading, setCheckoutLoading] = useState(false);
|
||||
|
||||
const { products, isLoading: productsLoading } = useShopifyProducts("product_type:Abonnemang");
|
||||
const { addItem, getCheckoutUrl, isLoading: cartLoading } = useCartStore();
|
||||
|
||||
const handleSelectPlan = (plan: typeof plans[0]) => {
|
||||
setSelectedPlan(`${plan.name} (${plan.kgPerWeek}/vecka)`);
|
||||
setDialogOpen(true);
|
||||
const handleSelectPlan = async (plan: typeof planDetails[0]) => {
|
||||
// Find the matching Shopify product
|
||||
const shopifyProduct = products.find(p => p.node.handle === plan.shopifyHandle);
|
||||
|
||||
if (!shopifyProduct) {
|
||||
toast.error("Kunde inte hitta produkten");
|
||||
return;
|
||||
}
|
||||
|
||||
const variant = shopifyProduct.node.variants.edges[0]?.node;
|
||||
if (!variant) {
|
||||
toast.error("Ingen variant tillgänglig");
|
||||
return;
|
||||
}
|
||||
|
||||
setCheckoutLoading(true);
|
||||
setSelectedPlan(plan.name);
|
||||
|
||||
try {
|
||||
await addItem({
|
||||
product: shopifyProduct,
|
||||
variantId: variant.id,
|
||||
variantTitle: variant.title,
|
||||
price: variant.price,
|
||||
quantity: 1,
|
||||
selectedOptions: variant.selectedOptions || [],
|
||||
});
|
||||
|
||||
// Small delay to ensure cart is created
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
const checkoutUrl = useCartStore.getState().getCheckoutUrl();
|
||||
if (checkoutUrl) {
|
||||
window.open(checkoutUrl, '_blank');
|
||||
toast.success("Omdirigerar till betalning...");
|
||||
} else {
|
||||
toast.error("Kunde inte skapa checkout");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Checkout error:', error);
|
||||
toast.error("Något gick fel");
|
||||
} finally {
|
||||
setCheckoutLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading = productsLoading || cartLoading || checkoutLoading;
|
||||
|
||||
return (
|
||||
<section id="plans" className="py-24 px-6 bg-background">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
@@ -70,7 +121,7 @@ const PlansSection = () => {
|
||||
|
||||
{/* Plans grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{plans.map((plan, index) => (
|
||||
{planDetails.map((plan, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative bg-card p-8 card-classic rounded-sm"
|
||||
@@ -111,9 +162,17 @@ const PlansSection = () => {
|
||||
{/* CTA Button */}
|
||||
<button
|
||||
onClick={() => handleSelectPlan(plan)}
|
||||
className="block w-full text-center py-3 px-6 rounded-sm font-semibold transition-all duration-300 btn-classic"
|
||||
disabled={isLoading}
|
||||
className="block w-full text-center py-3 px-6 rounded-sm font-semibold transition-all duration-300 btn-classic disabled:opacity-50"
|
||||
>
|
||||
Välj abonnemang
|
||||
{isLoading && selectedPlan === plan.name ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Laddar...
|
||||
</span>
|
||||
) : (
|
||||
"Beställ nu"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -124,16 +183,6 @@ const PlansSection = () => {
|
||||
❦
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order Dialog */}
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent className="max-w-xl max-h-[90vh] overflow-y-auto bg-card">
|
||||
<AbonnemangForm
|
||||
selectedPlan={selectedPlan}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useCartStore } from '@/stores/cartStore';
|
||||
|
||||
export function useCartSync() {
|
||||
const syncCart = useCartStore(state => state.syncCart);
|
||||
|
||||
useEffect(() => {
|
||||
syncCart();
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') syncCart();
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
}, [syncCart]);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { storefrontApiRequest, STOREFRONT_PRODUCTS_QUERY, ShopifyProduct } from '@/lib/shopify';
|
||||
|
||||
export function useShopifyProducts(query?: string) {
|
||||
const [products, setProducts] = useState<ShopifyProduct[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchProducts() {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await storefrontApiRequest(STOREFRONT_PRODUCTS_QUERY, {
|
||||
first: 10,
|
||||
query: query || null
|
||||
});
|
||||
if (data?.data?.products?.edges) {
|
||||
setProducts(data.data.products.edges);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch products:', err);
|
||||
setError('Kunde inte ladda produkter');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchProducts();
|
||||
}, [query]);
|
||||
|
||||
return { products, isLoading, error };
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { toast } from "sonner";
|
||||
|
||||
// Shopify API Configuration
|
||||
const SHOPIFY_API_VERSION = '2025-07';
|
||||
const SHOPIFY_STORE_PERMANENT_DOMAIN = 'sweet-office-treats-2ar62.myshopify.com';
|
||||
const SHOPIFY_STOREFRONT_URL = `https://${SHOPIFY_STORE_PERMANENT_DOMAIN}/api/${SHOPIFY_API_VERSION}/graphql.json`;
|
||||
const SHOPIFY_STOREFRONT_TOKEN = 'd9c2c43fc30d96d1742cc76da58c4ddb';
|
||||
|
||||
export interface ShopifyProduct {
|
||||
node: {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
handle: string;
|
||||
priceRange: {
|
||||
minVariantPrice: {
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
};
|
||||
};
|
||||
images: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
url: string;
|
||||
altText: string | null;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
variants: {
|
||||
edges: Array<{
|
||||
node: {
|
||||
id: string;
|
||||
title: string;
|
||||
price: {
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
};
|
||||
availableForSale: boolean;
|
||||
selectedOptions: Array<{
|
||||
name: string;
|
||||
value: string;
|
||||
}>;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
options: Array<{
|
||||
name: string;
|
||||
values: string[];
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
// Storefront API helper function
|
||||
export async function storefrontApiRequest(query: string, variables: Record<string, unknown> = {}) {
|
||||
const response = await fetch(SHOPIFY_STOREFRONT_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Shopify-Storefront-Access-Token': SHOPIFY_STOREFRONT_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query,
|
||||
variables,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 402) {
|
||||
toast.error("Shopify: Betalning krävs", {
|
||||
description: "Shopify API kräver en aktiv betalplan. Besök admin.shopify.com för att uppgradera.",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.errors) {
|
||||
throw new Error(`Error calling Shopify: ${data.errors.map((e: { message: string }) => e.message).join(', ')}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Fetch products query
|
||||
export const STOREFRONT_PRODUCTS_QUERY = `
|
||||
query GetProducts($first: Int!, $query: String) {
|
||||
products(first: $first, query: $query) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
description
|
||||
handle
|
||||
priceRange {
|
||||
minVariantPrice {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
images(first: 5) {
|
||||
edges {
|
||||
node {
|
||||
url
|
||||
altText
|
||||
}
|
||||
}
|
||||
}
|
||||
variants(first: 10) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
title
|
||||
price {
|
||||
amount
|
||||
currencyCode
|
||||
}
|
||||
availableForSale
|
||||
selectedOptions {
|
||||
name
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
options {
|
||||
name
|
||||
values
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Cart mutations
|
||||
export const CART_QUERY = `
|
||||
query cart($id: ID!) {
|
||||
cart(id: $id) { id totalQuantity }
|
||||
}
|
||||
`;
|
||||
|
||||
export const CART_CREATE_MUTATION = `
|
||||
mutation cartCreate($input: CartInput!) {
|
||||
cartCreate(input: $input) {
|
||||
cart {
|
||||
id
|
||||
checkoutUrl
|
||||
lines(first: 100) { edges { node { id merchandise { ... on ProductVariant { id } } } } }
|
||||
}
|
||||
userErrors { field message }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CART_LINES_ADD_MUTATION = `
|
||||
mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
|
||||
cartLinesAdd(cartId: $cartId, lines: $lines) {
|
||||
cart {
|
||||
id
|
||||
lines(first: 100) { edges { node { id merchandise { ... on ProductVariant { id } } } } }
|
||||
}
|
||||
userErrors { field message }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CART_LINES_UPDATE_MUTATION = `
|
||||
mutation cartLinesUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) {
|
||||
cartLinesUpdate(cartId: $cartId, lines: $lines) {
|
||||
cart { id }
|
||||
userErrors { field message }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CART_LINES_REMOVE_MUTATION = `
|
||||
mutation cartLinesRemove($cartId: ID!, $lineIds: [ID!]!) {
|
||||
cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
|
||||
cart { id }
|
||||
userErrors { field message }
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export function formatCheckoutUrl(checkoutUrl: string): string {
|
||||
try {
|
||||
const url = new URL(checkoutUrl);
|
||||
url.searchParams.set('channel', 'online_store');
|
||||
return url.toString();
|
||||
} catch {
|
||||
return checkoutUrl;
|
||||
}
|
||||
}
|
||||
|
||||
export function isCartNotFoundError(userErrors: Array<{ field: string[] | null; message: string }>): boolean {
|
||||
return userErrors.some(e =>
|
||||
e.message.toLowerCase().includes('cart not found') ||
|
||||
e.message.toLowerCase().includes('does not exist')
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import {
|
||||
storefrontApiRequest,
|
||||
ShopifyProduct,
|
||||
CART_QUERY,
|
||||
CART_CREATE_MUTATION,
|
||||
CART_LINES_ADD_MUTATION,
|
||||
CART_LINES_UPDATE_MUTATION,
|
||||
CART_LINES_REMOVE_MUTATION,
|
||||
formatCheckoutUrl,
|
||||
isCartNotFoundError
|
||||
} from '@/lib/shopify';
|
||||
|
||||
export interface CartItem {
|
||||
lineId: string | null;
|
||||
product: ShopifyProduct;
|
||||
variantId: string;
|
||||
variantTitle: string;
|
||||
price: { amount: string; currencyCode: string };
|
||||
quantity: number;
|
||||
selectedOptions: Array<{ name: string; value: string }>;
|
||||
}
|
||||
|
||||
interface CartStore {
|
||||
items: CartItem[];
|
||||
cartId: string | null;
|
||||
checkoutUrl: string | null;
|
||||
isLoading: boolean;
|
||||
isSyncing: boolean;
|
||||
addItem: (item: Omit<CartItem, 'lineId'>) => Promise<void>;
|
||||
updateQuantity: (variantId: string, quantity: number) => Promise<void>;
|
||||
removeItem: (variantId: string) => Promise<void>;
|
||||
clearCart: () => void;
|
||||
syncCart: () => Promise<void>;
|
||||
getCheckoutUrl: () => string | null;
|
||||
}
|
||||
|
||||
async function createShopifyCart(item: CartItem): Promise<{ cartId: string; checkoutUrl: string; lineId: string } | null> {
|
||||
const data = await storefrontApiRequest(CART_CREATE_MUTATION, {
|
||||
input: { lines: [{ quantity: item.quantity, merchandiseId: item.variantId }] },
|
||||
});
|
||||
|
||||
if (data?.data?.cartCreate?.userErrors?.length > 0) {
|
||||
console.error('Cart creation failed:', data.data.cartCreate.userErrors);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cart = data?.data?.cartCreate?.cart;
|
||||
if (!cart?.checkoutUrl) return null;
|
||||
|
||||
const lineId = cart.lines.edges[0]?.node?.id;
|
||||
if (!lineId) return null;
|
||||
|
||||
return { cartId: cart.id, checkoutUrl: formatCheckoutUrl(cart.checkoutUrl), lineId };
|
||||
}
|
||||
|
||||
async function addLineToShopifyCart(cartId: string, item: CartItem): Promise<{ success: boolean; lineId?: string; cartNotFound?: boolean }> {
|
||||
const data = await storefrontApiRequest(CART_LINES_ADD_MUTATION, {
|
||||
cartId,
|
||||
lines: [{ quantity: item.quantity, merchandiseId: item.variantId }],
|
||||
});
|
||||
|
||||
const userErrors = data?.data?.cartLinesAdd?.userErrors || [];
|
||||
if (isCartNotFoundError(userErrors)) return { success: false, cartNotFound: true };
|
||||
if (userErrors.length > 0) {
|
||||
console.error('Add line failed:', userErrors);
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const lines = data?.data?.cartLinesAdd?.cart?.lines?.edges || [];
|
||||
const newLine = lines.find((l: { node: { id: string; merchandise: { id: string } } }) => l.node.merchandise.id === item.variantId);
|
||||
return { success: true, lineId: newLine?.node?.id };
|
||||
}
|
||||
|
||||
async function updateShopifyCartLine(cartId: string, lineId: string, quantity: number): Promise<{ success: boolean; cartNotFound?: boolean }> {
|
||||
const data = await storefrontApiRequest(CART_LINES_UPDATE_MUTATION, {
|
||||
cartId,
|
||||
lines: [{ id: lineId, quantity }],
|
||||
});
|
||||
|
||||
const userErrors = data?.data?.cartLinesUpdate?.userErrors || [];
|
||||
if (isCartNotFoundError(userErrors)) return { success: false, cartNotFound: true };
|
||||
if (userErrors.length > 0) {
|
||||
console.error('Update line failed:', userErrors);
|
||||
return { success: false };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async function removeLineFromShopifyCart(cartId: string, lineId: string): Promise<{ success: boolean; cartNotFound?: boolean }> {
|
||||
const data = await storefrontApiRequest(CART_LINES_REMOVE_MUTATION, {
|
||||
cartId,
|
||||
lineIds: [lineId],
|
||||
});
|
||||
|
||||
const userErrors = data?.data?.cartLinesRemove?.userErrors || [];
|
||||
if (isCartNotFoundError(userErrors)) return { success: false, cartNotFound: true };
|
||||
if (userErrors.length > 0) {
|
||||
console.error('Remove line failed:', userErrors);
|
||||
return { success: false };
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export const useCartStore = create<CartStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
items: [],
|
||||
cartId: null,
|
||||
checkoutUrl: null,
|
||||
isLoading: false,
|
||||
isSyncing: false,
|
||||
|
||||
addItem: async (item) => {
|
||||
const { items, cartId, clearCart } = get();
|
||||
const existingItem = items.find(i => i.variantId === item.variantId);
|
||||
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
if (!cartId) {
|
||||
const result = await createShopifyCart({ ...item, lineId: null });
|
||||
if (result) {
|
||||
set({
|
||||
cartId: result.cartId,
|
||||
checkoutUrl: result.checkoutUrl,
|
||||
items: [{ ...item, lineId: result.lineId }]
|
||||
});
|
||||
}
|
||||
} else if (existingItem) {
|
||||
const newQuantity = existingItem.quantity + item.quantity;
|
||||
if (!existingItem.lineId) {
|
||||
console.error('Cannot update quantity for item without lineId:', existingItem);
|
||||
return;
|
||||
}
|
||||
const result = await updateShopifyCartLine(cartId, existingItem.lineId, newQuantity);
|
||||
if (result.success) {
|
||||
const currentItems = get().items;
|
||||
set({ items: currentItems.map(i => i.variantId === item.variantId ? { ...i, quantity: newQuantity } : i) });
|
||||
} else if (result.cartNotFound) {
|
||||
clearCart();
|
||||
}
|
||||
} else {
|
||||
const result = await addLineToShopifyCart(cartId, { ...item, lineId: null });
|
||||
if (result.success) {
|
||||
const currentItems = get().items;
|
||||
set({ items: [...currentItems, { ...item, lineId: result.lineId ?? null }] });
|
||||
} else if (result.cartNotFound) {
|
||||
clearCart();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add item:', error);
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
updateQuantity: async (variantId, quantity) => {
|
||||
if (quantity <= 0) {
|
||||
await get().removeItem(variantId);
|
||||
return;
|
||||
}
|
||||
|
||||
const { items, cartId, clearCart } = get();
|
||||
const item = items.find(i => i.variantId === variantId);
|
||||
if (!item?.lineId || !cartId) return;
|
||||
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const result = await updateShopifyCartLine(cartId, item.lineId, quantity);
|
||||
if (result.success) {
|
||||
const currentItems = get().items;
|
||||
set({ items: currentItems.map(i => i.variantId === variantId ? { ...i, quantity } : i) });
|
||||
} else if (result.cartNotFound) {
|
||||
clearCart();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update quantity:', error);
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
removeItem: async (variantId) => {
|
||||
const { items, cartId, clearCart } = get();
|
||||
const item = items.find(i => i.variantId === variantId);
|
||||
if (!item?.lineId || !cartId) return;
|
||||
|
||||
set({ isLoading: true });
|
||||
try {
|
||||
const result = await removeLineFromShopifyCart(cartId, item.lineId);
|
||||
if (result.success) {
|
||||
const currentItems = get().items;
|
||||
const newItems = currentItems.filter(i => i.variantId !== variantId);
|
||||
newItems.length === 0 ? clearCart() : set({ items: newItems });
|
||||
} else if (result.cartNotFound) {
|
||||
clearCart();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to remove item:', error);
|
||||
} finally {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
clearCart: () => set({ items: [], cartId: null, checkoutUrl: null }),
|
||||
getCheckoutUrl: () => get().checkoutUrl,
|
||||
|
||||
syncCart: async () => {
|
||||
const { cartId, isSyncing, clearCart } = get();
|
||||
if (!cartId || isSyncing) return;
|
||||
|
||||
set({ isSyncing: true });
|
||||
try {
|
||||
const data = await storefrontApiRequest(CART_QUERY, { id: cartId });
|
||||
if (!data) return;
|
||||
const cart = data?.data?.cart;
|
||||
if (!cart || cart.totalQuantity === 0) clearCart();
|
||||
} catch (error) {
|
||||
console.error('Failed to sync cart with Shopify:', error);
|
||||
} finally {
|
||||
set({ isSyncing: false });
|
||||
}
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'shopify-cart',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
partialize: (state) => ({ items: state.items, cartId: state.cartId, checkoutUrl: state.checkoutUrl }),
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user