@aurora-studio/starter-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/components/AddToCartButton.d.ts +15 -0
- package/dist/components/AddToCartButton.d.ts.map +1 -0
- package/dist/components/AddToCartButton.js +46 -0
- package/dist/components/AddToCartFly.d.ts +8 -0
- package/dist/components/AddToCartFly.d.ts.map +1 -0
- package/dist/components/AddToCartFly.js +33 -0
- package/dist/components/AuthProvider.d.ts +23 -0
- package/dist/components/AuthProvider.d.ts.map +1 -0
- package/dist/components/AuthProvider.js +79 -0
- package/dist/components/CartLink.d.ts +2 -0
- package/dist/components/CartLink.d.ts.map +1 -0
- package/dist/components/CartLink.js +30 -0
- package/dist/components/CartProvider.d.ts +30 -0
- package/dist/components/CartProvider.d.ts.map +1 -0
- package/dist/components/CartProvider.js +125 -0
- package/dist/components/CatalogueEmptyState.d.ts +11 -0
- package/dist/components/CatalogueEmptyState.d.ts.map +1 -0
- package/dist/components/CatalogueEmptyState.js +12 -0
- package/dist/components/CatalogueFilters.d.ts +24 -0
- package/dist/components/CatalogueFilters.d.ts.map +1 -0
- package/dist/components/CatalogueFilters.js +88 -0
- package/dist/components/CheckoutButton.d.ts +2 -0
- package/dist/components/CheckoutButton.d.ts.map +1 -0
- package/dist/components/CheckoutButton.js +70 -0
- package/dist/components/ConditionalHolmesScript.d.ts +7 -0
- package/dist/components/ConditionalHolmesScript.d.ts.map +1 -0
- package/dist/components/ConditionalHolmesScript.js +17 -0
- package/dist/components/FloatingLabelInput.d.ts +7 -0
- package/dist/components/FloatingLabelInput.d.ts.map +1 -0
- package/dist/components/FloatingLabelInput.js +13 -0
- package/dist/components/HolmesHomeRefresher.d.ts +8 -0
- package/dist/components/HolmesHomeRefresher.d.ts.map +1 -0
- package/dist/components/HolmesHomeRefresher.js +19 -0
- package/dist/components/HolmesProductViewTracker.d.ts +4 -0
- package/dist/components/HolmesProductViewTracker.d.ts.map +1 -0
- package/dist/components/HolmesProductViewTracker.js +39 -0
- package/dist/components/HolmesSprinkleIcon.d.ts +8 -0
- package/dist/components/HolmesSprinkleIcon.d.ts.map +1 -0
- package/dist/components/HolmesSprinkleIcon.js +9 -0
- package/dist/components/HolmesTidbits.d.ts +13 -0
- package/dist/components/HolmesTidbits.d.ts.map +1 -0
- package/dist/components/HolmesTidbits.js +33 -0
- package/dist/components/ProductCardSkeleton.d.ts +3 -0
- package/dist/components/ProductCardSkeleton.d.ts.map +1 -0
- package/dist/components/ProductCardSkeleton.js +5 -0
- package/dist/components/ProductDetailTabs.d.ts +4 -0
- package/dist/components/ProductDetailTabs.d.ts.map +1 -0
- package/dist/components/ProductDetailTabs.js +29 -0
- package/dist/components/ProductImage.d.ts +27 -0
- package/dist/components/ProductImage.d.ts.map +1 -0
- package/dist/components/ProductImage.js +33 -0
- package/dist/components/ProductImageGallery.d.ts +6 -0
- package/dist/components/ProductImageGallery.d.ts.map +1 -0
- package/dist/components/ProductImageGallery.js +25 -0
- package/dist/components/SearchDropdown.d.ts +11 -0
- package/dist/components/SearchDropdown.d.ts.map +1 -0
- package/dist/components/SearchDropdown.js +145 -0
- package/dist/components/SmartCartPanel.d.ts +3 -0
- package/dist/components/SmartCartPanel.d.ts.map +1 -0
- package/dist/components/SmartCartPanel.js +19 -0
- package/dist/components/SortDropdown.d.ts +8 -0
- package/dist/components/SortDropdown.d.ts.map +1 -0
- package/dist/components/SortDropdown.js +25 -0
- package/dist/components/StoreConfigContext.d.ts +6 -0
- package/dist/components/StoreConfigContext.d.ts.map +1 -0
- package/dist/components/StoreConfigContext.js +26 -0
- package/dist/components/StoreContext.d.ts +25 -0
- package/dist/components/StoreContext.d.ts.map +1 -0
- package/dist/components/StoreContext.js +73 -0
- package/dist/components/StoreContextBar.d.ts +2 -0
- package/dist/components/StoreContextBar.d.ts.map +1 -0
- package/dist/components/StoreContextBar.js +12 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/lib/aurora.d.ts +119 -0
- package/dist/lib/aurora.d.ts.map +1 -0
- package/dist/lib/aurora.js +235 -0
- package/dist/lib/format-price.d.ts +10 -0
- package/dist/lib/format-price.d.ts.map +1 -0
- package/dist/lib/format-price.js +18 -0
- package/dist/lib/holmes-events.d.ts +53 -0
- package/dist/lib/holmes-events.d.ts.map +1 -0
- package/dist/lib/holmes-events.js +73 -0
- package/dist/lib/image-url.d.ts +23 -0
- package/dist/lib/image-url.d.ts.map +1 -0
- package/dist/lib/image-url.js +70 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +9 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# @aurora-studio/starter-core
|
|
2
|
+
|
|
3
|
+
Shared **Aurora storefront** primitives for Hippo templates: SDK helpers (`lib/aurora`), Holmes event bridge, formatting, and reusable commerce UI (cart, product images, search, auth, store context).
|
|
4
|
+
|
|
5
|
+
Templates depend on this package and `@aurora-studio/sdk`. See each `aurora-hippo-*` repo for app wiring (`next.config` must `transpilePackages: ["@aurora-studio/starter-core", "@aurora-studio/sdk"]`).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface AddToCartButtonProps {
|
|
2
|
+
recordId: string;
|
|
3
|
+
tableSlug: string;
|
|
4
|
+
name: string;
|
|
5
|
+
unitAmount: number;
|
|
6
|
+
/** Variable-weight product: show weight input, unitAmount = price_per_unit (cents) */
|
|
7
|
+
sellByWeight?: boolean;
|
|
8
|
+
unit?: string;
|
|
9
|
+
/** Product image URL for basket display */
|
|
10
|
+
imageUrl?: string | null;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function AddToCartButton({ recordId, tableSlug, name, unitAmount, sellByWeight, unit, imageUrl, className, }: AddToCartButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=AddToCartButton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AddToCartButton.d.ts","sourceRoot":"","sources":["../../src/components/AddToCartButton.tsx"],"names":[],"mappings":"AAOA,UAAU,oBAAoB;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,sFAAsF;IACtF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,SAAS,EACT,IAAI,EACJ,UAAU,EACV,YAAY,EACZ,IAAW,EACX,QAAQ,EACR,SAAS,GACV,EAAE,oBAAoB,2CA4FtB"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Check } from "lucide-react";
|
|
5
|
+
import { useCart } from "./CartProvider";
|
|
6
|
+
import { useAddToCartFly } from "./AddToCartFly";
|
|
7
|
+
export function AddToCartButton({ recordId, tableSlug, name, unitAmount, sellByWeight, unit = "kg", imageUrl, className, }) {
|
|
8
|
+
const { items, addItem } = useCart();
|
|
9
|
+
const flyCtx = useAddToCartFly();
|
|
10
|
+
const [weight, setWeight] = useState("1");
|
|
11
|
+
const cartId = `${tableSlug}:${recordId}`;
|
|
12
|
+
const inCart = items.some((i) => i.id === cartId);
|
|
13
|
+
if (sellByWeight) {
|
|
14
|
+
const handleAdd = (e) => {
|
|
15
|
+
const w = parseFloat(weight);
|
|
16
|
+
if (!Number.isFinite(w) || w <= 0)
|
|
17
|
+
return;
|
|
18
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
19
|
+
flyCtx?.triggerFly(imageUrl ?? null, rect);
|
|
20
|
+
addItem({
|
|
21
|
+
recordId,
|
|
22
|
+
tableSlug,
|
|
23
|
+
name,
|
|
24
|
+
unitAmount,
|
|
25
|
+
quantity: w,
|
|
26
|
+
sellByWeight: true,
|
|
27
|
+
unit,
|
|
28
|
+
imageUrl,
|
|
29
|
+
});
|
|
30
|
+
};
|
|
31
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("input", { type: "number", step: "0.1", min: "0.1", value: weight, onChange: (e) => setWeight(e.target.value), className: "w-20 px-2 py-2 rounded-lg border border-aurora-border bg-aurora-surface text-aurora-text" }), _jsx("span", { className: "text-aurora-muted", children: unit })] }), _jsx("button", { type: "button", onClick: handleAdd, disabled: inCart, className: inCart
|
|
32
|
+
? `inline-flex items-center gap-1.5 text-aurora-primary font-medium cursor-default ${className ?? ""}`.trim()
|
|
33
|
+
: className ??
|
|
34
|
+
"h-12 px-4 rounded-xl bg-aurora-primary text-white font-semibold hover:bg-aurora-primary-dark transition-colors", children: inCart ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "w-4 h-4 shrink-0", "aria-hidden": true }), "Added"] })) : ("Add to cart") })] }));
|
|
35
|
+
}
|
|
36
|
+
const handleAdd = (e) => {
|
|
37
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
38
|
+
flyCtx?.triggerFly(imageUrl ?? null, rect);
|
|
39
|
+
addItem({ recordId, tableSlug, name, unitAmount, imageUrl });
|
|
40
|
+
};
|
|
41
|
+
const baseClass = className ??
|
|
42
|
+
"h-12 px-4 rounded-xl bg-aurora-primary text-white font-semibold hover:bg-aurora-primary-dark transition-colors";
|
|
43
|
+
return (_jsx("button", { type: "button", onClick: handleAdd, disabled: inCart, className: inCart
|
|
44
|
+
? `inline-flex items-center gap-1.5 text-aurora-primary font-medium cursor-default ${className ?? ""}`.trim()
|
|
45
|
+
: baseClass, children: inCart ? (_jsxs(_Fragment, { children: [_jsx(Check, { className: "w-4 h-4 shrink-0", "aria-hidden": true }), "Added"] })) : ("Add to cart") }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function useAddToCartFly(): {
|
|
2
|
+
triggerFly: (imageUrl: string | null, fromRect: DOMRect) => void;
|
|
3
|
+
setCartRef: (el: HTMLAnchorElement | null) => void;
|
|
4
|
+
} | null;
|
|
5
|
+
export declare function AddToCartFlyProvider({ children }: {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=AddToCartFly.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AddToCartFly.d.ts","sourceRoot":"","sources":["../../src/components/AddToCartFly.tsx"],"names":[],"mappings":"AAiBA,wBAAgB,eAAe;gBAJjB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,OAAO,KAAK,IAAI;gBACpD,CAAC,EAAE,EAAE,iBAAiB,GAAG,IAAI,KAAK,IAAI;SAMnD;AAED,wBAAgB,oBAAoB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAAE,2CAiD/E"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useCallback, useState, useRef, useEffect } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
const AddToCartFlyContext = createContext(null);
|
|
6
|
+
export function useAddToCartFly() {
|
|
7
|
+
const ctx = useContext(AddToCartFlyContext);
|
|
8
|
+
return ctx;
|
|
9
|
+
}
|
|
10
|
+
export function AddToCartFlyProvider({ children }) {
|
|
11
|
+
const [fly, setFly] = useState(null);
|
|
12
|
+
const cartRef = useRef(null);
|
|
13
|
+
const setCartRef = useCallback((el) => {
|
|
14
|
+
cartRef.current = el;
|
|
15
|
+
}, []);
|
|
16
|
+
const triggerFly = useCallback((imageUrl, fromRect) => {
|
|
17
|
+
const toRect = cartRef.current?.getBoundingClientRect() ?? new DOMRect(0, 0, 0, 0);
|
|
18
|
+
setFly({ imageUrl, fromRect, toRect, key: Date.now() });
|
|
19
|
+
}, []);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!fly)
|
|
22
|
+
return;
|
|
23
|
+
const t = setTimeout(() => setFly(null), 500);
|
|
24
|
+
return () => clearTimeout(t);
|
|
25
|
+
}, [fly]);
|
|
26
|
+
return (_jsxs(AddToCartFlyContext.Provider, { value: { triggerFly, setCartRef }, children: [children, fly && typeof window !== "undefined" && createPortal(_jsx("div", { className: "add-to-cart-fly fixed inset-0 pointer-events-none z-[99999]", "aria-hidden": true, children: _jsx("div", { className: "add-to-cart-fly-img absolute w-12 h-12 rounded-xl overflow-hidden bg-aurora-surface border-2 border-aurora-primary shadow-lg", style: {
|
|
27
|
+
["--fly-from-x"]: `${(fly.fromRect?.left ?? 0) + (fly.fromRect?.width ?? 0) / 2 - 24}px`,
|
|
28
|
+
["--fly-from-y"]: `${(fly.fromRect?.top ?? 0) + (fly.fromRect?.height ?? 0) / 2 - 24}px`,
|
|
29
|
+
["--fly-to-x"]: `${(fly.toRect?.left ?? 0) + (fly.toRect?.width ?? 0) / 2 - 24}px`,
|
|
30
|
+
["--fly-to-y"]: `${(fly.toRect?.top ?? 0) + (fly.toRect?.height ?? 0) / 2 - 24}px`,
|
|
31
|
+
animation: "add-to-cart-fly 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards",
|
|
32
|
+
}, children: fly.imageUrl ? (_jsx("img", { src: fly.imageUrl, alt: "", className: "w-full h-full object-cover" })) : (_jsx("div", { className: "w-full h-full flex items-center justify-center text-aurora-primary text-lg", children: "+" })) }) }, fly.key), document.body)] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface AppUser {
|
|
3
|
+
id: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
user_metadata?: Record<string, unknown>;
|
|
6
|
+
app_metadata?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
interface AuthState {
|
|
9
|
+
user: AppUser | null;
|
|
10
|
+
token: string | null;
|
|
11
|
+
loading: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface AuthContextValue extends AuthState {
|
|
14
|
+
setSession: (token: string, user: AppUser) => void;
|
|
15
|
+
signOut: () => Promise<void>;
|
|
16
|
+
fetchSession: () => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare function AuthProvider({ children }: {
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare function useAuth(): AuthContextValue;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=AuthProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AuthProvider.d.ts","sourceRoot":"","sources":["../../src/components/AuthProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAIf,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACxC;AAED,UAAU,SAAS;IACjB,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,UAAU,gBAAiB,SAAQ,SAAS;IAC1C,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACnD,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACnC;AAID,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CAoEjE;AAED,wBAAgB,OAAO,IAAI,gBAAgB,CAM1C"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useState, } from "react";
|
|
4
|
+
const AUTH_TOKEN_KEY = "aurora_app_token";
|
|
5
|
+
const AuthContext = createContext(null);
|
|
6
|
+
export function AuthProvider({ children }) {
|
|
7
|
+
const [state, setState] = useState({
|
|
8
|
+
user: null,
|
|
9
|
+
token: null,
|
|
10
|
+
loading: true,
|
|
11
|
+
});
|
|
12
|
+
const fetchSession = useCallback(async () => {
|
|
13
|
+
if (typeof window === "undefined")
|
|
14
|
+
return;
|
|
15
|
+
const token = localStorage.getItem(AUTH_TOKEN_KEY);
|
|
16
|
+
if (!token) {
|
|
17
|
+
setState((s) => ({ ...s, user: null, token: null, loading: false }));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch("/api/auth/session", {
|
|
22
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
23
|
+
});
|
|
24
|
+
if (res.ok) {
|
|
25
|
+
const data = (await res.json());
|
|
26
|
+
setState((s) => ({ ...s, user: data.user, token, loading: false }));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
localStorage.removeItem(AUTH_TOKEN_KEY);
|
|
30
|
+
setState((s) => ({ ...s, user: null, token: null, loading: false }));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
localStorage.removeItem(AUTH_TOKEN_KEY);
|
|
35
|
+
setState((s) => ({ ...s, user: null, token: null, loading: false }));
|
|
36
|
+
}
|
|
37
|
+
}, []);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
fetchSession();
|
|
40
|
+
}, [fetchSession]);
|
|
41
|
+
const setSession = useCallback((token, user) => {
|
|
42
|
+
if (typeof window !== "undefined") {
|
|
43
|
+
localStorage.setItem(AUTH_TOKEN_KEY, token);
|
|
44
|
+
}
|
|
45
|
+
setState((s) => ({ ...s, user, token, loading: false }));
|
|
46
|
+
}, []);
|
|
47
|
+
const signOut = useCallback(async () => {
|
|
48
|
+
const token = state.token ?? (typeof window !== "undefined" ? localStorage.getItem(AUTH_TOKEN_KEY) : null);
|
|
49
|
+
if (token) {
|
|
50
|
+
try {
|
|
51
|
+
await fetch("/api/auth/signout", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// ignore
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (typeof window !== "undefined") {
|
|
61
|
+
localStorage.removeItem(AUTH_TOKEN_KEY);
|
|
62
|
+
}
|
|
63
|
+
setState((s) => ({ ...s, user: null, token: null }));
|
|
64
|
+
}, [state.token]);
|
|
65
|
+
const value = {
|
|
66
|
+
...state,
|
|
67
|
+
setSession,
|
|
68
|
+
signOut,
|
|
69
|
+
fetchSession,
|
|
70
|
+
};
|
|
71
|
+
return _jsx(AuthContext.Provider, { value: value, children: children });
|
|
72
|
+
}
|
|
73
|
+
export function useAuth() {
|
|
74
|
+
const ctx = useContext(AuthContext);
|
|
75
|
+
if (!ctx) {
|
|
76
|
+
throw new Error("useAuth must be used within AuthProvider");
|
|
77
|
+
}
|
|
78
|
+
return ctx;
|
|
79
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CartLink.d.ts","sourceRoot":"","sources":["../../src/components/CartLink.tsx"],"names":[],"mappings":"AASA,wBAAgB,QAAQ,4CAsGvB"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { ShoppingCart } from "lucide-react";
|
|
5
|
+
import { useCart } from "./CartProvider";
|
|
6
|
+
import { useAddToCartFly } from "./AddToCartFly";
|
|
7
|
+
import { formatPrice } from "../lib/format-price";
|
|
8
|
+
import { useState, useRef, useEffect } from "react";
|
|
9
|
+
export function CartLink() {
|
|
10
|
+
const { items, total } = useCart();
|
|
11
|
+
const flyCtx = useAddToCartFly();
|
|
12
|
+
const [open, setOpen] = useState(false);
|
|
13
|
+
const ref = useRef(null);
|
|
14
|
+
const linkRef = useRef(null);
|
|
15
|
+
const count = items.reduce((sum, i) => sum + i.quantity, 0);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
flyCtx?.setCartRef(linkRef.current);
|
|
18
|
+
return () => flyCtx?.setCartRef(null);
|
|
19
|
+
}, [flyCtx]);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
function handleClickOutside(e) {
|
|
22
|
+
if (ref.current && !ref.current.contains(e.target)) {
|
|
23
|
+
setOpen(false);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
27
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
28
|
+
}, []);
|
|
29
|
+
return (_jsxs("div", { ref: ref, className: "relative", onMouseEnter: () => setOpen(true), onMouseLeave: () => setOpen(false), children: [_jsxs(Link, { ref: linkRef, href: "/cart", className: "flex items-center gap-2 text-sm text-aurora-muted hover:text-aurora-text transition-colors font-medium", children: [_jsx(ShoppingCart, { className: "w-5 h-5 shrink-0" }), _jsx("span", { children: "Cart" }), count > 0 && (_jsx("span", { className: "inline-flex items-center justify-center min-w-[1.25rem] h-5 px-2 rounded-full bg-aurora-primary text-white text-xs font-semibold cart-badge", children: count }, count))] }), open && count > 0 && (_jsx("div", { className: "absolute top-full right-0 mt-1 pt-1 z-[200]", children: _jsxs("div", { className: "rounded-xl bg-aurora-surface border border-aurora-border shadow-xl w-80 max-h-96 overflow-hidden", children: [_jsxs("div", { className: "p-4 border-b border-aurora-border", children: [_jsxs("p", { className: "font-semibold text-sm", children: [count, " ", count === 1 ? "item" : "items"] }), _jsx("p", { className: "text-lg font-bold text-aurora-primary mt-0.5", children: formatPrice(total) })] }), _jsxs("div", { className: "max-h-48 overflow-y-auto", children: [items.slice(0, 5).map((item) => (_jsxs("div", { className: "flex gap-3 p-3 border-b border-aurora-border last:border-0", children: [_jsx("div", { className: "w-12 h-12 rounded-lg bg-aurora-surface-hover shrink-0 overflow-hidden", children: item.imageUrl ? (_jsx("img", { src: item.imageUrl, alt: "", className: "w-full h-full object-cover" })) : (_jsx("div", { className: "w-full h-full flex items-center justify-center text-aurora-muted text-lg", children: "-" })) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-medium truncate", children: item.name }), _jsxs("p", { className: "text-xs text-aurora-muted", children: [formatPrice(item.unitAmount), " \u00D7 ", item.quantity, item.sellByWeight ? ` ${item.unit || "kg"}` : ""] })] })] }, item.id))), items.length > 5 && (_jsxs("p", { className: "text-xs text-aurora-muted p-3 text-center", children: ["+", items.length - 5, " more"] }))] }), _jsx("div", { className: "p-4", children: _jsx(Link, { href: "/cart", onClick: () => setOpen(false), className: "block w-full py-3 rounded-xl bg-aurora-primary text-white text-center font-semibold hover:bg-aurora-primary-dark transition-colors", children: "View cart \u00B7 Checkout" }) })] }) }))] }));
|
|
30
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export interface CartItem {
|
|
3
|
+
id: string;
|
|
4
|
+
recordId: string;
|
|
5
|
+
tableSlug: string;
|
|
6
|
+
name: string;
|
|
7
|
+
unitAmount: number;
|
|
8
|
+
quantity: number;
|
|
9
|
+
/** Variable-weight product: quantity is weight, unit is display unit */
|
|
10
|
+
sellByWeight?: boolean;
|
|
11
|
+
unit?: string;
|
|
12
|
+
/** Product image URL for basket display */
|
|
13
|
+
imageUrl?: string | null;
|
|
14
|
+
}
|
|
15
|
+
interface CartContextValue {
|
|
16
|
+
items: CartItem[];
|
|
17
|
+
addItem: (item: Omit<CartItem, "id" | "quantity"> & {
|
|
18
|
+
quantity?: number;
|
|
19
|
+
}) => void;
|
|
20
|
+
removeItem: (id: string) => void;
|
|
21
|
+
updateQuantity: (id: string, quantity: number) => void;
|
|
22
|
+
clearCart: () => void;
|
|
23
|
+
total: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function CartProvider({ children }: {
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export declare function useCart(): CartContextValue;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=CartProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CartProvider.d.ts","sourceRoot":"","sources":["../../src/components/CartProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAmBD,UAAU,gBAAgB;IACxB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,GAAG,UAAU,CAAC,GAAG;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACnF,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvD,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf;AAID,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,2CA4GjE;AAED,wBAAgB,OAAO,qBAItB"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useCallback, useState, useEffect, useRef, } from "react";
|
|
4
|
+
import { holmesCartUpdate } from "../lib/holmes-events";
|
|
5
|
+
const CART_KEY = "aurora-cart";
|
|
6
|
+
function loadCart() {
|
|
7
|
+
if (typeof window === "undefined")
|
|
8
|
+
return [];
|
|
9
|
+
try {
|
|
10
|
+
const stored = localStorage.getItem(CART_KEY);
|
|
11
|
+
return stored ? JSON.parse(stored) : [];
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function saveCart(items) {
|
|
18
|
+
if (typeof window === "undefined")
|
|
19
|
+
return;
|
|
20
|
+
localStorage.setItem(CART_KEY, JSON.stringify(items));
|
|
21
|
+
}
|
|
22
|
+
const CartContext = createContext(null);
|
|
23
|
+
export function CartProvider({ children }) {
|
|
24
|
+
const [items, setItems] = useState([]);
|
|
25
|
+
const [mounted, setMounted] = useState(false);
|
|
26
|
+
const itemsRef = useRef(items);
|
|
27
|
+
const bootstrapFiredRef = useRef(false);
|
|
28
|
+
itemsRef.current = items;
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setItems(loadCart());
|
|
31
|
+
setMounted(true);
|
|
32
|
+
}, []);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (mounted)
|
|
35
|
+
saveCart(items);
|
|
36
|
+
}, [items, mounted]);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!mounted)
|
|
39
|
+
return;
|
|
40
|
+
const count = items.reduce((s, i) => s + i.quantity, 0);
|
|
41
|
+
const holmesItems = items.map((i) => ({
|
|
42
|
+
id: i.recordId,
|
|
43
|
+
name: i.name,
|
|
44
|
+
price: i.unitAmount,
|
|
45
|
+
}));
|
|
46
|
+
holmesCartUpdate(count, holmesItems);
|
|
47
|
+
}, [items, mounted]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!mounted)
|
|
50
|
+
return;
|
|
51
|
+
const fireBootstrap = () => {
|
|
52
|
+
if (bootstrapFiredRef.current)
|
|
53
|
+
return;
|
|
54
|
+
bootstrapFiredRef.current = true;
|
|
55
|
+
const currentItems = itemsRef.current;
|
|
56
|
+
const count = currentItems.reduce((s, i) => s + i.quantity, 0);
|
|
57
|
+
const holmesItems = currentItems.map((i) => ({
|
|
58
|
+
id: i.recordId,
|
|
59
|
+
name: i.name,
|
|
60
|
+
price: i.unitAmount,
|
|
61
|
+
}));
|
|
62
|
+
holmesCartUpdate(count, holmesItems, true);
|
|
63
|
+
};
|
|
64
|
+
const onReady = () => fireBootstrap();
|
|
65
|
+
if (window.holmes?.getSessionId && !bootstrapFiredRef.current) {
|
|
66
|
+
fireBootstrap();
|
|
67
|
+
}
|
|
68
|
+
document.addEventListener("holmes:ready", onReady);
|
|
69
|
+
return () => document.removeEventListener("holmes:ready", onReady);
|
|
70
|
+
}, [mounted]);
|
|
71
|
+
const addItem = useCallback((item) => {
|
|
72
|
+
const qty = item.quantity ?? 1;
|
|
73
|
+
const cartId = `${item.tableSlug}:${item.recordId}`;
|
|
74
|
+
const isNew = !itemsRef.current.some((i) => i.id === cartId);
|
|
75
|
+
setItems((prev) => {
|
|
76
|
+
const existing = prev.find((i) => i.id === cartId);
|
|
77
|
+
if (existing && existing.sellByWeight === item.sellByWeight) {
|
|
78
|
+
return prev.map((i) => i.id === cartId ? { ...i, quantity: i.quantity + qty } : i);
|
|
79
|
+
}
|
|
80
|
+
return [...prev, { ...item, id: cartId, recordId: item.recordId, quantity: qty }];
|
|
81
|
+
});
|
|
82
|
+
if (isNew && typeof window !== "undefined") {
|
|
83
|
+
window.dispatchEvent(new CustomEvent("cart:itemAdded", { detail: { name: item.name } }));
|
|
84
|
+
}
|
|
85
|
+
}, []);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!mounted)
|
|
88
|
+
return;
|
|
89
|
+
const handler = (e) => {
|
|
90
|
+
const d = e.detail;
|
|
91
|
+
if (!d?.products?.length || !d.tableSlug)
|
|
92
|
+
return;
|
|
93
|
+
for (const p of d.products) {
|
|
94
|
+
addItem({
|
|
95
|
+
recordId: p.id,
|
|
96
|
+
tableSlug: d.tableSlug,
|
|
97
|
+
name: p.name,
|
|
98
|
+
unitAmount: p.price,
|
|
99
|
+
imageUrl: p.image,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
document.addEventListener("holmes:addBundle", handler);
|
|
104
|
+
return () => document.removeEventListener("holmes:addBundle", handler);
|
|
105
|
+
}, [addItem, mounted]);
|
|
106
|
+
const removeItem = useCallback((id) => {
|
|
107
|
+
setItems((prev) => prev.filter((i) => i.id !== id));
|
|
108
|
+
}, []);
|
|
109
|
+
const updateQuantity = useCallback((id, quantity) => {
|
|
110
|
+
if (quantity <= 0) {
|
|
111
|
+
setItems((prev) => prev.filter((i) => i.id !== id));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
setItems((prev) => prev.map((i) => (i.id === id ? { ...i, quantity } : i)));
|
|
115
|
+
}, []);
|
|
116
|
+
const clearCart = useCallback(() => setItems([]), []);
|
|
117
|
+
const total = items.reduce((sum, i) => sum + i.unitAmount * i.quantity, 0);
|
|
118
|
+
return (_jsx(CartContext.Provider, { value: { items, addItem, removeItem, updateQuantity, clearCart, total }, children: children }));
|
|
119
|
+
}
|
|
120
|
+
export function useCart() {
|
|
121
|
+
const ctx = useContext(CartContext);
|
|
122
|
+
if (!ctx)
|
|
123
|
+
throw new Error("useCart must be used within CartProvider");
|
|
124
|
+
return ctx;
|
|
125
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type CatalogueEmptyStateProps = {
|
|
2
|
+
hasCategory: boolean;
|
|
3
|
+
hasStore: boolean;
|
|
4
|
+
categories?: {
|
|
5
|
+
name: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
}[];
|
|
8
|
+
};
|
|
9
|
+
export declare function CatalogueEmptyState({ hasCategory, hasStore, categories, }: CatalogueEmptyStateProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=CatalogueEmptyState.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CatalogueEmptyState.d.ts","sourceRoot":"","sources":["../../src/components/CatalogueEmptyState.tsx"],"names":[],"mappings":"AAGA,KAAK,wBAAwB,GAAG;IAC9B,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC/C,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,EAClC,WAAW,EACX,QAAQ,EACR,UAAe,GAChB,EAAE,wBAAwB,2CAwE1B"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { ShoppingBag, Store, LayoutGrid } from "lucide-react";
|
|
4
|
+
export function CatalogueEmptyState({ hasCategory, hasStore, categories = [], }) {
|
|
5
|
+
if (!hasStore) {
|
|
6
|
+
return (_jsxs("div", { className: "flex flex-col items-center justify-center py-16 px-6 text-center max-w-md mx-auto", children: [_jsx("div", { className: "w-20 h-20 rounded-full bg-aurora-surface-hover flex items-center justify-center mb-6 ring-4 ring-aurora-primary/10", children: _jsx(Store, { className: "w-10 h-10 text-aurora-primary" }) }), _jsx("h2", { className: "text-xl font-bold text-aurora-text mb-2", children: "Select a store to browse" }), _jsx("p", { className: "text-aurora-muted mb-8", children: "Choose your local store to see products and start shopping." }), _jsxs(Link, { href: "/stores", className: "inline-flex items-center justify-center gap-2 h-12 px-6 rounded-xl bg-aurora-primary text-white font-semibold hover:bg-aurora-primary-dark transition-colors", children: [_jsx(Store, { className: "w-5 h-5" }), "Find your store"] })] }));
|
|
7
|
+
}
|
|
8
|
+
if (hasCategory) {
|
|
9
|
+
return (_jsxs("div", { className: "flex flex-col items-center justify-center py-16 px-6 text-center max-w-md mx-auto", children: [_jsx("div", { className: "w-20 h-20 rounded-full bg-aurora-surface-hover flex items-center justify-center mb-6 ring-4 ring-aurora-primary/10", children: _jsx(LayoutGrid, { className: "w-10 h-10 text-aurora-primary" }) }), _jsx("h2", { className: "text-xl font-bold text-aurora-text mb-2", children: "No products in this category yet" }), _jsx("p", { className: "text-aurora-muted mb-8", children: "This category is empty. Try another category or browse all products." }), _jsxs("div", { className: "flex flex-wrap justify-center gap-3", children: [_jsxs(Link, { href: "/catalogue", className: "inline-flex items-center justify-center gap-2 h-12 px-6 rounded-xl bg-aurora-primary text-white font-semibold hover:bg-aurora-primary-dark transition-colors", children: [_jsx(LayoutGrid, { className: "w-5 h-5" }), "View all products"] }), categories.slice(0, 3).map((cat) => (_jsx(Link, { href: `/catalogue?category=${cat.slug}`, className: "inline-flex items-center h-12 px-5 rounded-xl border-2 border-aurora-border text-aurora-text font-medium hover:border-aurora-primary hover:text-aurora-primary transition-colors", children: cat.name }, cat.slug)))] })] }));
|
|
10
|
+
}
|
|
11
|
+
return (_jsxs("div", { className: "flex flex-col items-center justify-center py-16 px-6 text-center max-w-md mx-auto", children: [_jsx("div", { className: "w-20 h-20 rounded-full bg-aurora-surface-hover flex items-center justify-center mb-6 ring-4 ring-aurora-primary/10", children: _jsx(ShoppingBag, { className: "w-10 h-10 text-aurora-primary" }) }), _jsx("h2", { className: "text-xl font-bold text-aurora-text mb-2", children: "No products yet" }), _jsx("p", { className: "text-aurora-muted mb-8", children: "Products will appear here once they're added. Check back soon or browse our categories." }), _jsxs(Link, { href: "/catalogue", className: "inline-flex items-center justify-center gap-2 h-12 px-6 rounded-xl bg-aurora-primary text-white font-semibold hover:bg-aurora-primary-dark transition-colors", children: [_jsx(ShoppingBag, { className: "w-5 h-5" }), "Browse catalogue"] })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type CategoryItem = {
|
|
2
|
+
name: string;
|
|
3
|
+
slug: string;
|
|
4
|
+
};
|
|
5
|
+
export type SortOption = "featured" | "bestsellers" | "new" | "sale";
|
|
6
|
+
export declare const SORT_OPTIONS: {
|
|
7
|
+
id: SortOption;
|
|
8
|
+
label: string;
|
|
9
|
+
}[];
|
|
10
|
+
type CatalogueFiltersProps = {
|
|
11
|
+
categories: CategoryItem[];
|
|
12
|
+
currentCategory: string;
|
|
13
|
+
currentSort: SortOption;
|
|
14
|
+
onSortChange: (sort: SortOption) => void;
|
|
15
|
+
storeName?: string;
|
|
16
|
+
onClose?: () => void;
|
|
17
|
+
variant?: "sidebar" | "drawer";
|
|
18
|
+
suggestedSlugs?: string[];
|
|
19
|
+
/** When set, categories are reordered: mission-relevant first (progressive narrowing). */
|
|
20
|
+
missionPrioritySlugs?: string[];
|
|
21
|
+
};
|
|
22
|
+
export declare function CatalogueFilters({ categories, currentCategory, currentSort, onSortChange, storeName, onClose, variant, suggestedSlugs, missionPrioritySlugs, }: CatalogueFiltersProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=CatalogueFilters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CatalogueFilters.d.ts","sourceRoot":"","sources":["../../src/components/CatalogueFilters.tsx"],"names":[],"mappings":"AAsBA,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAkC1D,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,aAAa,GAAG,KAAK,GAAG,MAAM,CAAC;AAErE,eAAO,MAAM,YAAY,EAAE;IAAE,EAAE,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAK3D,CAAC;AAEF,KAAK,qBAAqB,GAAG;IAC3B,UAAU,EAAE,YAAY,EAAE,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,UAAU,CAAC;IACxB,YAAY,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACzC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,0FAA0F;IAC1F,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC,CAAC;AA8BF,wBAAgB,gBAAgB,CAAC,EAC/B,UAAU,EACV,eAAe,EACf,WAAW,EACX,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAmB,EACnB,cAAmB,EACnB,oBAAyB,GAC1B,EAAE,qBAAqB,2CAuGvB"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { ChevronDown, Baby, Beer, Candy, Cat, Wheat, Sparkles, CupSoda, Droplets, HeartPulse, Apple, Drumstick, Shirt, } from "lucide-react";
|
|
6
|
+
import { HolmesSprinkleIcon } from "./HolmesSprinkleIcon";
|
|
7
|
+
/** Map category slug (or partial name) to icon for sidebar. */
|
|
8
|
+
function getCategoryIcon(slug) {
|
|
9
|
+
const s = slug.toLowerCase();
|
|
10
|
+
const map = {
|
|
11
|
+
"baby-food": Baby,
|
|
12
|
+
baby: Baby,
|
|
13
|
+
beer: Beer,
|
|
14
|
+
candy: Candy,
|
|
15
|
+
"cat-food": Cat,
|
|
16
|
+
cat: Cat,
|
|
17
|
+
cereal: Wheat,
|
|
18
|
+
cleaning: Sparkles,
|
|
19
|
+
dishwashing: Sparkles,
|
|
20
|
+
tea: CupSoda,
|
|
21
|
+
water: Droplets,
|
|
22
|
+
"health-care": HeartPulse,
|
|
23
|
+
healthcare: HeartPulse,
|
|
24
|
+
juices: Apple,
|
|
25
|
+
juice: Apple,
|
|
26
|
+
poultry: Drumstick,
|
|
27
|
+
"skin-care": Shirt,
|
|
28
|
+
skincare: Shirt,
|
|
29
|
+
vegetables: Apple,
|
|
30
|
+
fruits: Apple,
|
|
31
|
+
dairy: Droplets,
|
|
32
|
+
bakery: Wheat,
|
|
33
|
+
snacks: Candy,
|
|
34
|
+
beverages: CupSoda,
|
|
35
|
+
};
|
|
36
|
+
return map[s] ?? Wheat;
|
|
37
|
+
}
|
|
38
|
+
export const SORT_OPTIONS = [
|
|
39
|
+
{ id: "featured", label: "Featured" },
|
|
40
|
+
{ id: "bestsellers", label: "Bestsellers" },
|
|
41
|
+
{ id: "new", label: "New Arrivals" },
|
|
42
|
+
{ id: "sale", label: "On Sale" },
|
|
43
|
+
];
|
|
44
|
+
function FilterSection({ title, children, defaultOpen = true, }) {
|
|
45
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
46
|
+
return (_jsxs("section", { className: "border-b border-aurora-border last:border-0", children: [_jsxs("button", { type: "button", onClick: () => setOpen((o) => !o), className: "flex items-center justify-between w-full py-3 text-left", children: [_jsx("h3", { className: "text-xs font-semibold uppercase tracking-wider text-aurora-muted", children: title }), _jsx(ChevronDown, { className: `w-4 h-4 text-aurora-muted transition-transform ${open ? "rotate-180" : ""}` })] }), open && _jsx("div", { className: "pb-3", children: children })] }));
|
|
47
|
+
}
|
|
48
|
+
export function CatalogueFilters({ categories, currentCategory, currentSort, onSortChange, storeName, onClose, variant = "sidebar", suggestedSlugs = [], missionPrioritySlugs = [], }) {
|
|
49
|
+
const sortedCategories = [...categories].sort((a, b) => {
|
|
50
|
+
const aSuggested = suggestedSlugs.includes(a.slug);
|
|
51
|
+
const bSuggested = suggestedSlugs.includes(b.slug);
|
|
52
|
+
if (aSuggested && !bSuggested)
|
|
53
|
+
return -1;
|
|
54
|
+
if (!aSuggested && bSuggested)
|
|
55
|
+
return 1;
|
|
56
|
+
if (missionPrioritySlugs.length > 0) {
|
|
57
|
+
const aIdx = missionPrioritySlugs.indexOf(a.slug);
|
|
58
|
+
const bIdx = missionPrioritySlugs.indexOf(b.slug);
|
|
59
|
+
if (aIdx >= 0 && bIdx < 0)
|
|
60
|
+
return -1;
|
|
61
|
+
if (aIdx < 0 && bIdx >= 0)
|
|
62
|
+
return 1;
|
|
63
|
+
if (aIdx >= 0 && bIdx >= 0)
|
|
64
|
+
return aIdx - bIdx;
|
|
65
|
+
}
|
|
66
|
+
return 0;
|
|
67
|
+
});
|
|
68
|
+
const content = (_jsxs("div", { className: "space-y-0", children: [_jsx(FilterSection, { title: "Categories", children: _jsxs("nav", { className: "space-y-1", children: [_jsx(Link, { href: "/catalogue", onClick: onClose, className: `block px-3 py-2 rounded-component text-sm font-medium transition-colors ${!currentCategory
|
|
69
|
+
? "bg-aurora-accent/20 text-aurora-accent border border-aurora-accent/40"
|
|
70
|
+
: "text-aurora-muted hover:text-aurora-text hover:bg-aurora-surface-hover border border-transparent"}`, children: "All categories" }), sortedCategories.map((cat) => {
|
|
71
|
+
const isSuggested = suggestedSlugs.includes(cat.slug);
|
|
72
|
+
return (_jsxs(Link, { href: `/catalogue?category=${encodeURIComponent(cat.slug)}`, onClick: onClose, className: `flex items-center gap-2 px-3 py-2 rounded-component text-sm font-medium transition-colors ${currentCategory === cat.slug
|
|
73
|
+
? "bg-aurora-accent/20 text-aurora-accent border border-aurora-accent/40"
|
|
74
|
+
: "text-aurora-muted hover:text-aurora-text hover:bg-aurora-surface-hover border border-transparent"}`, children: [(() => {
|
|
75
|
+
const Icon = getCategoryIcon(cat.slug);
|
|
76
|
+
return _jsx(Icon, { className: "w-4 h-4 shrink-0 text-aurora-muted", "aria-hidden": true });
|
|
77
|
+
})(), isSuggested ? _jsx(HolmesSprinkleIcon, { className: "shrink-0" }) : null, cat.name] }, cat.slug));
|
|
78
|
+
})] }) }), _jsx(FilterSection, { title: "Sort by", defaultOpen: true, children: _jsx("div", { className: "space-y-1", children: SORT_OPTIONS.map((opt) => (_jsx("button", { type: "button", onClick: () => {
|
|
79
|
+
onSortChange(opt.id);
|
|
80
|
+
onClose?.();
|
|
81
|
+
}, className: `w-full text-left px-3 py-2 rounded-component text-sm font-medium transition-colors ${currentSort === opt.id
|
|
82
|
+
? "bg-aurora-accent/20 text-aurora-accent border border-aurora-accent/40"
|
|
83
|
+
: "text-aurora-muted hover:text-aurora-text hover:bg-aurora-surface-hover border border-transparent"}`, children: opt.label }, opt.id))) }) }), storeName && (_jsxs("p", { className: "text-xs text-aurora-muted pt-2 border-t border-aurora-border", children: ["Showing products from ", storeName] }))] }));
|
|
84
|
+
if (variant === "drawer") {
|
|
85
|
+
return (_jsx("div", { className: "bg-aurora-surface border-t border-aurora-border p-6", children: content }));
|
|
86
|
+
}
|
|
87
|
+
return (_jsx("aside", { className: "w-56 shrink-0 hidden lg:block", children: _jsx("div", { className: "pattern-well sticky top-24 space-y-6 rounded-component border border-aurora-border p-4", children: content }) }));
|
|
88
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CheckoutButton.d.ts","sourceRoot":"","sources":["../../src/components/CheckoutButton.tsx"],"names":[],"mappings":"AAMA,wBAAgB,cAAc,4CAiF7B"}
|