@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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useCart } from "./CartProvider";
|
|
5
|
+
import { getApiBase, getTenantSlug } from "../lib/aurora";
|
|
6
|
+
export function CheckoutButton() {
|
|
7
|
+
const { items, total, clearCart } = useCart();
|
|
8
|
+
const [loading, setLoading] = useState(false);
|
|
9
|
+
const handleCheckout = async () => {
|
|
10
|
+
if (items.length === 0)
|
|
11
|
+
return;
|
|
12
|
+
setLoading(true);
|
|
13
|
+
const apiBase = getApiBase();
|
|
14
|
+
const tenantSlug = getTenantSlug();
|
|
15
|
+
if (!apiBase || !tenantSlug) {
|
|
16
|
+
alert("API not configured. Set NEXT_PUBLIC_AURORA_API_URL and NEXT_PUBLIC_TENANT_SLUG.");
|
|
17
|
+
setLoading(false);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const origin = typeof window !== "undefined" ? window.location.origin : "";
|
|
21
|
+
const successUrl = `${origin}/checkout/success`;
|
|
22
|
+
const cancelUrl = `${origin}/cart`;
|
|
23
|
+
const lineItems = items.map((i) => ({
|
|
24
|
+
productId: i.recordId,
|
|
25
|
+
tableSlug: i.tableSlug,
|
|
26
|
+
quantity: i.quantity,
|
|
27
|
+
priceData: {
|
|
28
|
+
unitAmount: i.unitAmount,
|
|
29
|
+
currency: "GBP",
|
|
30
|
+
productData: { name: i.name },
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
const holmes_session_id = typeof window !== "undefined" ? window.holmes?.getSessionId?.() : undefined;
|
|
34
|
+
const holmes_mission_start_timestamp = typeof window !== "undefined" ? window.holmes?.getMissionStartTimestamp?.() : undefined;
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch("/api/checkout/sessions", {
|
|
37
|
+
method: "POST",
|
|
38
|
+
headers: { "Content-Type": "application/json" },
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
successUrl,
|
|
41
|
+
cancelUrl,
|
|
42
|
+
lineItems,
|
|
43
|
+
...(holmes_session_id && { holmes_session_id }),
|
|
44
|
+
...(holmes_mission_start_timestamp != null && {
|
|
45
|
+
holmes_mission_start_timestamp,
|
|
46
|
+
}),
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const err = await res.json().catch(() => ({}));
|
|
51
|
+
throw new Error(err.error ?? "Checkout failed");
|
|
52
|
+
}
|
|
53
|
+
const data = (await res.json());
|
|
54
|
+
if (data.url) {
|
|
55
|
+
clearCart();
|
|
56
|
+
window.location.href = data.url;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
throw new Error("No checkout URL returned");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
alert(e instanceof Error ? e.message : "Checkout failed");
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
return (_jsx("button", { type: "button", onClick: handleCheckout, disabled: loading || items.length === 0, className: "w-full sm:w-auto px-6 py-3 rounded-component bg-aurora-accent text-aurora-bg font-semibold hover:opacity-90 disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-aurora-primary focus:ring-offset-2 focus:ring-offset-aurora-surface transition-opacity", children: loading ? "Processing…" : "Proceed to checkout" }));
|
|
70
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads Holmes script when Aurora API and tenant are configured.
|
|
3
|
+
* Rendered as server component so script is in initial HTML.
|
|
4
|
+
* To disable Holmes: set cookie holmes_holdout=1 (the script checks this).
|
|
5
|
+
*/
|
|
6
|
+
export declare function ConditionalHolmesScript(): import("react/jsx-runtime").JSX.Element | null;
|
|
7
|
+
//# sourceMappingURL=ConditionalHolmesScript.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConditionalHolmesScript.d.ts","sourceRoot":"","sources":["../../src/components/ConditionalHolmesScript.tsx"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,uBAAuB,mDAUtC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import Script from "next/script";
|
|
3
|
+
import { getHolmesScriptUrl } from "@aurora-studio/sdk";
|
|
4
|
+
/**
|
|
5
|
+
* Loads Holmes script when Aurora API and tenant are configured.
|
|
6
|
+
* Rendered as server component so script is in initial HTML.
|
|
7
|
+
* To disable Holmes: set cookie holmes_holdout=1 (the script checks this).
|
|
8
|
+
*/
|
|
9
|
+
export function ConditionalHolmesScript() {
|
|
10
|
+
const apiUrl = process.env.NEXT_PUBLIC_AURORA_API_URL;
|
|
11
|
+
const tenantSlug = process.env.NEXT_PUBLIC_TENANT_SLUG;
|
|
12
|
+
if (!apiUrl || !tenantSlug) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const src = getHolmesScriptUrl(apiUrl, tenantSlug);
|
|
16
|
+
return _jsx(Script, { src: src, strategy: "afterInteractive" });
|
|
17
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface FloatingLabelInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "placeholder"> {
|
|
2
|
+
label: string;
|
|
3
|
+
error?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function FloatingLabelInput({ label, error, id, className, value, ...props }: FloatingLabelInputProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=FloatingLabelInput.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FloatingLabelInput.d.ts","sourceRoot":"","sources":["../../src/components/FloatingLabelInput.tsx"],"names":[],"mappings":"AAIA,UAAU,uBAAwB,SAAQ,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,aAAa,CAAC;IACxG,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,SAAc,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,EAAE,uBAAuB,2CAmChH"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useRef, useState } from "react";
|
|
4
|
+
export function FloatingLabelInput({ label, error, id, className = "", value, ...props }) {
|
|
5
|
+
const generatedId = useId();
|
|
6
|
+
const inputId = id ?? generatedId;
|
|
7
|
+
const inputRef = useRef(null);
|
|
8
|
+
const [focused, setFocused] = useState(false);
|
|
9
|
+
const floatLabel = focused || Boolean(value != null && value !== "");
|
|
10
|
+
return (_jsxs("div", { className: "relative", children: [_jsx("input", { ref: inputRef, id: inputId, value: value, onFocus: (e) => { setFocused(true); props.onFocus?.(e); }, onBlur: (e) => { setFocused(false); props.onBlur?.(e); }, placeholder: " ", className: `peer w-full h-12 px-4 rounded-xl bg-aurora-bg border text-aurora-text placeholder:text-transparent focus:outline-none focus:ring-2 focus:ring-aurora-primary/50 focus:border-aurora-primary transition-colors ${error ? "border-aurora-error" : "border-aurora-border"} ${className}`, ...props }), _jsx("label", { htmlFor: inputId, className: `absolute left-4 transition-all duration-200 pointer-events-none ${floatLabel
|
|
11
|
+
? "-top-1 -translate-y-full text-xs text-aurora-muted bg-aurora-bg px-1"
|
|
12
|
+
: "top-1/2 -translate-y-1/2 text-base text-aurora-muted"}`, children: label }), error && _jsx("p", { className: "mt-1 text-sm text-aurora-error", children: error })] }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatches holmes:refreshHome so the Holmes script re-fetches home
|
|
3
|
+
* personalization when the user navigates to the home page.
|
|
4
|
+
* Also listens for holmes:ready and re-dispatches refresh - so when the script
|
|
5
|
+
* loads after we mount (e.g. slow network), we trigger the fetch.
|
|
6
|
+
*/
|
|
7
|
+
export declare function HolmesHomeRefresher(): null;
|
|
8
|
+
//# sourceMappingURL=HolmesHomeRefresher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HolmesHomeRefresher.d.ts","sourceRoot":"","sources":["../../src/components/HolmesHomeRefresher.tsx"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,wBAAgB,mBAAmB,SAUlC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Dispatches holmes:refreshHome so the Holmes script re-fetches home
|
|
5
|
+
* personalization when the user navigates to the home page.
|
|
6
|
+
* Also listens for holmes:ready and re-dispatches refresh - so when the script
|
|
7
|
+
* loads after we mount (e.g. slow network), we trigger the fetch.
|
|
8
|
+
*/
|
|
9
|
+
export function HolmesHomeRefresher() {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
document.dispatchEvent(new CustomEvent("holmes:refreshHome"));
|
|
12
|
+
const onReady = () => {
|
|
13
|
+
document.dispatchEvent(new CustomEvent("holmes:refreshHome"));
|
|
14
|
+
};
|
|
15
|
+
document.addEventListener("holmes:ready", onReady);
|
|
16
|
+
return () => document.removeEventListener("holmes:ready", onReady);
|
|
17
|
+
}, []);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HolmesProductViewTracker.d.ts","sourceRoot":"","sources":["../../src/components/HolmesProductViewTracker.tsx"],"names":[],"mappings":"AA+BA,wBAAgB,wBAAwB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,QAO5E"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { holmesProductView } from "../lib/holmes-events";
|
|
4
|
+
const STORAGE_KEY = "holmes_products_viewed";
|
|
5
|
+
const MAX_VIEWED = 20;
|
|
6
|
+
function getViewedIds() {
|
|
7
|
+
if (typeof window === "undefined")
|
|
8
|
+
return [];
|
|
9
|
+
try {
|
|
10
|
+
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
11
|
+
if (!raw)
|
|
12
|
+
return [];
|
|
13
|
+
return JSON.parse(raw);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function appendViewed(productId) {
|
|
20
|
+
const current = getViewedIds();
|
|
21
|
+
const filtered = current.filter((id) => id !== productId);
|
|
22
|
+
const next = [productId, ...filtered].slice(0, MAX_VIEWED);
|
|
23
|
+
try {
|
|
24
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(next));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
/* ignore */
|
|
28
|
+
}
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
export function HolmesProductViewTracker({ productId }) {
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!productId)
|
|
34
|
+
return;
|
|
35
|
+
const allIds = appendViewed(productId);
|
|
36
|
+
holmesProductView(allIds);
|
|
37
|
+
}, [productId]);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small sparkle/sprinkle icon indicating Holmes AI suggestion.
|
|
3
|
+
* Tooltip shows "Holmes suggestion" on hover.
|
|
4
|
+
*/
|
|
5
|
+
export declare function HolmesSprinkleIcon({ className }: {
|
|
6
|
+
className?: string;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=HolmesSprinkleIcon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HolmesSprinkleIcon.d.ts","sourceRoot":"","sources":["../../src/components/HolmesSprinkleIcon.tsx"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,SAAS,EAAE,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,2CAmBvE"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Small sparkle/sprinkle icon indicating Holmes AI suggestion.
|
|
5
|
+
* Tooltip shows "Holmes suggestion" on hover.
|
|
6
|
+
*/
|
|
7
|
+
export function HolmesSprinkleIcon({ className }) {
|
|
8
|
+
return (_jsx("span", { className: `inline-flex items-center justify-center ${className ?? ""}`, title: "Personalised for you", "aria-label": "Personalised for you", children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: "w-3.5 h-3.5 text-aurora-accent", children: [_jsx("path", { d: "M12 1l1.5 4.5L18 7l-4.5 1.5L12 13l-1.5-4.5L6 7l4.5-1.5L12 1z" }), _jsx("path", { d: "M5 16l1 3 3 1-3 1-1 3-1-3-3-1 3-1 1-3z", opacity: "0.7" }), _jsx("path", { d: "M19 19l1 3 3 1-3 1-1 3-1-3-3-1 3-1 1-3z", opacity: "0.7" })] }) }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type Props = {
|
|
2
|
+
entity: string;
|
|
3
|
+
entityType?: "recipe" | "ingredient" | "product";
|
|
4
|
+
/** Compact layout for inline use (e.g. product card) */
|
|
5
|
+
variant?: "default" | "compact";
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Holmes tidbits - origin, pairing, tip for recipes, ingredients, products.
|
|
9
|
+
* Renders nothing if no tidbits. Prominent but unobtrusive.
|
|
10
|
+
*/
|
|
11
|
+
export declare function HolmesTidbits({ entity, entityType, variant, }: Props): import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=HolmesTidbits.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HolmesTidbits.d.ts","sourceRoot":"","sources":["../../src/components/HolmesTidbits.tsx"],"names":[],"mappings":"AAoBA,KAAK,KAAK,GAAG;IACX,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,QAAQ,GAAG,YAAY,GAAG,SAAS,CAAC;IACjD,wDAAwD;IACxD,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;CACjC,CAAC;AAEF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAC5B,MAAM,EACN,UAAqB,EACrB,OAAmB,GACpB,EAAE,KAAK,kDA+DP"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { holmesTidbits } from "../lib/aurora";
|
|
6
|
+
import { HolmesSprinkleIcon } from "./HolmesSprinkleIcon";
|
|
7
|
+
const CATEGORY_LABELS = {
|
|
8
|
+
origin: "Origin",
|
|
9
|
+
pairing: "Pairs well",
|
|
10
|
+
tip: "Tip",
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Holmes tidbits - origin, pairing, tip for recipes, ingredients, products.
|
|
14
|
+
* Renders nothing if no tidbits. Prominent but unobtrusive.
|
|
15
|
+
*/
|
|
16
|
+
export function HolmesTidbits({ entity, entityType = "recipe", variant = "default", }) {
|
|
17
|
+
const [tidbits, setTidbits] = useState([]);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!entity?.trim()) {
|
|
20
|
+
setTidbits([]);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
holmesTidbits(entity.trim(), entityType)
|
|
24
|
+
.then((res) => setTidbits(res.tidbits ?? []))
|
|
25
|
+
.catch(() => setTidbits([]));
|
|
26
|
+
}, [entity, entityType]);
|
|
27
|
+
if (tidbits.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
if (variant === "compact") {
|
|
30
|
+
return (_jsx("div", { className: "flex flex-wrap gap-2", children: tidbits.slice(0, 2).map((t) => (_jsxs("span", { className: "inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-aurora-primary/10 text-aurora-muted text-xs border border-aurora-primary/20", children: [_jsx(HolmesSprinkleIcon, {}), _jsx("span", { children: t.content })] }, t.id))) }));
|
|
31
|
+
}
|
|
32
|
+
return (_jsxs("div", { className: "p-4 rounded-xl bg-aurora-surface/60 border border-aurora-border", children: [_jsxs("div", { className: "flex items-center gap-2 mb-2.5", children: [_jsx(HolmesSprinkleIcon, { className: "shrink-0" }), _jsx("span", { className: "text-xs font-medium text-aurora-muted uppercase tracking-wider", children: "Holmes insight" })] }), _jsx("div", { className: "space-y-2.5", children: tidbits.slice(0, 3).map((t) => (_jsxs("div", { children: [_jsx("span", { className: "text-[10px] uppercase tracking-wider text-aurora-muted/80 mr-1.5", children: CATEGORY_LABELS[t.category] ?? t.category }), _jsxs("p", { className: "text-sm text-aurora-text leading-relaxed", children: [t.content, t.source_url && (_jsx(Link, { href: t.source_url, target: "_blank", rel: "noopener noreferrer", className: "ml-1.5 text-aurora-accent hover:underline text-xs", children: "Learn more" }))] })] }, t.id))) })] }));
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductCardSkeleton.d.ts","sourceRoot":"","sources":["../../src/components/ProductCardSkeleton.tsx"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,wBAAgB,mBAAmB,4CAalC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/** Matches product card structure exactly for loading state - preserves layout width */
|
|
3
|
+
export function ProductCardSkeleton() {
|
|
4
|
+
return (_jsxs("div", { className: "group p-4 rounded-xl bg-aurora-surface border border-aurora-border overflow-hidden w-full min-w-[160px] min-h-[280px] flex flex-col", children: [_jsxs("div", { className: "block", children: [_jsx("div", { className: "aspect-square rounded-lg bg-aurora-surface-hover mb-3 overflow-hidden skeleton-shimmer" }), _jsx("div", { className: "h-4 rounded skeleton-shimmer w-3/4 mb-1" }), _jsx("div", { className: "h-4 rounded skeleton-shimmer w-1/4" })] }), _jsx("div", { className: "mt-auto pt-3", children: _jsx("div", { className: "h-12 rounded-xl skeleton-shimmer w-full" }) })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductDetailTabs.d.ts","sourceRoot":"","sources":["../../src/components/ProductDetailTabs.tsx"],"names":[],"mappings":"AAIA,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,2CA6EhF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
export function ProductDetailTabs({ record }) {
|
|
5
|
+
const [active, setActive] = useState("details");
|
|
6
|
+
const features = record.features;
|
|
7
|
+
const featuresList = Array.isArray(features)
|
|
8
|
+
? features
|
|
9
|
+
: typeof features === "string"
|
|
10
|
+
? (() => {
|
|
11
|
+
try {
|
|
12
|
+
const p = JSON.parse(features);
|
|
13
|
+
return Array.isArray(p) ? p : [];
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
})()
|
|
19
|
+
: [];
|
|
20
|
+
const storageInstructions = record.storage_instructions;
|
|
21
|
+
const tabs = [
|
|
22
|
+
{ id: "details", label: "Product Details" },
|
|
23
|
+
{ id: "nutrition", label: "Nutrition Facts" },
|
|
24
|
+
{ id: "feedback", label: "Customer Feedback" },
|
|
25
|
+
];
|
|
26
|
+
return (_jsxs("div", { className: "pattern-well rounded-xl border border-aurora-border p-6", children: [_jsx("div", { className: "flex gap-4 border-b border-aurora-border", children: tabs.map((t) => (_jsx("button", { type: "button", onClick: () => setActive(t.id), className: `py-3 font-medium border-b-2 transition-colors -mb-[2px] ${active === t.id
|
|
27
|
+
? "border-aurora-accent text-aurora-accent"
|
|
28
|
+
: "border-transparent text-aurora-muted hover:text-aurora-text"}`, children: t.label }, t.id))) }), _jsxs("div", { className: "py-6", children: [active === "details" && (_jsxs("div", { className: "space-y-4", children: [_jsxs("p", { className: "text-aurora-muted", children: [String(record.description ?? record.name ?? ""), " - High quality product."] }), featuresList.length > 0 && (_jsx("ul", { className: "list-disc list-inside space-y-1", children: featuresList.map((f, i) => (_jsx("li", { children: String(f) }, i))) })), storageInstructions && (_jsxs("div", { children: [_jsx("h4", { className: "font-semibold mb-2", children: "Storage Instructions" }), _jsx("p", { className: "text-aurora-muted text-sm", children: storageInstructions })] }))] })), active === "nutrition" && (_jsx("p", { className: "text-aurora-muted", children: "Nutrition information will be displayed here when available." })), active === "feedback" && (_jsx("p", { className: "text-aurora-muted", children: "Customer reviews will be displayed here when available." }))] })] }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type ProductImageProps = {
|
|
2
|
+
src?: string | null;
|
|
3
|
+
alt?: string;
|
|
4
|
+
baseUrl?: string | null;
|
|
5
|
+
className?: string;
|
|
6
|
+
fallback?: React.ReactNode;
|
|
7
|
+
/** For record-based usage - pass record to extract url */
|
|
8
|
+
record?: Record<string, unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* How to fit the image in its container when aspect ratios differ.
|
|
11
|
+
* - "contain": Preserve aspect ratio, show full image (may letterbox). Use for product thumbs with mixed aspect ratios.
|
|
12
|
+
* - "cover": Fill container, crop if needed. Default.
|
|
13
|
+
*/
|
|
14
|
+
objectFit?: "cover" | "contain";
|
|
15
|
+
/**
|
|
16
|
+
* When true, requests CDN-optimized thumbnail size for Contentful URLs (reduces payload for small displays).
|
|
17
|
+
*/
|
|
18
|
+
thumbnail?: boolean;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Product image with onError fallback to avoid broken image icons.
|
|
22
|
+
* Uses imageBaseUrl from store config (or baseUrl prop) for relative URLs.
|
|
23
|
+
* Use objectFit="contain" for product cards to preserve portrait/landscape aspect ratios.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ProductImage({ src, alt, baseUrl, className, fallback, record, objectFit, thumbnail, }: ProductImageProps): import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=ProductImage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductImage.d.ts","sourceRoot":"","sources":["../../src/components/ProductImage.tsx"],"names":[],"mappings":"AAUA,KAAK,iBAAiB,GAAG;IACvB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAChC;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,CAAC;AAgBF;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAC3B,GAAG,EACH,GAAQ,EACR,OAAO,EACP,SAAS,EACT,QAA2B,EAC3B,MAAM,EACN,SAAmB,EACnB,SAAiB,GAClB,EAAE,iBAAiB,2CAiCnB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { resolveProductImageUrl, getImageUrlFromRecord, getThumbnailImageUrl, } from "../lib/image-url";
|
|
5
|
+
import { useStoreConfigImageBase } from "./StoreConfigContext";
|
|
6
|
+
const DEFAULT_FALLBACK = (_jsx("span", { className: "w-full h-full flex items-center justify-center text-aurora-muted text-2xl", "aria-hidden": true, children: "\u2013" }));
|
|
7
|
+
const objectFitClass = {
|
|
8
|
+
cover: "object-cover",
|
|
9
|
+
contain: "object-contain",
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Product image with onError fallback to avoid broken image icons.
|
|
13
|
+
* Uses imageBaseUrl from store config (or baseUrl prop) for relative URLs.
|
|
14
|
+
* Use objectFit="contain" for product cards to preserve portrait/landscape aspect ratios.
|
|
15
|
+
*/
|
|
16
|
+
export function ProductImage({ src, alt = "", baseUrl, className, fallback = DEFAULT_FALLBACK, record, objectFit = "cover", thumbnail = false, }) {
|
|
17
|
+
const [errored, setErrored] = useState(false);
|
|
18
|
+
const configBase = useStoreConfigImageBase();
|
|
19
|
+
const rawUrl = record !== undefined ? getImageUrlFromRecord(record) : src;
|
|
20
|
+
let resolved = resolveProductImageUrl(rawUrl, baseUrl ?? configBase);
|
|
21
|
+
if (resolved && thumbnail) {
|
|
22
|
+
resolved = getThumbnailImageUrl(resolved) ?? resolved;
|
|
23
|
+
}
|
|
24
|
+
const fitClass = objectFitClass[objectFit];
|
|
25
|
+
const base = (className ?? "w-full h-full")
|
|
26
|
+
.replace(/\bobject-(cover|contain)\b/g, "")
|
|
27
|
+
.trim();
|
|
28
|
+
const mergedClassName = base ? `${base} ${fitClass}`.trim() : `w-full h-full ${fitClass}`;
|
|
29
|
+
if (!resolved || errored) {
|
|
30
|
+
return (_jsx("div", { className: "w-full h-full flex items-center justify-center min-h-[1px]", children: fallback }));
|
|
31
|
+
}
|
|
32
|
+
return (_jsx("img", { src: resolved, alt: alt, className: mergedClassName, onError: () => setErrored(true), suppressHydrationWarning: true }));
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductImageGallery.d.ts","sourceRoot":"","sources":["../../src/components/ProductImageGallery.tsx"],"names":[],"mappings":"AAiBA,KAAK,wBAAwB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,EAAE,MAAM,EAAE,EAAE,wBAAwB,2CA+CvE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ProductImage } from "./ProductImage";
|
|
5
|
+
function getImageUrls(record) {
|
|
6
|
+
const images = record.images;
|
|
7
|
+
if (Array.isArray(images) && images.length > 0) {
|
|
8
|
+
return [...new Set(images.filter((u) => typeof u === "string"))];
|
|
9
|
+
}
|
|
10
|
+
const primary = ["image_url", "image", "thumbnail", "photo"].find((f) => record[f]);
|
|
11
|
+
const primaryUrl = primary ? String(record[primary]) : null;
|
|
12
|
+
const extras = ["image_2", "image_3", "image_4"].filter((f) => record[f]).map((f) => String(record[f]));
|
|
13
|
+
if (primaryUrl)
|
|
14
|
+
return [primaryUrl, ...extras];
|
|
15
|
+
return extras;
|
|
16
|
+
}
|
|
17
|
+
export function ProductImageGallery({ record }) {
|
|
18
|
+
const urls = getImageUrls(record);
|
|
19
|
+
const [selected, setSelected] = useState(0);
|
|
20
|
+
if (urls.length === 0) {
|
|
21
|
+
return (_jsx("div", { className: "pattern-well rounded-component overflow-hidden aspect-square flex items-center justify-center text-aurora-muted text-6xl", children: "-" }));
|
|
22
|
+
}
|
|
23
|
+
const mainUrl = urls[selected] ?? urls[0];
|
|
24
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsx("div", { className: "pattern-well rounded-xl overflow-hidden aspect-square shadow-sm ring-1 ring-aurora-border/50 p-4", children: _jsx(ProductImage, { src: mainUrl, className: "w-full h-full object-contain cursor-zoom-in", fallback: _jsx("span", { className: "w-full h-full flex items-center justify-center text-aurora-muted text-4xl", children: "-" }) }) }), urls.length > 1 && (_jsx("div", { className: "flex gap-2 overflow-x-auto pb-1", children: urls.map((url, i) => (_jsx("button", { type: "button", onClick: () => setSelected(i), className: `shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 bg-aurora-surface-hover transition-colors ${selected === i ? "border-aurora-primary" : "border-aurora-border hover:border-aurora-primary/50"}`, children: _jsx(ProductImage, { src: url, className: "w-full h-full object-contain", fallback: _jsx("span", { className: "w-full h-full flex items-center justify-center text-aurora-muted text-sm", children: "-" }) }) }, i))) }))] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function SearchDropdown({ vendorId, placeholder, fullWidth, excludeDietary, getRecipeSuggestion, }: {
|
|
2
|
+
vendorId?: string;
|
|
3
|
+
placeholder?: string;
|
|
4
|
+
/** When true, input fills container (e.g. for CommandSurface hero). */
|
|
5
|
+
fullWidth?: boolean;
|
|
6
|
+
/** Dietary exclusions for search (e.g. from app context). */
|
|
7
|
+
excludeDietary?: string[];
|
|
8
|
+
/** Optional grocery-style recipe line suggestion from query (e.g. cart-intelligence). */
|
|
9
|
+
getRecipeSuggestion?: (query: string) => string | null;
|
|
10
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=SearchDropdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SearchDropdown.d.ts","sourceRoot":"","sources":["../../src/components/SearchDropdown.tsx"],"names":[],"mappings":"AA4BA,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,WAA4C,EAC5C,SAAS,EACT,cAAmB,EACnB,mBAAmB,GACpB,EAAE;IACD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,yFAAyF;IACzF,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CACxD,2CA0RA"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { Search } from "lucide-react";
|
|
6
|
+
import { formatPrice, toCents } from "../lib/format-price";
|
|
7
|
+
import { search } from "../lib/aurora";
|
|
8
|
+
import { holmesSearch } from "../lib/holmes-events";
|
|
9
|
+
import { useCart } from "./CartProvider";
|
|
10
|
+
import { ProductImage } from "./ProductImage";
|
|
11
|
+
const RECENT_KEY = "aurora-search-recent";
|
|
12
|
+
const RECENT_MAX = 5;
|
|
13
|
+
function loadRecent() {
|
|
14
|
+
if (typeof window === "undefined")
|
|
15
|
+
return [];
|
|
16
|
+
try {
|
|
17
|
+
const stored = localStorage.getItem(RECENT_KEY);
|
|
18
|
+
return stored ? JSON.parse(stored) : [];
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function saveRecent(terms) {
|
|
25
|
+
if (typeof window === "undefined")
|
|
26
|
+
return;
|
|
27
|
+
localStorage.setItem(RECENT_KEY, JSON.stringify(terms.slice(0, RECENT_MAX)));
|
|
28
|
+
}
|
|
29
|
+
export function SearchDropdown({ vendorId, placeholder = "Search milk, bananas, pasta…", fullWidth, excludeDietary = [], getRecipeSuggestion, }) {
|
|
30
|
+
const [query, setQuery] = useState("");
|
|
31
|
+
const [hits, setHits] = useState([]);
|
|
32
|
+
const [loading, setLoading] = useState(false);
|
|
33
|
+
const [open, setOpen] = useState(false);
|
|
34
|
+
const [recentSearches, setRecentSearches] = useState([]);
|
|
35
|
+
const debounceRef = useRef(null);
|
|
36
|
+
const containerRef = useRef(null);
|
|
37
|
+
const { addItem } = useCart();
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
setRecentSearches(loadRecent());
|
|
40
|
+
}, []);
|
|
41
|
+
const addToRecent = useCallback((term) => {
|
|
42
|
+
const t = term.trim().toLowerCase();
|
|
43
|
+
if (!t)
|
|
44
|
+
return;
|
|
45
|
+
setRecentSearches((prev) => {
|
|
46
|
+
const next = [t, ...prev.filter((x) => x !== t)].slice(0, RECENT_MAX);
|
|
47
|
+
saveRecent(next);
|
|
48
|
+
return next;
|
|
49
|
+
});
|
|
50
|
+
}, []);
|
|
51
|
+
const doSearch = useCallback(async (q) => {
|
|
52
|
+
if (!q.trim()) {
|
|
53
|
+
setHits([]);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
holmesSearch(q.trim());
|
|
57
|
+
setLoading(true);
|
|
58
|
+
try {
|
|
59
|
+
const res = await search({
|
|
60
|
+
q: q.trim(),
|
|
61
|
+
limit: 12,
|
|
62
|
+
vendorId,
|
|
63
|
+
excludeDietary: excludeDietary.length ? excludeDietary : undefined,
|
|
64
|
+
});
|
|
65
|
+
setHits(res.hits ?? []);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
setHits([]);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
}, [vendorId, excludeDietary]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (debounceRef.current)
|
|
76
|
+
clearTimeout(debounceRef.current);
|
|
77
|
+
if (!query.trim()) {
|
|
78
|
+
setHits([]);
|
|
79
|
+
if (!open)
|
|
80
|
+
return;
|
|
81
|
+
setOpen(true);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
setOpen(true);
|
|
85
|
+
debounceRef.current = setTimeout(() => doSearch(query), 180);
|
|
86
|
+
return () => {
|
|
87
|
+
if (debounceRef.current)
|
|
88
|
+
clearTimeout(debounceRef.current);
|
|
89
|
+
};
|
|
90
|
+
}, [query, doSearch, open]);
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
function handleClickOutside(e) {
|
|
93
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
94
|
+
setOpen(false);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
98
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
99
|
+
}, []);
|
|
100
|
+
const categories = [
|
|
101
|
+
...new Set(hits
|
|
102
|
+
.map((h) => h.category_name ?? h.category)
|
|
103
|
+
.filter(Boolean)),
|
|
104
|
+
].slice(0, 3);
|
|
105
|
+
const brands = [
|
|
106
|
+
...new Set(hits
|
|
107
|
+
.map((h) => h.brand ?? h.brand_name)
|
|
108
|
+
.filter(Boolean)),
|
|
109
|
+
].slice(0, 3);
|
|
110
|
+
const handleProductSelect = (hit, quickAdd) => {
|
|
111
|
+
addToRecent(query);
|
|
112
|
+
if (quickAdd && hit.price != null && Number(hit.price) > 0 && hit.tableSlug) {
|
|
113
|
+
addItem({
|
|
114
|
+
recordId: hit.recordId,
|
|
115
|
+
tableSlug: hit.tableSlug,
|
|
116
|
+
name: hit.name ?? hit.title ?? hit.snippet ?? hit.recordId ?? "",
|
|
117
|
+
unitAmount: toCents(hit.price) ?? 0,
|
|
118
|
+
imageUrl: hit.image_url,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const showRecent = open && query.trim() && !loading && hits.length === 0 && recentSearches.length > 0;
|
|
123
|
+
const recipeSuggestion = getRecipeSuggestion?.(query) ?? null;
|
|
124
|
+
const showRecipeSuggestion = open && query.trim().length >= 2 && recipeSuggestion && !loading;
|
|
125
|
+
return (_jsxs("div", { ref: containerRef, className: `relative w-full ${fullWidth ? "" : "max-w-[280px]"}`, children: [_jsxs("div", { className: "relative", children: [_jsx(Search, { className: "absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-aurora-muted" }), _jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), onFocus: () => (query.trim() || recentSearches.length) && setOpen(true), placeholder: placeholder, className: "w-full pl-9 pr-3 py-2 h-9 text-sm rounded-lg bg-aurora-surface border border-aurora-border text-aurora-text placeholder:text-aurora-muted focus:outline-none focus:ring-1 focus:ring-aurora-primary/50 focus:border-aurora-primary/70", "aria-label": "Search products" })] }), open && (query.trim() || showRecent) && (_jsx("div", { className: "absolute top-full left-0 right-0 mt-1 rounded-component bg-aurora-surface border border-aurora-border shadow-xl z-[9999] max-h-96 overflow-y-auto", children: loading ? (_jsx("div", { className: "p-4 text-aurora-muted text-sm", children: "Searching\u2026" })) : hits.length === 0 && !showRecent && !showRecipeSuggestion ? (_jsx("div", { className: "p-4 text-aurora-muted text-sm", children: "No results" })) : (_jsxs("div", { className: "py-2", children: [showRecipeSuggestion && (_jsx("div", { className: "px-3 py-1.5 text-xs font-semibold uppercase tracking-wider text-aurora-muted border-b border-aurora-border", children: "Suggestions" })), showRecipeSuggestion && (_jsxs(Link, { href: `/catalogue?q=${encodeURIComponent(recipeSuggestion.replace("?", "").split(" ")[0].toLowerCase())}`, onClick: () => {
|
|
126
|
+
addToRecent(recipeSuggestion);
|
|
127
|
+
setOpen(false);
|
|
128
|
+
}, className: "flex items-center gap-3 px-4 py-2 hover:bg-aurora-surface-hover transition-colors border-b border-aurora-border", children: [_jsx(Search, { className: "w-4 h-4 text-aurora-muted shrink-0" }), _jsx("span", { className: "font-medium truncate", children: recipeSuggestion })] })), hits.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { className: "px-3 py-1.5 text-xs font-semibold uppercase tracking-wider text-aurora-muted border-b border-aurora-border", children: "Products" }), _jsx("ul", { children: hits.slice(0, 6).map((hit) => (_jsx("li", { className: "group/item", children: _jsxs("div", { className: "flex items-center gap-3 px-4 py-2 hover:bg-aurora-surface-hover transition-colors", children: [_jsxs(Link, { href: `/catalogue/${hit.recordId}`, onClick: () => {
|
|
129
|
+
handleProductSelect(hit);
|
|
130
|
+
setOpen(false);
|
|
131
|
+
}, className: "flex items-center gap-3 flex-1 min-w-0", children: [_jsx("div", { className: "w-10 h-10 rounded-component bg-aurora-surface-hover shrink-0 overflow-hidden", children: _jsx(ProductImage, { src: hit.image_url, className: "w-full h-full", objectFit: "contain", thumbnail: true, fallback: _jsx("div", { className: "w-full h-full flex items-center justify-center text-aurora-muted text-xs", children: "-" }) }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "font-medium truncate", children: hit.name ?? hit.title ?? hit.snippet ?? hit.recordId }), hit.price != null && Number(hit.price) > 0 && (_jsx("p", { className: "text-sm text-aurora-primary font-semibold", children: formatPrice(toCents(hit.price) ?? 0) }))] })] }), hit.price != null && Number(hit.price) > 0 && (_jsx("button", { type: "button", onClick: (e) => {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
handleProductSelect(hit, true);
|
|
134
|
+
setOpen(false);
|
|
135
|
+
}, className: "shrink-0 px-3 py-1.5 rounded-lg bg-aurora-primary text-white text-xs font-medium hover:bg-aurora-primary-dark transition-colors opacity-0 group-hover/item:opacity-100", children: "Quick add" }))] }) }, `${hit.tableSlug}-${hit.recordId}`))) }), categories.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { className: "px-3 py-1.5 text-xs font-semibold uppercase tracking-wider text-aurora-muted border-t border-b border-aurora-border mt-1", children: "Categories" }), _jsx("ul", { children: categories.map((cat) => (_jsx("li", { children: _jsx(Link, { href: `/catalogue?category=${encodeURIComponent(String(cat).toLowerCase().replace(/\s+/g, "-"))}`, onClick: () => {
|
|
136
|
+
addToRecent(query);
|
|
137
|
+
setOpen(false);
|
|
138
|
+
}, className: "flex items-center gap-3 px-4 py-2 hover:bg-aurora-surface-hover transition-colors", children: _jsx("span", { className: "font-medium truncate", children: cat }) }) }, cat))) })] })), brands.length > 0 && (_jsxs(_Fragment, { children: [_jsx("div", { className: "px-3 py-1.5 text-xs font-semibold uppercase tracking-wider text-aurora-muted border-t border-b border-aurora-border mt-1", children: "Brands" }), _jsx("ul", { children: brands.map((brand) => (_jsx("li", { children: _jsx(Link, { href: `/catalogue?q=${encodeURIComponent(brand)}`, onClick: () => {
|
|
139
|
+
addToRecent(query);
|
|
140
|
+
setOpen(false);
|
|
141
|
+
}, className: "flex items-center gap-3 px-4 py-2 hover:bg-aurora-surface-hover transition-colors", children: _jsx("span", { className: "font-medium truncate", children: brand }) }) }, brand))) })] }))] })), showRecent && (_jsxs(_Fragment, { children: [_jsx("div", { className: "px-3 py-1.5 text-xs font-semibold uppercase tracking-wider text-aurora-muted border-b border-aurora-border", children: "Recent searches" }), _jsx("ul", { children: recentSearches.map((term) => (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => {
|
|
142
|
+
setQuery(term);
|
|
143
|
+
doSearch(term);
|
|
144
|
+
}, className: "w-full text-left flex items-center gap-3 px-4 py-2 hover:bg-aurora-surface-hover transition-colors", children: [_jsx(Search, { className: "w-4 h-4 text-aurora-muted shrink-0" }), _jsx("span", { className: "font-medium truncate", children: term })] }) }, term))) })] }))] })) }))] }));
|
|
145
|
+
}
|