Add guest cart drawer, Continue handoff, and shared customer login.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
ad05241a77
commit
16340c3b48
+187
@@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import type { GuestCartLine } from "./customer";
|
||||
|
||||
export type CartItem = GuestCartLine;
|
||||
|
||||
const KEY = "meshkee-guest-cart";
|
||||
const LEGACY_KEY = "salar-cart";
|
||||
const CART_EVENT = "cart-updated";
|
||||
|
||||
function cookieDomain(): string | undefined {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
const host = window.location.hostname.toLowerCase();
|
||||
if (host === "localhost" || host === "127.0.0.1") return undefined;
|
||||
const parts = host.split(".").filter(Boolean);
|
||||
if (parts.length < 2) return undefined;
|
||||
return `.${parts.slice(-2).join(".")}`;
|
||||
}
|
||||
|
||||
function parseItems(raw: string | null): CartItem[] {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed)) return [];
|
||||
return parsed
|
||||
.map((entry) => normalizeLine(entry))
|
||||
.filter((item): item is CartItem => Boolean(item));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeLine(entry: unknown): CartItem | null {
|
||||
if (!entry || typeof entry !== "object") return null;
|
||||
const item = entry as Record<string, unknown>;
|
||||
const id = String(item.id ?? item.variantId ?? item.storeItemVariantId ?? "").trim();
|
||||
if (!id) return null;
|
||||
const quantity = Math.max(1, Math.floor(Number(item.quantity ?? item.qty) || 1));
|
||||
const price = Number(item.price) || 0;
|
||||
const name = String(item.name ?? "محصول");
|
||||
const slug = item.slug ? String(item.slug) : undefined;
|
||||
const image = item.image != null && item.image !== "" ? String(item.image) : undefined;
|
||||
return { id, name, ...(slug ? { slug } : {}), price, quantity, ...(image ? { image } : {}) };
|
||||
}
|
||||
|
||||
function writeSharedCookie(items: CartItem[]) {
|
||||
if (typeof document === "undefined") return;
|
||||
const domain = cookieDomain();
|
||||
const maxAge = 60 * 60 * 24 * 30;
|
||||
const encoded = encodeURIComponent(
|
||||
JSON.stringify(
|
||||
items.map(({ id, name, slug, price, quantity }) => ({
|
||||
id,
|
||||
name,
|
||||
slug,
|
||||
price,
|
||||
quantity,
|
||||
image: null as string | null,
|
||||
})),
|
||||
),
|
||||
);
|
||||
if (encoded.length > 3500) return;
|
||||
let cookie = `${KEY}=${encoded}; path=/; max-age=${maxAge}; SameSite=Lax`;
|
||||
if (domain) cookie += `; domain=${domain}`;
|
||||
document.cookie = cookie;
|
||||
}
|
||||
|
||||
function notify() {
|
||||
window.dispatchEvent(new Event(CART_EVENT));
|
||||
}
|
||||
|
||||
export function getCart(): CartItem[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
const primary = parseItems(window.localStorage.getItem(KEY));
|
||||
if (primary.length > 0) return primary;
|
||||
const legacy = parseItems(window.localStorage.getItem(LEGACY_KEY));
|
||||
if (legacy.length > 0) {
|
||||
writeCart(legacy);
|
||||
window.localStorage.removeItem(LEGACY_KEY);
|
||||
return legacy;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function writeCart(items: CartItem[]) {
|
||||
if (typeof window === "undefined") return;
|
||||
window.localStorage.setItem(KEY, JSON.stringify(items));
|
||||
window.localStorage.removeItem(LEGACY_KEY);
|
||||
try {
|
||||
writeSharedCookie(items);
|
||||
} catch {
|
||||
// ignore cookie failures
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
export function cartCount() {
|
||||
return getCart().reduce((sum, item) => sum + item.quantity, 0);
|
||||
}
|
||||
|
||||
export function cartTotal() {
|
||||
return getCart().reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||
}
|
||||
|
||||
export function addToCart(item: CartItem) {
|
||||
const cart = getCart();
|
||||
const quantity = Math.max(1, Math.floor(item.quantity) || 1);
|
||||
const index = cart.findIndex((entry) => entry.id === item.id);
|
||||
if (index >= 0) {
|
||||
cart[index] = {
|
||||
...cart[index],
|
||||
...item,
|
||||
quantity: cart[index].quantity + quantity,
|
||||
};
|
||||
} else {
|
||||
cart.push({ ...item, quantity });
|
||||
}
|
||||
writeCart(cart);
|
||||
}
|
||||
|
||||
export function setCartQuantity(id: string, quantity: number) {
|
||||
if (quantity < 1) {
|
||||
writeCart(getCart().filter((item) => item.id !== id));
|
||||
return;
|
||||
}
|
||||
writeCart(getCart().map((item) => (item.id === id ? { ...item, quantity } : item)));
|
||||
}
|
||||
|
||||
export function removeFromCart(id: string) {
|
||||
writeCart(getCart().filter((item) => item.id !== id));
|
||||
}
|
||||
|
||||
type CartContextValue = {
|
||||
items: CartItem[];
|
||||
itemCount: number;
|
||||
total: number;
|
||||
addItem: (item: CartItem) => void;
|
||||
removeItem: (id: string) => void;
|
||||
setQuantity: (id: string, quantity: number) => void;
|
||||
};
|
||||
|
||||
const CartContext = createContext<CartContextValue | null>(null);
|
||||
|
||||
export function CartProvider({ children }: { children: ReactNode }) {
|
||||
const [items, setItems] = useState<CartItem[]>([]);
|
||||
|
||||
const sync = useCallback(() => setItems(getCart()), []);
|
||||
|
||||
useEffect(() => {
|
||||
sync();
|
||||
window.addEventListener(CART_EVENT, sync);
|
||||
window.addEventListener("storage", sync);
|
||||
return () => {
|
||||
window.removeEventListener(CART_EVENT, sync);
|
||||
window.removeEventListener("storage", sync);
|
||||
};
|
||||
}, [sync]);
|
||||
|
||||
const addItem = useCallback((item: CartItem) => addToCart(item), []);
|
||||
const removeItem = useCallback((id: string) => removeFromCart(id), []);
|
||||
const setQuantity = useCallback((id: string, quantity: number) => setCartQuantity(id, quantity), []);
|
||||
|
||||
const itemCount = useMemo(() => items.reduce((sum, item) => sum + item.quantity, 0), [items]);
|
||||
const total = useMemo(() => items.reduce((sum, item) => sum + item.price * item.quantity, 0), [items]);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ items, itemCount, total, addItem, removeItem, setQuantity }),
|
||||
[items, itemCount, total, addItem, removeItem, setQuantity],
|
||||
);
|
||||
|
||||
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
|
||||
}
|
||||
|
||||
export function useCart() {
|
||||
const ctx = useContext(CartContext);
|
||||
if (!ctx) throw new Error("useCart must be used within CartProvider");
|
||||
return ctx;
|
||||
}
|
||||
Reference in New Issue
Block a user