@snowcone-app/ui 0.1.43 → 0.2.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/CHANGELOG.md +26 -0
- package/README.md +18 -4
- package/package.json +9 -5
- package/src/components/CanvasIsolationBoundary.tsx +202 -0
- package/src/components/LoadingOverlayPrism.tsx +251 -0
- package/src/composed/AddToCart.tsx +229 -0
- package/src/composed/ArtAlignment.tsx +703 -0
- package/src/composed/ArtSelector.tsx +290 -0
- package/src/composed/ArtworkCustomizer.tsx +212 -0
- package/src/composed/CanvasEditor.tsx +79 -0
- package/src/composed/ColorPicker.tsx +111 -0
- package/src/composed/CurrentSelectionDisplay.tsx +86 -0
- package/src/composed/HeroProductImage.tsx +1071 -0
- package/src/composed/Lightbox.index.ts +2 -0
- package/src/composed/Lightbox.tsx +230 -0
- package/src/composed/PlacementClipShapeSelector.tsx +88 -0
- package/src/composed/PlacementTabs.tsx +179 -0
- package/src/composed/ProductCard.tsx +298 -0
- package/src/composed/ProductGallery.tsx +54 -0
- package/src/composed/ProductImage.tsx +129 -0
- package/src/composed/ProductList.tsx +147 -0
- package/src/composed/ProductOptions.tsx +305 -0
- package/src/composed/RealtimeMockup.tsx +121 -0
- package/src/composed/TileCount.tsx +348 -0
- package/src/composed/carousels/HeroCarousel.tsx +240 -0
- package/src/composed/carousels/MobileProductCarousel.tsx +1002 -0
- package/src/composed/carousels/index.ts +11 -0
- package/src/composed/carousels/types.ts +58 -0
- package/src/composed/grids/MasonryGrid.tsx +238 -0
- package/src/composed/grids/index.ts +9 -0
- package/src/composed/search/CurrentRefinements.tsx +80 -0
- package/src/composed/search/Filters.tsx +49 -0
- package/src/composed/search/FiltersButton.tsx +57 -0
- package/src/composed/search/FiltersDrawer.tsx +375 -0
- package/src/composed/search/ProductGrid.tsx +118 -0
- package/src/composed/search/ProductHit.tsx +56 -0
- package/src/composed/search/SearchBox.tsx +109 -0
- package/src/composed/search/SearchProvider.tsx +136 -0
- package/src/composed/search/facetConfig.ts +16 -0
- package/src/composed/search/index.ts +22 -0
- package/src/composed/search/meilisearchAdapter.ts +20 -0
- package/src/composed/search/types.ts +22 -0
- package/src/composed/zoom/EnhancedImageViewer.tsx +505 -0
- package/src/composed/zoom/ResponsiveZoom.tsx +134 -0
- package/src/composed/zoom/ZoomOverlay.tsx +194 -0
- package/src/composed/zoom/index.ts +12 -0
- package/src/composed/zoom/types.ts +12 -0
- package/src/design-system/ColorPalette.tsx +126 -0
- package/src/design-system/ColorSwatch.tsx +49 -0
- package/src/design-system/DesignSystemPage.tsx +130 -0
- package/src/design-system/ThemeSwitcher.tsx +181 -0
- package/src/design-system/TypographyScale.tsx +106 -0
- package/src/design-system/index.ts +5 -0
- package/src/ecommerce/stories/HeroProductImage.stories.tsx +66 -0
- package/src/ecommerce/stories/PDPHeroGallery.stories.tsx +105 -0
- package/src/ecommerce/stories/PDPInfoPanel.stories.tsx +472 -0
- package/src/ecommerce/stories/PDPLayout.stories.tsx +365 -0
- package/src/hooks/useBrand.ts +41 -0
- package/src/hooks/useCanvasContext.ts +127 -0
- package/src/hooks/useDeviceDetection.ts +64 -0
- package/src/hooks/useFocusTrap.ts +70 -0
- package/src/hooks/useImagePreloader.ts +268 -0
- package/src/hooks/useImageTransition.ts +608 -0
- package/src/hooks/usePlacementsProcessor.ts +74 -0
- package/src/hooks/useProductGallery.ts +193 -0
- package/src/hooks/useProductPage.ts +467 -0
- package/src/hooks/useRenderGuard.ts +96 -0
- package/src/hooks/useScrollDirection.ts +196 -0
- package/src/hooks/viewport/index.ts +25 -0
- package/src/hooks/viewport/useContainerWidth.ts +59 -0
- package/src/hooks/viewport/useMediaQuery.ts +52 -0
- package/src/hooks/viewport/useResponsiveImageCap.ts +149 -0
- package/src/hooks/viewport/useViewportDimensions.ts +135 -0
- package/src/hooks/viewport/useWideMonitorMode.ts +150 -0
- package/src/hooks/visibility/index.ts +15 -0
- package/src/hooks/visibility/observerPool.ts +150 -0
- package/src/index.ts +240 -0
- package/src/layouts/hero-zoom/HeroShrinkLayout.tsx +209 -0
- package/src/layouts/hero-zoom/HeroZoomLayout.tsx +351 -0
- package/src/layouts/hero-zoom/index.ts +30 -0
- package/src/layouts/hero-zoom/stories/HeroZoomLayout.stories.tsx +350 -0
- package/src/layouts/hero-zoom/types.ts +113 -0
- package/src/layouts/hero-zoom/useHeroZoomScales.ts +156 -0
- package/src/layouts/index.ts +9 -0
- package/src/layouts/pdp/EdgeBlurBox.tsx +210 -0
- package/src/layouts/pdp/ImageBlurExtension.tsx +215 -0
- package/src/layouts/pdp/ImageEdgeBlur.tsx +215 -0
- package/src/layouts/pdp/PDPLayout.tsx +246 -0
- package/src/layouts/pdp/SimpleImageBlur.tsx +140 -0
- package/src/layouts/pdp/index.ts +40 -0
- package/src/lib/env.ts +15 -0
- package/src/lib/locale.ts +167 -0
- package/src/lib/router.tsx +46 -0
- package/src/lib/utils.ts +6 -0
- package/src/lightbox/README.md +77 -0
- package/src/next/index.tsx +26 -0
- package/src/patterns/MockupPriorityProvider.tsx +1014 -0
- package/src/patterns/Product.tsx +850 -0
- package/src/patterns/ProductPageProvider.tsx +224 -0
- package/src/patterns/RealtimeProvider.tsx +1162 -0
- package/src/patterns/ShopProvider.tsx +603 -0
- package/src/personalization/PersonalizationBridge.tsx +235 -0
- package/src/personalization/PersonalizationContext.ts +29 -0
- package/src/personalization/PersonalizationInputs.tsx +110 -0
- package/src/personalization/PersonalizationProvider.tsx +407 -0
- package/src/personalization/canvas-stub.d.ts +22 -0
- package/src/personalization/index.ts +43 -0
- package/src/personalization/types.ts +48 -0
- package/src/personalization/usePersonalization.ts +32 -0
- package/src/personalization/usePersonalizationShimmer.ts +159 -0
- package/src/personalization/utils.ts +59 -0
- package/src/primitives/BrandLogo.tsx +65 -0
- package/src/primitives/BrandName.tsx +51 -0
- package/src/primitives/Button.tsx +123 -0
- package/src/primitives/ColorSwatch.tsx +221 -0
- package/src/primitives/DragHintAnimation.tsx +190 -0
- package/src/primitives/EdgeSwipeGuards.tsx +60 -0
- package/src/primitives/FloatingActionGroup.tsx +176 -0
- package/src/primitives/ProductPrice.tsx +171 -0
- package/src/primitives/ProgressiveBlur.tsx +295 -0
- package/src/primitives/ThemeToggle.tsx +125 -0
- package/src/primitives/__tests__/story-coverage.test.ts +98 -0
- package/src/primitives/accordion.tsx +280 -0
- package/src/primitives/badge.tsx +137 -0
- package/src/primitives/card.tsx +61 -0
- package/src/primitives/checkbox.tsx +56 -0
- package/src/primitives/collapsible.tsx +51 -0
- package/src/primitives/drawer.tsx +828 -0
- package/src/primitives/dropdown-menu.tsx +197 -0
- package/src/primitives/fieldset.tsx +73 -0
- package/src/primitives/index.ts +138 -0
- package/src/primitives/input.tsx +91 -0
- package/src/primitives/kbd.tsx +130 -0
- package/src/primitives/label.tsx +20 -0
- package/src/primitives/link.tsx +182 -0
- package/src/primitives/popover.tsx +80 -0
- package/src/primitives/radio-group.tsx +79 -0
- package/src/primitives/scroll-fade.tsx +159 -0
- package/src/primitives/select.tsx +170 -0
- package/src/primitives/separator.tsx +25 -0
- package/src/primitives/slider.tsx +221 -0
- package/src/primitives/spinner.tsx +72 -0
- package/src/primitives/stories/Accordion.stories.tsx +121 -0
- package/src/primitives/stories/Badge.stories.tsx +221 -0
- package/src/primitives/stories/Button.stories.tsx +185 -0
- package/src/primitives/stories/Card.stories.tsx +171 -0
- package/src/primitives/stories/Checkbox.stories.tsx +214 -0
- package/src/primitives/stories/Collapsible.stories.tsx +230 -0
- package/src/primitives/stories/Drawer.stories.tsx +378 -0
- package/src/primitives/stories/DropdownMenu.stories.tsx +182 -0
- package/src/primitives/stories/Fieldset.stories.tsx +212 -0
- package/src/primitives/stories/Input.stories.tsx +172 -0
- package/src/primitives/stories/Kbd.stories.tsx +183 -0
- package/src/primitives/stories/Label.stories.tsx +98 -0
- package/src/primitives/stories/Link.stories.tsx +260 -0
- package/src/primitives/stories/Popover.stories.tsx +178 -0
- package/src/primitives/stories/RadioGroup.stories.tsx +205 -0
- package/src/primitives/stories/Select.stories.tsx +222 -0
- package/src/primitives/stories/Separator.stories.tsx +134 -0
- package/src/primitives/stories/Slider.stories.tsx +203 -0
- package/src/primitives/stories/Spinner.stories.tsx +142 -0
- package/src/primitives/stories/Surface.stories.tsx +257 -0
- package/src/primitives/stories/Switch.stories.tsx +131 -0
- package/src/primitives/stories/Tabs.stories.tsx +275 -0
- package/src/primitives/stories/TextField.stories.tsx +139 -0
- package/src/primitives/stories/Textarea.stories.tsx +148 -0
- package/src/primitives/stories/Tooltip.stories.tsx +119 -0
- package/src/primitives/surface.tsx +86 -0
- package/src/primitives/switch.tsx +35 -0
- package/src/primitives/tabs.tsx +206 -0
- package/src/primitives/text-field.tsx +84 -0
- package/src/primitives/textarea.tsx +50 -0
- package/src/primitives/tooltip.tsx +58 -0
- package/src/services/CanvasExportService.ts +518 -0
- package/src/styles/base.css +380 -0
- package/src/styles/defaults.css +280 -0
- package/src/styles/globals.css +1242 -0
- package/src/styles/index.css +17 -0
- package/src/styles/ne-themes.css +4740 -0
- package/src/styles/tailwind.css +11 -0
- package/src/styles/tokens.css +117 -0
- package/src/styles/utilities.css +188 -0
- package/src/themes/apply-theme.ts +449 -0
- package/src/themes/getThemeStyles.ts +454 -0
- package/src/themes/index.ts +48 -0
- package/src/themes/oklch-theme.ts +283 -0
- package/src/themes/presets.ts +989 -0
- package/src/themes/types.ts +386 -0
- package/src/themes/useTheme.tsx +450 -0
- package/src/utils/dev-warnings.ts +161 -0
- package/src/utils/devWarnings.ts +153 -0
- package/dist/styles.css +0 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useMemo, useCallback } from "react";
|
|
4
|
+
import type { CatalogProduct } from "@snowcone-app/sdk";
|
|
5
|
+
import { getProduct, listProducts } from "@snowcone-app/sdk";
|
|
6
|
+
import { createDevFetcher } from "@snowcone-app/sdk/dev-fetcher";
|
|
7
|
+
|
|
8
|
+
export interface UseProductGalleryOptions {
|
|
9
|
+
endpoint?: string;
|
|
10
|
+
mode?: "mock" | "live";
|
|
11
|
+
limit?: number; // For listProducts when no productIds provided
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseProductGalleryReturn {
|
|
15
|
+
products: CatalogProduct[];
|
|
16
|
+
loading: boolean;
|
|
17
|
+
error: string | null;
|
|
18
|
+
refetch: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* useProductGallery - Fetch and manage product collections
|
|
23
|
+
*
|
|
24
|
+
* A React hook for fetching either specific products by ID or listing all
|
|
25
|
+
* available products. Handles loading states, errors, and provides refetch
|
|
26
|
+
* capability. Perfect for product catalogs, related products, and galleries.
|
|
27
|
+
*
|
|
28
|
+
* Features:
|
|
29
|
+
* - Fetch specific products by ID array
|
|
30
|
+
* - List all products (empty array)
|
|
31
|
+
* - Loading and error states
|
|
32
|
+
* - Refetch capability
|
|
33
|
+
* - Mock/live mode support
|
|
34
|
+
* - Automatic deduplication via memoization
|
|
35
|
+
* - Limit for product lists
|
|
36
|
+
*
|
|
37
|
+
* **Modes:**
|
|
38
|
+
* - Specific products: Pass array of IDs `['id1', 'id2']`
|
|
39
|
+
* - List all: Pass empty array `[]` or omit parameter
|
|
40
|
+
*
|
|
41
|
+
* **Use Cases:**
|
|
42
|
+
* - Product catalog pages
|
|
43
|
+
* - Related/recommended products
|
|
44
|
+
* - Search results
|
|
45
|
+
* - Recently viewed
|
|
46
|
+
* - User favorites
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* // Fetch specific products
|
|
51
|
+
* const { products, loading, error, refetch } = useProductGallery(
|
|
52
|
+
* ['BEEB77', 'AR2P3G'],
|
|
53
|
+
* { endpoint: 'http://localhost:3000', mode: 'mock' }
|
|
54
|
+
* );
|
|
55
|
+
*
|
|
56
|
+
* if (loading) return <Spinner />;
|
|
57
|
+
* if (error) return <Error message={error} onRetry={refetch} />;
|
|
58
|
+
*
|
|
59
|
+
* return (
|
|
60
|
+
* <div className="grid grid-cols-3 gap-4">
|
|
61
|
+
* {products.map(product => (
|
|
62
|
+
* <ProductCard key={product.id} product={product} />
|
|
63
|
+
* ))}
|
|
64
|
+
* </div>
|
|
65
|
+
* );
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* // List all products with limit
|
|
71
|
+
* const { products, loading } = useProductGallery(
|
|
72
|
+
* [], // Empty array = list all
|
|
73
|
+
* { mode: 'mock', limit: 12 }
|
|
74
|
+
* );
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```tsx
|
|
79
|
+
* // Use in Remix loader
|
|
80
|
+
* export async function loader() {
|
|
81
|
+
* // Note: In loaders, use SDK functions directly
|
|
82
|
+
* // This hook is for client components
|
|
83
|
+
* const products = await listProducts({ ... });
|
|
84
|
+
* return json({ products });
|
|
85
|
+
* }
|
|
86
|
+
*
|
|
87
|
+
* // Then in component
|
|
88
|
+
* function ProductGallery() {
|
|
89
|
+
* const { products } = useLoaderData<typeof loader>();
|
|
90
|
+
* // Or use hook for client-side filtering
|
|
91
|
+
* const { products: filtered } = useProductGallery(
|
|
92
|
+
* products.map(p => p.id)
|
|
93
|
+
* );
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* // With error handling and retry
|
|
100
|
+
* function ProductCatalog() {
|
|
101
|
+
* const { products, loading, error, refetch } = useProductGallery([], {
|
|
102
|
+
* mode: 'live',
|
|
103
|
+
* limit: 20
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* if (error) {
|
|
107
|
+
* return (
|
|
108
|
+
* <div>
|
|
109
|
+
* <p>Failed to load: {error}</p>
|
|
110
|
+
* <button onClick={refetch}>Retry</button>
|
|
111
|
+
* </div>
|
|
112
|
+
* );
|
|
113
|
+
* }
|
|
114
|
+
*
|
|
115
|
+
* return <ProductGrid products={products} loading={loading} />;
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* @param productIds - Array of product IDs to fetch (empty array = list all)
|
|
120
|
+
* @param options - Configuration options
|
|
121
|
+
* @param options.endpoint - API endpoint URL
|
|
122
|
+
* @param options.mode - Data source ("mock" or "live", default: "mock")
|
|
123
|
+
* @param options.limit - Max products to return when listing all (default: 10)
|
|
124
|
+
* @returns Object with products array, loading state, error, and refetch function
|
|
125
|
+
*/
|
|
126
|
+
export function useProductGallery(
|
|
127
|
+
productIds: string[] = [], // Make optional - empty array means "list all products"
|
|
128
|
+
options: UseProductGalleryOptions = {}
|
|
129
|
+
): UseProductGalleryReturn {
|
|
130
|
+
const { endpoint, mode = "mock", limit = 10 } = options;
|
|
131
|
+
|
|
132
|
+
const [products, setProducts] = useState<CatalogProduct[]>([]);
|
|
133
|
+
const [loading, setLoading] = useState(true);
|
|
134
|
+
const [error, setError] = useState<string | null>(null);
|
|
135
|
+
|
|
136
|
+
// Memoize the productIds array to prevent infinite loops from array reference changes
|
|
137
|
+
const memoizedProductIds = useMemo(
|
|
138
|
+
() => productIds,
|
|
139
|
+
[JSON.stringify(productIds)]
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const fetchProducts = useCallback(async () => {
|
|
143
|
+
try {
|
|
144
|
+
setLoading(true);
|
|
145
|
+
setError(null);
|
|
146
|
+
|
|
147
|
+
const baseUrl =
|
|
148
|
+
endpoint ||
|
|
149
|
+
(window as any)?.snowcone?.endpoint ||
|
|
150
|
+
"http://192.168.4.32:5175";
|
|
151
|
+
const customFetcher =
|
|
152
|
+
mode === "mock" ? createDevFetcher(baseUrl) : undefined;
|
|
153
|
+
|
|
154
|
+
let fetchedProducts: CatalogProduct[];
|
|
155
|
+
|
|
156
|
+
if (memoizedProductIds.length === 0) {
|
|
157
|
+
// List all products when no specific IDs provided
|
|
158
|
+
const response = await listProducts({
|
|
159
|
+
baseUrl,
|
|
160
|
+
fetcher: customFetcher as any,
|
|
161
|
+
});
|
|
162
|
+
fetchedProducts = response.items.slice(0, limit); // Apply limit
|
|
163
|
+
} else {
|
|
164
|
+
// Fetch specific products by ID
|
|
165
|
+
const productPromises = memoizedProductIds.map((productId) =>
|
|
166
|
+
getProduct(productId, { baseUrl, fetcher: customFetcher as any })
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const results = await Promise.all(productPromises);
|
|
170
|
+
fetchedProducts = results.filter(Boolean) as CatalogProduct[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
setProducts(fetchedProducts);
|
|
174
|
+
} catch (err: any) {
|
|
175
|
+
console.error("Failed to load products:", err);
|
|
176
|
+
setError(err.message || "Failed to load products");
|
|
177
|
+
} finally {
|
|
178
|
+
setLoading(false);
|
|
179
|
+
}
|
|
180
|
+
}, [memoizedProductIds, endpoint, mode, limit]);
|
|
181
|
+
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
// Always fetch - either specific products or list all products
|
|
184
|
+
fetchProducts();
|
|
185
|
+
}, [fetchProducts]);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
products,
|
|
189
|
+
loading,
|
|
190
|
+
error,
|
|
191
|
+
refetch: fetchProducts,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo } from "react";
|
|
4
|
+
import {
|
|
5
|
+
useProduct,
|
|
6
|
+
useProductOptional,
|
|
7
|
+
useDesignOptional,
|
|
8
|
+
type ReactProductContext,
|
|
9
|
+
type Artwork,
|
|
10
|
+
type PlacementDesign,
|
|
11
|
+
type ClipShape,
|
|
12
|
+
} from "../patterns/Product";
|
|
13
|
+
import {
|
|
14
|
+
useShop,
|
|
15
|
+
useShopOptional,
|
|
16
|
+
type ShopContextValue,
|
|
17
|
+
} from "../patterns/ShopProvider";
|
|
18
|
+
import {
|
|
19
|
+
useRealtimeOptional,
|
|
20
|
+
type RealtimeContextValue,
|
|
21
|
+
} from "../patterns/RealtimeProvider";
|
|
22
|
+
import {
|
|
23
|
+
useMockupPriorityOptional,
|
|
24
|
+
type MockupPriorityContextValue,
|
|
25
|
+
} from "../patterns/MockupPriorityProvider";
|
|
26
|
+
import type { CatalogProduct, OptionSelection, ImageAlignment } from "@snowcone-app/sdk";
|
|
27
|
+
|
|
28
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
29
|
+
// PROVIDER STATUS - For helpful error messages
|
|
30
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
31
|
+
|
|
32
|
+
export interface ProviderStatus {
|
|
33
|
+
hasShop: boolean;
|
|
34
|
+
hasProduct: boolean;
|
|
35
|
+
hasRealtime: boolean;
|
|
36
|
+
hasPriority: boolean;
|
|
37
|
+
hasArtwork: boolean;
|
|
38
|
+
missing: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check which providers are available in the current context.
|
|
43
|
+
* Useful for debugging and conditional rendering.
|
|
44
|
+
*/
|
|
45
|
+
export function useProviderStatus(): ProviderStatus {
|
|
46
|
+
const shop = useShopOptional();
|
|
47
|
+
const product = useProductOptional();
|
|
48
|
+
const design = useDesignOptional();
|
|
49
|
+
const realtime = useRealtimeOptional();
|
|
50
|
+
const priority = useMockupPriorityOptional();
|
|
51
|
+
|
|
52
|
+
const missing: string[] = [];
|
|
53
|
+
if (!shop) missing.push("Shop");
|
|
54
|
+
if (!product) missing.push("Product");
|
|
55
|
+
if (!realtime) missing.push("RealtimeProvider");
|
|
56
|
+
if (!priority) missing.push("MockupPriorityProvider");
|
|
57
|
+
if (!design?.selectedArtwork) missing.push("ArtSelector or selectedArtwork");
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
hasShop: !!shop,
|
|
61
|
+
hasProduct: !!product,
|
|
62
|
+
hasRealtime: !!realtime,
|
|
63
|
+
hasPriority: !!priority,
|
|
64
|
+
hasArtwork: !!design?.selectedArtwork,
|
|
65
|
+
missing,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
70
|
+
// PRODUCT PAGE CONTEXT - Unified interface
|
|
71
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
72
|
+
|
|
73
|
+
export interface ProductPageContext {
|
|
74
|
+
// ── Product Data ──────────────────────────────────────────────────────────
|
|
75
|
+
/** The product being displayed */
|
|
76
|
+
product: CatalogProduct | undefined;
|
|
77
|
+
/** Whether product data is loading */
|
|
78
|
+
isLoading: boolean;
|
|
79
|
+
/** Current variant selection (size, color, etc.) */
|
|
80
|
+
selection: OptionSelection;
|
|
81
|
+
/** Update variant selection */
|
|
82
|
+
updateSelection: (selection: OptionSelection) => void;
|
|
83
|
+
/** Current price based on selection */
|
|
84
|
+
currentPrice: number | undefined;
|
|
85
|
+
|
|
86
|
+
// ── Artwork & Design ──────────────────────────────────────────────────────
|
|
87
|
+
/** Currently selected artwork */
|
|
88
|
+
selectedArtwork: Artwork | undefined;
|
|
89
|
+
/** Set the selected artwork */
|
|
90
|
+
setSelectedArtwork: (artwork: Artwork | undefined) => void;
|
|
91
|
+
/** All available artworks */
|
|
92
|
+
artworks: Artwork[];
|
|
93
|
+
/** Add an artwork to the collection */
|
|
94
|
+
addArtwork: (artwork: Artwork) => void;
|
|
95
|
+
/** Placement designs for current product */
|
|
96
|
+
placements: Record<string, PlacementDesign>;
|
|
97
|
+
/** Currently selected placement */
|
|
98
|
+
selectedPlacement: string | undefined;
|
|
99
|
+
/** Set selected placement */
|
|
100
|
+
setSelectedPlacement: (placement: string) => void;
|
|
101
|
+
|
|
102
|
+
// ── Design Methods ────────────────────────────────────────────────────────
|
|
103
|
+
/** Update a placement's design */
|
|
104
|
+
setPlacementDesign: (placement: string, design: Partial<PlacementDesign>) => void;
|
|
105
|
+
/** Get a placement's design */
|
|
106
|
+
getPlacementDesign: (placement: string) => PlacementDesign | undefined;
|
|
107
|
+
/** Apply artwork to a specific placement */
|
|
108
|
+
applyArtworkToPlacement: (placement: string, artwork?: Artwork) => void;
|
|
109
|
+
/** Set clip shape for a placement */
|
|
110
|
+
setPlacementClipShape: (placement: string, clipShape: ClipShape) => void;
|
|
111
|
+
/** Get clip shape for a placement */
|
|
112
|
+
getPlacementClipShape: (placement: string) => ClipShape;
|
|
113
|
+
|
|
114
|
+
// ── Realtime (Optional) ───────────────────────────────────────────────────
|
|
115
|
+
/** Realtime mockup context (if RealtimeProvider is present) */
|
|
116
|
+
realtime: RealtimeContextValue | undefined;
|
|
117
|
+
/** Whether realtime mockups are available */
|
|
118
|
+
hasRealtime: boolean;
|
|
119
|
+
|
|
120
|
+
// ── Priority (Optional) ───────────────────────────────────────────────────
|
|
121
|
+
/** Priority context (if MockupPriorityProvider is present) */
|
|
122
|
+
priority: MockupPriorityContextValue | undefined;
|
|
123
|
+
/** Whether priority system is available */
|
|
124
|
+
hasPriority: boolean;
|
|
125
|
+
|
|
126
|
+
// ── Shop ──────────────────────────────────────────────────────────────────
|
|
127
|
+
/** Queue an image update (throttled) */
|
|
128
|
+
queueImageUpdate: ShopContextValue["queueImageUpdate"];
|
|
129
|
+
/** Cancel a queued image update */
|
|
130
|
+
cancelImageUpdate: ShopContextValue["cancelImageUpdate"];
|
|
131
|
+
/** Get cached product data */
|
|
132
|
+
getCachedProduct: (productId: string) => CatalogProduct | undefined;
|
|
133
|
+
|
|
134
|
+
// ── Provider Status ───────────────────────────────────────────────────────
|
|
135
|
+
/** Check which providers are available */
|
|
136
|
+
providers: ProviderStatus;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* useProductPage - Unified hook for building product pages
|
|
141
|
+
*
|
|
142
|
+
* Combines all necessary contexts (Product, Shop, Realtime, Priority) into
|
|
143
|
+
* a single, easy-to-use interface. This is the recommended way to access
|
|
144
|
+
* context in custom product page components.
|
|
145
|
+
*
|
|
146
|
+
* **Requirements:**
|
|
147
|
+
* - Must be used within a `<Shop>` provider
|
|
148
|
+
* - Must be used within a `<Product>` provider
|
|
149
|
+
* - RealtimeProvider and MockupPriorityProvider are optional
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```tsx
|
|
153
|
+
* function MyProductPage() {
|
|
154
|
+
* const {
|
|
155
|
+
* product,
|
|
156
|
+
* selection,
|
|
157
|
+
* updateSelection,
|
|
158
|
+
* selectedArtwork,
|
|
159
|
+
* setSelectedArtwork,
|
|
160
|
+
* placements,
|
|
161
|
+
* realtime,
|
|
162
|
+
* } = useProductPage();
|
|
163
|
+
*
|
|
164
|
+
* if (!product) return <div>Loading...</div>;
|
|
165
|
+
*
|
|
166
|
+
* return (
|
|
167
|
+
* <div>
|
|
168
|
+
* <h1>{product.name}</h1>
|
|
169
|
+
* <button onClick={() => setSelectedArtwork(myArtwork)}>
|
|
170
|
+
* Set Artwork
|
|
171
|
+
* </button>
|
|
172
|
+
* </div>
|
|
173
|
+
* );
|
|
174
|
+
* }
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* @throws Error if used outside of Shop or Product context
|
|
178
|
+
*/
|
|
179
|
+
export function useProductPage(): ProductPageContext {
|
|
180
|
+
const shop = useShop(); // Required - will throw if missing
|
|
181
|
+
const productCtx = useProduct(); // Required - will throw if missing
|
|
182
|
+
const realtime = useRealtimeOptional();
|
|
183
|
+
const priority = useMockupPriorityOptional();
|
|
184
|
+
|
|
185
|
+
const providers = useProviderStatus();
|
|
186
|
+
|
|
187
|
+
return useMemo(
|
|
188
|
+
() => ({
|
|
189
|
+
// Product Data
|
|
190
|
+
product: productCtx.product,
|
|
191
|
+
isLoading: productCtx.loading ?? false,
|
|
192
|
+
selection: productCtx.selection ?? {},
|
|
193
|
+
updateSelection: productCtx.updateSelection ?? (() => {}),
|
|
194
|
+
currentPrice: productCtx.currentPrice,
|
|
195
|
+
|
|
196
|
+
// Artwork & Design
|
|
197
|
+
selectedArtwork: productCtx.selectedArtwork,
|
|
198
|
+
setSelectedArtwork: productCtx.setSelectedArtwork,
|
|
199
|
+
artworks: shop.artworks,
|
|
200
|
+
addArtwork: shop.addArtwork,
|
|
201
|
+
placements: productCtx.placements,
|
|
202
|
+
selectedPlacement: productCtx.selectedPlacement,
|
|
203
|
+
setSelectedPlacement: productCtx.setSelectedPlacement,
|
|
204
|
+
|
|
205
|
+
// Design Methods
|
|
206
|
+
setPlacementDesign: productCtx.setPlacementDesign,
|
|
207
|
+
getPlacementDesign: productCtx.getPlacementDesign,
|
|
208
|
+
applyArtworkToPlacement: productCtx.applyArtworkToPlacement,
|
|
209
|
+
setPlacementClipShape: productCtx.setPlacementClipShape,
|
|
210
|
+
getPlacementClipShape: productCtx.getPlacementClipShape,
|
|
211
|
+
|
|
212
|
+
// Realtime
|
|
213
|
+
realtime,
|
|
214
|
+
hasRealtime: !!realtime,
|
|
215
|
+
|
|
216
|
+
// Priority
|
|
217
|
+
priority,
|
|
218
|
+
hasPriority: !!priority,
|
|
219
|
+
|
|
220
|
+
// Shop
|
|
221
|
+
queueImageUpdate: shop.queueImageUpdate,
|
|
222
|
+
cancelImageUpdate: shop.cancelImageUpdate,
|
|
223
|
+
getCachedProduct: shop.getProduct,
|
|
224
|
+
|
|
225
|
+
// Provider Status
|
|
226
|
+
providers,
|
|
227
|
+
}),
|
|
228
|
+
[productCtx, shop, realtime, priority, providers]
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* useProductPageOptional - Like useProductPage but returns undefined if contexts missing
|
|
234
|
+
*
|
|
235
|
+
* Use this when you're not sure if you're inside the required providers.
|
|
236
|
+
* Returns undefined instead of throwing an error.
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```tsx
|
|
240
|
+
* function MaybeProductComponent() {
|
|
241
|
+
* const page = useProductPageOptional();
|
|
242
|
+
*
|
|
243
|
+
* if (!page) {
|
|
244
|
+
* return <div>Not inside a product page</div>;
|
|
245
|
+
* }
|
|
246
|
+
*
|
|
247
|
+
* return <div>{page.product?.name}</div>;
|
|
248
|
+
* }
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
export function useProductPageOptional(): ProductPageContext | undefined {
|
|
252
|
+
const shop = useShopOptional();
|
|
253
|
+
const productCtx = useProductOptional();
|
|
254
|
+
const realtime = useRealtimeOptional();
|
|
255
|
+
const priority = useMockupPriorityOptional();
|
|
256
|
+
|
|
257
|
+
const providers = useProviderStatus();
|
|
258
|
+
|
|
259
|
+
if (!shop || !productCtx) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
// Product Data
|
|
265
|
+
product: productCtx.product,
|
|
266
|
+
isLoading: productCtx.loading ?? false,
|
|
267
|
+
selection: productCtx.selection ?? {},
|
|
268
|
+
updateSelection: productCtx.updateSelection ?? (() => {}),
|
|
269
|
+
currentPrice: productCtx.currentPrice,
|
|
270
|
+
|
|
271
|
+
// Artwork & Design
|
|
272
|
+
selectedArtwork: productCtx.selectedArtwork,
|
|
273
|
+
setSelectedArtwork: productCtx.setSelectedArtwork,
|
|
274
|
+
artworks: shop.artworks,
|
|
275
|
+
addArtwork: shop.addArtwork,
|
|
276
|
+
placements: productCtx.placements,
|
|
277
|
+
selectedPlacement: productCtx.selectedPlacement,
|
|
278
|
+
setSelectedPlacement: productCtx.setSelectedPlacement,
|
|
279
|
+
|
|
280
|
+
// Design Methods
|
|
281
|
+
setPlacementDesign: productCtx.setPlacementDesign,
|
|
282
|
+
getPlacementDesign: productCtx.getPlacementDesign,
|
|
283
|
+
applyArtworkToPlacement: productCtx.applyArtworkToPlacement,
|
|
284
|
+
setPlacementClipShape: productCtx.setPlacementClipShape,
|
|
285
|
+
getPlacementClipShape: productCtx.getPlacementClipShape,
|
|
286
|
+
|
|
287
|
+
// Realtime
|
|
288
|
+
realtime,
|
|
289
|
+
hasRealtime: !!realtime,
|
|
290
|
+
|
|
291
|
+
// Priority
|
|
292
|
+
priority,
|
|
293
|
+
hasPriority: !!priority,
|
|
294
|
+
|
|
295
|
+
// Shop
|
|
296
|
+
queueImageUpdate: shop.queueImageUpdate,
|
|
297
|
+
cancelImageUpdate: shop.cancelImageUpdate,
|
|
298
|
+
getCachedProduct: shop.getProduct,
|
|
299
|
+
|
|
300
|
+
// Provider Status
|
|
301
|
+
providers,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
306
|
+
// PROJECTION HOOKS - Simplified access to nested state
|
|
307
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* useCurrentPlacements - Get placements for the current product
|
|
311
|
+
*
|
|
312
|
+
* Simplified access to placement data without dealing with nested state.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* ```tsx
|
|
316
|
+
* const placements = useCurrentPlacements();
|
|
317
|
+
* // { front: { imageUrl: '...', alignment: {...} }, back: {...} }
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
export function useCurrentPlacements(): Record<string, PlacementDesign> {
|
|
321
|
+
const productCtx = useProductOptional();
|
|
322
|
+
return productCtx?.placements ?? {};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* useArtworkAlignment - Get alignment for a specific artwork on a placement
|
|
327
|
+
*
|
|
328
|
+
* @param placement - The placement name (e.g., "front", "back")
|
|
329
|
+
* @returns The alignment or undefined
|
|
330
|
+
*
|
|
331
|
+
* @example
|
|
332
|
+
* ```tsx
|
|
333
|
+
* const alignment = useArtworkAlignment("front");
|
|
334
|
+
* // { x: 0.5, y: 0.5, scale: 1, rotation: 0 }
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
export function useArtworkAlignment(
|
|
338
|
+
placement: string
|
|
339
|
+
): ImageAlignment | undefined {
|
|
340
|
+
const productCtx = useProductOptional();
|
|
341
|
+
return productCtx?.placements[placement]?.alignment;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* useSetArtworkAlignment - Set alignment for a placement
|
|
346
|
+
*
|
|
347
|
+
* @returns A function to update alignment
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```tsx
|
|
351
|
+
* const setAlignment = useSetArtworkAlignment();
|
|
352
|
+
* setAlignment("front", "center");
|
|
353
|
+
* setAlignment("back", "top");
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
export function useSetArtworkAlignment(): (
|
|
357
|
+
placement: string,
|
|
358
|
+
alignment: ImageAlignment
|
|
359
|
+
) => void {
|
|
360
|
+
const productCtx = useProductOptional();
|
|
361
|
+
|
|
362
|
+
return useCallback(
|
|
363
|
+
(placement: string, alignment: ImageAlignment) => {
|
|
364
|
+
if (productCtx) {
|
|
365
|
+
const currentDesign = productCtx.placements[placement];
|
|
366
|
+
productCtx.setPlacementDesign(placement, {
|
|
367
|
+
...currentDesign,
|
|
368
|
+
alignment,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
[productCtx]
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* useSelectedArtwork - Get and set the selected artwork
|
|
378
|
+
*
|
|
379
|
+
* A simplified hook for artwork selection.
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* ```tsx
|
|
383
|
+
* const [artwork, setArtwork] = useSelectedArtwork();
|
|
384
|
+
*
|
|
385
|
+
* return (
|
|
386
|
+
* <button onClick={() => setArtwork({ type: 'regular', src: url })}>
|
|
387
|
+
* Select Artwork
|
|
388
|
+
* </button>
|
|
389
|
+
* );
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
392
|
+
export function useSelectedArtwork(): [
|
|
393
|
+
Artwork | undefined,
|
|
394
|
+
(artwork: Artwork | undefined) => void
|
|
395
|
+
] {
|
|
396
|
+
const productCtx = useProductOptional();
|
|
397
|
+
const shopCtx = useShopOptional();
|
|
398
|
+
|
|
399
|
+
// Prefer product context, fall back to shop context
|
|
400
|
+
const ctx = productCtx || shopCtx;
|
|
401
|
+
|
|
402
|
+
const setArtwork = useCallback(
|
|
403
|
+
(artwork: Artwork | undefined) => {
|
|
404
|
+
ctx?.setSelectedArtwork(artwork);
|
|
405
|
+
},
|
|
406
|
+
[ctx]
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
return [ctx?.selectedArtwork, setArtwork];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* useProductSelection - Get and set variant selection
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```tsx
|
|
417
|
+
* const [selection, updateSelection] = useProductSelection();
|
|
418
|
+
*
|
|
419
|
+
* return (
|
|
420
|
+
* <button onClick={() => updateSelection({ ...selection, size: 'L' })}>
|
|
421
|
+
* Large
|
|
422
|
+
* </button>
|
|
423
|
+
* );
|
|
424
|
+
* ```
|
|
425
|
+
*/
|
|
426
|
+
export function useProductSelection(): [
|
|
427
|
+
OptionSelection,
|
|
428
|
+
(selection: OptionSelection) => void
|
|
429
|
+
] {
|
|
430
|
+
const productCtx = useProductOptional();
|
|
431
|
+
|
|
432
|
+
const update = useCallback(
|
|
433
|
+
(selection: OptionSelection) => {
|
|
434
|
+
productCtx?.updateSelection?.(selection);
|
|
435
|
+
},
|
|
436
|
+
[productCtx]
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
return [productCtx?.selection ?? {}, update];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* usePlacementClipShape - Get and set clip shape for a placement
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```tsx
|
|
447
|
+
* const [clipShape, setClipShape] = usePlacementClipShape("front");
|
|
448
|
+
* // clipShape: 'rectangle' | 'circle' | 'custom'
|
|
449
|
+
* ```
|
|
450
|
+
*/
|
|
451
|
+
export function usePlacementClipShape(
|
|
452
|
+
placement: string
|
|
453
|
+
): [ClipShape, (shape: ClipShape) => void] {
|
|
454
|
+
const productCtx = useProductOptional();
|
|
455
|
+
|
|
456
|
+
const setShape = useCallback(
|
|
457
|
+
(shape: ClipShape) => {
|
|
458
|
+
productCtx?.setPlacementClipShape(placement, shape);
|
|
459
|
+
},
|
|
460
|
+
[productCtx, placement]
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
return [
|
|
464
|
+
productCtx?.getPlacementClipShape(placement) ?? "rectangle",
|
|
465
|
+
setShape,
|
|
466
|
+
];
|
|
467
|
+
}
|