Files

188 lines
5.5 KiB
TypeScript

"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;
}