{filteredThemes.length === 0 ? (
-
+
No themes found
Try adjusting your search or filters
@@ -192,142 +186,136 @@ export function ThemeMarketplacePanel({ currentTheme, onApplyTheme }: ThemeMarke
) : (
- {filteredThemes.map((theme) => (
-
- {/* Preview Image */}
-
- {/* Theme color swatches as preview */}
-
-
- {/* Overlay with quick actions */}
-
-
-
-
- {/* Current theme badge */}
- {isCurrentTheme(theme.id) && (
-
-
- Active
-
+ {filteredThemes.map((theme) => {
+ const active = isActive(theme.id);
+ return (
+
-
-
-
-
-
{theme.name}
-
- by {theme.author} β’ v{theme.version}
-
+ >
+ {/* Color Swatch Preview */}
+
+
+
+ {[
+ theme.theme.colors.primary,
+ theme.theme.colors.secondary,
+ theme.theme.colors.accent,
+ ].map((color, i) => (
+
+ ))}
+
-
-
- {theme.rating.toFixed(1)}
+
+ {/* Hover overlay */}
+
+
-
-
-
-
-
- {theme.description}
-
-
- {/* Tags */}
-
- {theme.tags.map((tag) => (
-
- {tag}
+
+ {active && (
+
+
+ Active
- ))}
+ )}
-
-
-
-
-
-
- {theme.downloads.toLocaleString()} installs
-
-
-
-
-
-
- ))}
+
+
+
+
+
+ {theme.name}
+
+
+ by StormCom β’{' '}
+ {getIndustryLabel(theme.industry)}
+
+
+
+
+ {theme.rating.toFixed(1)}
+
+
+
+
+
+
+ {theme.description}
+
+
+ {theme.tags.slice(0, 3).map((tag) => (
+
+ {tag}
+
+ ))}
+
+
+
+
+
+
+
+ {theme.downloads.toLocaleString()} installs
+
+
+
+
+
+ );
+ })}
)}
);
-}
-
-// Helper functions
-function getCategoryForTheme(id: ThemeTemplateId): ThemeCategory {
- const categories: Record = {
- modern: 'modern',
- classic: 'classic',
- bold: 'popular',
- elegant: 'popular',
- minimal: 'minimal',
- boutique: 'modern',
- };
- return categories[id] || 'all';
-}
-
-function getTagsForTheme(id: ThemeTemplateId): string[] {
- const tags: Record = {
- modern: ['Clean', 'Minimal', 'Professional'],
- classic: ['Traditional', 'Warm', 'Trustworthy'],
- bold: ['Vibrant', 'Eye-catching', 'Modern'],
- elegant: ['Luxury', 'Sophisticated', 'Premium'],
- minimal: ['Ultra-clean', 'Simple', 'Apple-inspired'],
- boutique: ['Playful', 'Friendly', 'Small Business'],
- };
- return tags[id] || [];
-}
+}
\ No newline at end of file
diff --git a/src/components/dashboard/storefront/editor/theme-settings-panel.tsx b/src/components/dashboard/storefront/editor/theme-settings-panel.tsx
index 207f0760..8db3417b 100644
--- a/src/components/dashboard/storefront/editor/theme-settings-panel.tsx
+++ b/src/components/dashboard/storefront/editor/theme-settings-panel.tsx
@@ -99,6 +99,7 @@ const FONT_OPTIONS: { value: FontFamily; label: string }[] = [
{ value: 'poppins', label: 'Poppins' },
{ value: 'playfair', label: 'Playfair Display' },
{ value: 'montserrat', label: 'Montserrat' },
+ { value: 'cormorant', label: 'Cormorant Garamond' },
];
const LAYOUT_OPTIONS: { value: LayoutVariant; label: string }[] = [
diff --git a/src/components/dashboard/storefront/theme-selector.tsx b/src/components/dashboard/storefront/theme-selector.tsx
index 9c407a8c..19c9e25d 100644
--- a/src/components/dashboard/storefront/theme-selector.tsx
+++ b/src/components/dashboard/storefront/theme-selector.tsx
@@ -6,8 +6,9 @@
* Allows selecting theme templates and customizing colors.
*/
-import { getAllThemeTemplates, getThemeTemplate } from "@/lib/storefront/theme-templates";
-import type { ThemeSettings, ThemeTemplateId } from "@/lib/storefront/types";
+import { getAllThemeTemplates, getThemeTemplate, getV2Templates, getLegacyTemplates } from "@/lib/storefront/theme-templates";
+import { FONT_FAMILIES, LAYOUT_META, RADIUS_OPTIONS } from "@/lib/storefront/theme-catalog";
+import type { ThemeSettings, ThemeTemplateId, FontFamily, LayoutVariant } from "@/lib/storefront/types";
import { isValidHexColor } from "@/lib/storefront/defaults";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
@@ -34,7 +35,8 @@ interface ThemeSelectorProps {
}
export function ThemeSelector({ theme, onThemeChange }: ThemeSelectorProps) {
- const templates = getAllThemeTemplates();
+ const v2Templates = getV2Templates();
+ const legacyTemplates = getLegacyTemplates();
const handleTemplateSelect = (templateId: ThemeTemplateId) => {
const template = getThemeTemplate(templateId);
@@ -45,7 +47,6 @@ export function ThemeSelector({ theme, onThemeChange }: ThemeSelectorProps) {
};
const handleColorChange = (key: keyof ThemeSettings["colors"], value: string) => {
- // Validate hex color
if (!isValidHexColor(value)) return;
onThemeChange({
@@ -57,6 +58,65 @@ export function ThemeSelector({ theme, onThemeChange }: ThemeSelectorProps) {
});
};
+ const TemplateCard = ({ template }: { template: ReturnType[number] }) => (
+
+
+
+
+ );
+
return (
@@ -85,69 +145,34 @@ export function ThemeSelector({ theme, onThemeChange }: ThemeSelectorProps) {
handleTemplateSelect(value as ThemeTemplateId)}
- className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"
+ className="space-y-6"
>
- {templates.map((template) => (
-
-
-
+ {/* Modern v2 themes */}
+
+
+ {/* Classic / Legacy themes */}
+
@@ -253,12 +278,12 @@ export function ThemeSelector({ theme, onThemeChange }: ThemeSelectorProps) {
- Choose the primary font for your storefront
+ Primary font used for body text
+
+
+
+
+
+
+
+ Font used for headings (h1βh6). Falls back to body font when not set.
+
+
+
@@ -300,7 +357,7 @@ export function ThemeSelector({ theme, onThemeChange }: ThemeSelectorProps) {
onValueChange={(value) =>
onThemeChange({
...theme,
- layout: value as ThemeSettings["layout"],
+ layout: value as LayoutVariant,
})
}
>
@@ -308,9 +365,13 @@ export function ThemeSelector({ theme, onThemeChange }: ThemeSelectorProps) {
- Full Width
- Boxed
- Centered
+ {(Object.entries(LAYOUT_META) as Array<[LayoutVariant, { label: string }]>).map(
+ ([value, meta]) => (
+
+ {meta.label}
+
+ ),
+ )}
@@ -339,11 +400,11 @@ export function ThemeSelector({ theme, onThemeChange }: ThemeSelectorProps) {
- None (Sharp)
- Small
- Medium
- Large
- Extra Large
+ {RADIUS_OPTIONS.map((opt) => (
+
+ {opt.label}
+
+ ))}
diff --git a/src/components/integrations/facebook/conversions-dashboard.tsx b/src/components/integrations/facebook/conversions-dashboard.tsx
index 6c523144..54949875 100644
--- a/src/components/integrations/facebook/conversions-dashboard.tsx
+++ b/src/components/integrations/facebook/conversions-dashboard.tsx
@@ -221,7 +221,9 @@ export function ConversionsDashboard({ integrationId: _integrationId }: Conversi
if (eventNameFilter && eventNameFilter !== 'all') params.set('eventName', eventNameFilter);
if (searchQuery) params.set('search', searchQuery);
- const response = await fetch(`/api/integrations/facebook/conversions?${params}`);
+ const response = await fetch(`/api/integrations/facebook/conversions?${params}`, {
+ credentials: 'include',
+ });
const data = await response.json();
if (data.success) {
@@ -245,7 +247,9 @@ export function ConversionsDashboard({ integrationId: _integrationId }: Conversi
// Fetch retry queue stats
const fetchRetryStats = useCallback(async () => {
try {
- const response = await fetch('/api/integrations/facebook/conversions/retry');
+ const response = await fetch('/api/integrations/facebook/conversions/retry', {
+ credentials: 'include',
+ });
const data = await response.json();
if (data.success) {
diff --git a/src/components/integrations/facebook/dashboard.tsx b/src/components/integrations/facebook/dashboard.tsx
index 77be7723..9b735c06 100644
--- a/src/components/integrations/facebook/dashboard.tsx
+++ b/src/components/integrations/facebook/dashboard.tsx
@@ -133,7 +133,9 @@ export function FacebookDashboard({ integration, countryCode = 'BD' }: Props) {
setHealthLoading(true);
try {
- const response = await fetch('/api/integrations/facebook/status');
+ const response = await fetch('/api/integrations/facebook/status', {
+ credentials: 'include',
+ });
const data = await response.json();
if (data.health) {
@@ -155,7 +157,9 @@ export function FacebookDashboard({ integration, countryCode = 'BD' }: Props) {
const handleConnect = async () => {
setConnecting(true);
try {
- const response = await fetch('/api/integrations/facebook/oauth/connect');
+ const response = await fetch('/api/integrations/facebook/oauth/connect', {
+ credentials: 'include',
+ });
const data = await response.json();
if (data.url) {
diff --git a/src/components/integrations/facebook/messenger-inbox.tsx b/src/components/integrations/facebook/messenger-inbox.tsx
index 25853930..8036aba2 100644
--- a/src/components/integrations/facebook/messenger-inbox.tsx
+++ b/src/components/integrations/facebook/messenger-inbox.tsx
@@ -91,7 +91,9 @@ export function MessengerInbox({ selectedConversationId, onSelectConversation }:
params.set('sync', 'true');
}
- const response = await fetch(`/api/integrations/facebook/messages?${params}`);
+ const response = await fetch(`/api/integrations/facebook/messages?${params}`, {
+ credentials: 'include',
+ });
const data = await response.json();
if (!response.ok) {
diff --git a/src/components/inventory/inventory-history-dialog.tsx b/src/components/inventory/inventory-history-dialog.tsx
index 19f388fe..ff2b8e0a 100644
--- a/src/components/inventory/inventory-history-dialog.tsx
+++ b/src/components/inventory/inventory-history-dialog.tsx
@@ -122,7 +122,9 @@ export function InventoryHistoryDialog({
params.append('reason', reasonFilter);
}
- const response = await fetch(`/api/inventory/history?${params}`);
+ const response = await fetch(`/api/inventory/history?${params}`, {
+ credentials: 'include',
+ });
const data = await response.json();
if (response.ok) {
diff --git a/src/components/inventory/low-stock-widget.tsx b/src/components/inventory/low-stock-widget.tsx
index af6143c8..44b6cd25 100644
--- a/src/components/inventory/low-stock-widget.tsx
+++ b/src/components/inventory/low-stock-widget.tsx
@@ -43,7 +43,9 @@ export function LowStockWidget({ storeId, maxItems = 5, className = '' }: LowSto
const fetchLowStockItems = useCallback(async () => {
try {
setLoading(true);
- const response = await fetch(`/api/inventory/low-stock?storeId=${storeId}`);
+ const response = await fetch(`/api/inventory/low-stock?storeId=${storeId}`, {
+ credentials: 'include',
+ });
const data = await response.json();
if (response.ok) {
diff --git a/src/components/order-detail-client.tsx b/src/components/order-detail-client.tsx
index 5e310cfa..11f3e319 100644
--- a/src/components/order-detail-client.tsx
+++ b/src/components/order-detail-client.tsx
@@ -175,7 +175,9 @@ export function OrderDetailClient({ orderId, storeId }: OrderDetailClientProps)
});
console.log('[Order Detail] Fetching order:', { orderId, storeId });
- const response = await fetch(`/api/orders/${orderId}?${queryParams.toString()}`);
+ const response = await fetch(`/api/orders/${orderId}?${queryParams.toString()}`, {
+ credentials: 'include',
+ });
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
@@ -247,6 +249,7 @@ export function OrderDetailClient({ orderId, storeId }: OrderDetailClientProps)
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
+ credentials: 'include',
});
console.log('[Order Update] Response status:', response.status);
@@ -297,6 +300,7 @@ export function OrderDetailClient({ orderId, storeId }: OrderDetailClientProps)
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
+ credentials: 'include',
});
console.log('[Tracking Update] Response status:', response.status);
@@ -338,6 +342,7 @@ export function OrderDetailClient({ orderId, storeId }: OrderDetailClientProps)
const response = await fetch(`/api/orders/${order.id}/refund`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
body: JSON.stringify({
storeId,
refundAmount: amount,
@@ -373,7 +378,9 @@ export function OrderDetailClient({ orderId, storeId }: OrderDetailClientProps)
storeId,
});
- const response = await fetch(`/api/orders/${order.id}/invoice?${queryParams.toString()}`);
+ const response = await fetch(`/api/orders/${order.id}/invoice?${queryParams.toString()}`, {
+ credentials: 'include',
+ });
if (!response.ok) {
throw new Error('Failed to generate invoice');
diff --git a/src/components/orders-table.tsx b/src/components/orders-table.tsx
index 5d84bb7a..9bb1382b 100644
--- a/src/components/orders-table.tsx
+++ b/src/components/orders-table.tsx
@@ -299,7 +299,9 @@ export function OrdersTable({ storeId }: OrdersTableProps) {
params.set('dateTo', dateTo.toISOString());
}
- const response = await fetch(`/api/orders?${params.toString()}`);
+ const response = await fetch(`/api/orders?${params.toString()}`, {
+ credentials: 'include',
+ });
if (!response.ok) {
throw new Error('Failed to fetch orders');
@@ -346,7 +348,9 @@ export function OrdersTable({ storeId }: OrdersTableProps) {
since: lastCheckedAtRef.current, // Use ref instead of state
});
- const response = await fetch(`/api/orders/check-updates?${params.toString()}`);
+ const response = await fetch(`/api/orders/check-updates?${params.toString()}`, {
+ credentials: 'include',
+ });
if (!response.ok) {
console.error('Failed to check for order updates');
diff --git a/src/components/product-edit-form.tsx b/src/components/product-edit-form.tsx
index d4bd0dc6..7b2cddd4 100644
--- a/src/components/product-edit-form.tsx
+++ b/src/components/product-edit-form.tsx
@@ -143,7 +143,9 @@ export function ProductEditForm({ productId }: ProductEditFormProps) {
async function fetchProductStore() {
try {
// Fetch minimal product info to get its storeId
- const response = await fetch(`/api/products/${productId}/store`);
+ const response = await fetch(`/api/products/${productId}/store`, {
+ credentials: 'include',
+ });
if (response.ok) {
const data = await response.json();
if (data.storeId && data.storeId !== storeId) {
@@ -169,7 +171,9 @@ export function ProductEditForm({ productId }: ProductEditFormProps) {
setFetching(true);
try {
- const response = await fetch(`/api/products/${productId}?storeId=${storeId}`);
+ const response = await fetch(`/api/products/${productId}?storeId=${storeId}`, {
+ credentials: 'include',
+ });
if (!response.ok) throw new Error('Failed to fetch product');
const product: FetchedProduct = await response.json();
diff --git a/src/components/product/brand-selector.tsx b/src/components/product/brand-selector.tsx
index 8ab8d9df..b984f688 100644
--- a/src/components/product/brand-selector.tsx
+++ b/src/components/product/brand-selector.tsx
@@ -50,7 +50,9 @@ export function BrandSelector({
setLoading(true);
setError(null);
- const response = await fetch(`/api/brands?storeId=${storeId}&perPage=100&isPublished=true`);
+ const response = await fetch(`/api/brands?storeId=${storeId}&perPage=100&isPublished=true`, {
+ credentials: 'include',
+ });
if (!response.ok) {
throw new Error('Failed to fetch brands');
diff --git a/src/components/product/category-selector.tsx b/src/components/product/category-selector.tsx
index 9e50a62c..abc13df9 100644
--- a/src/components/product/category-selector.tsx
+++ b/src/components/product/category-selector.tsx
@@ -51,7 +51,9 @@ export function CategorySelector({
setLoading(true);
setError(null);
- const response = await fetch(`/api/categories?storeId=${storeId}&perPage=100&isPublished=true`);
+ const response = await fetch(`/api/categories?storeId=${storeId}&perPage=100&isPublished=true`, {
+ credentials: 'include',
+ });
if (!response.ok) {
throw new Error('Failed to fetch categories');
diff --git a/src/components/store-selector.tsx b/src/components/store-selector.tsx
index f532c5c0..bff18b80 100644
--- a/src/components/store-selector.tsx
+++ b/src/components/store-selector.tsx
@@ -181,7 +181,10 @@ export function StoreSelector({ onStoreChange }: StoreSelectorProps) {
const handleCreateStore = async () => {
setCreating(true);
try {
- const response = await fetch('/api/demo/create-store', { method: 'POST' });
+ const response = await fetch('/api/demo/create-store', {
+ method: 'POST',
+ credentials: 'include',
+ });
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || 'Failed to create store');
diff --git a/src/components/storefront/blocks/cart/cart-slideover-block.tsx b/src/components/storefront/blocks/cart/cart-slideover-block.tsx
new file mode 100644
index 00000000..d983f69b
--- /dev/null
+++ b/src/components/storefront/blocks/cart/cart-slideover-block.tsx
@@ -0,0 +1,294 @@
+"use client";
+/**
+ * Cart Slide-over Block
+ *
+ * Ported from tailwind-css-prebuild-ui-themes shopping-carts/slide_over.jsx.
+ * Replaces @headlessui Dialog + Transition with shadcn Sheet.
+ * All colours use CSS theme variables.
+ */
+
+import { Fragment } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { X, ShoppingBag, Minus, Plus, Trash2 } from "lucide-react";
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet";
+import { Button } from "@/components/ui/button";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Separator } from "@/components/ui/separator";
+import { cn } from "@/lib/utils";
+import type { CartBlockProps, CartItem } from "../types";
+
+// ββ Line item ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function CartLineItem({
+ item,
+ onUpdateQuantity,
+ onRemoveItem,
+}: {
+ item: CartItem;
+ onUpdateQuantity?: (id: string, qty: number) => void;
+ onRemoveItem?: (id: string) => void;
+}) {
+ return (
+
+ {/* Product image */}
+
+ {item.imageSrc ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ {/* Details */}
+
+
+
+
+
+ {item.name}
+
+
+ {(item.color || item.size) && (
+
+ {[item.color, item.size].filter(Boolean).join(" Β· ")}
+
+ )}
+ {item.inStock === false && (
+
Out of stock
+ )}
+
+ {/* Price */}
+
{item.price}
+
+
+ {/* Quantity controls + remove */}
+
+ {onUpdateQuantity ? (
+
+
+
+ {item.quantity}
+
+
+
+ ) : (
+
Qty {item.quantity}
+ )}
+
+ {onRemoveItem && (
+
+ )}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Cart Slide-over (Sheet)
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * CartSlideoverBlock
+ *
+ * A slide-over (Sheet) shopping cart panel.
+ * Rendered client-side only; open/close controlled externally.
+ *
+ * @example
+ *
setCartOpen(false)}
+ * items={cartItems}
+ * subtotal="$239.00"
+ * checkoutHref="/checkout"
+ * continueShoppingHref="/products"
+ * />
+ */
+export function CartSlideoverBlock({
+ items,
+ subtotal,
+ shippingNote = "Shipping and taxes calculated at checkout.",
+ checkoutHref,
+ continueShoppingHref,
+ onUpdateQuantity,
+ onRemoveItem,
+ open = false,
+ onClose,
+}: CartBlockProps) {
+ return (
+ !v && onClose?.()}>
+
+ {/* Header */}
+
+
+
+
+ Shopping Cart
+ {items.length > 0 && (
+
+ {items.reduce((acc, i) => acc + i.quantity, 0)}
+
+ )}
+
+
+
+
+ {/* Body */}
+ {items.length === 0 ? (
+
+
+
+
+
+
Your cart is empty
+
+ Start shopping to add items to your cart.
+
+
+
+
+ ) : (
+ <>
+
+
+ {items.map((item) => (
+
+ ))}
+
+
+
+ {/* Footer */}
+
+
+
Subtotal
+
{subtotal}
+
+ {shippingNote && (
+
{shippingNote}
+ )}
+
+
+
+
+
+ or{" "}
+
+
+
+
+
+ >
+ )}
+
+
+ );
+}
+
+// ββ Cart Popover (compact, for desktop nav) ββββββββββββββββββββββββββββ
+export function CartSummaryMini({
+ items,
+ subtotal,
+ checkoutHref,
+}: Pick) {
+ const totalQty = items.reduce((a, i) => a + i.quantity, 0);
+
+ return (
+
+
+ Cart ({totalQty} {totalQty === 1 ? "item" : "items"})
+
+
+ {items.length === 0 ? (
+
Your cart is empty
+ ) : (
+ <>
+
+
+
+ Subtotal
+ {subtotal}
+
+
+ >
+ )}
+
+ );
+}
+
+export type { CartBlockProps, CartItem };
diff --git a/src/components/storefront/blocks/category/category-filters-block.tsx b/src/components/storefront/blocks/category/category-filters-block.tsx
new file mode 100644
index 00000000..205c6467
--- /dev/null
+++ b/src/components/storefront/blocks/category/category-filters-block.tsx
@@ -0,0 +1,407 @@
+"use client";
+/**
+ * Category Filters Block
+ *
+ * Ported from tailwind-css-prebuild-ui-themes
+ * ecommerce/category-filters/sidebar_filters.jsx
+ *
+ * Provides:
+ * β FilterSidebar: desktop-permanent sidebar
+ * β FilterMobileDrawer: mobile sheet/drawer
+ * β ActiveFiltersBar: active filter chips with clear button
+ * β SortDropdown: sort-order selector
+ *
+ * All colours use CSS theme variables.
+ */
+
+import { useState } from "react";
+import { SlidersHorizontal, X, ChevronDown, ChevronUp, Check } from "lucide-react";
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+ SheetTrigger,
+} from "@/components/ui/sheet";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Separator } from "@/components/ui/separator";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Label } from "@/components/ui/label";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { cn } from "@/lib/utils";
+import type { CategoryFiltersProps, FilterGroup, ActiveFilter } from "../types";
+
+// ββ Sort Dropdown βββββββββββββββββββββββββββββββββββββββββββββββββββββ
+export function SortDropdown({
+ sortOptions: options = [],
+ currentSort,
+ onSortChange,
+}: Pick) {
+ const current = options.find((o: { value: string; label: string }) => o.value === currentSort) ?? options[0];
+
+ return (
+
+
+
+
+
+ {options.map((opt: { value: string; label: string }) => (
+
+ ))}
+
+
+ );
+}
+
+// ββ Active Filter Chips ββββββββββββββββββββββββββββββββββββββββββββββββ
+export function ActiveFiltersBar({
+ activeFilters = [],
+ onClearFilter,
+ onClearAll,
+}: Pick) {
+ if (activeFilters.length === 0) return null;
+
+ return (
+
+ Filters:
+ {activeFilters.map((filter) => (
+
+ {filter.label}: {filter.value}
+
+
+ ))}
+
+
+ );
+}
+
+// ββ Single filter group (accordion-style) βββββββββββββββββββββββββββββ
+function FilterGroupPanel({
+ group,
+ onFilterChange,
+}: {
+ group: FilterGroup;
+ onFilterChange?: CategoryFiltersProps["onFilterChange"];
+}) {
+ const [expanded, setExpanded] = useState(true);
+
+ return (
+
+
+
+ {expanded && (
+
+ {group.type === "checkbox" &&
+ group.options.map((option) => (
+
+
+ onFilterChange?.(group.id, option.value, v === true)
+ }
+ />
+
+
+ ))}
+
+ {group.type === "radio" &&
+ group.options.map((option) => (
+
+ onFilterChange?.(group.id, option.value, true)}
+ className="size-4 border-border text-primary focus:ring-primary"
+ />
+
+
+ ))}
+
+ {group.type === "color" && (
+
+ {group.options.map((option) => (
+
+ )}
+
+ )}
+
+ );
+}
+
+// ββ Filter panels list βββββββββββββββββββββββββββββββββββββββββββββββββ
+function FilterPanelList({
+ filters,
+ onFilterChange,
+}: Pick) {
+ return (
+
+ {filters.map((group) => (
+
+ ))}
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// FilterSidebar β permanent desktop sidebar
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * FilterSidebar
+ *
+ * Permanent left-side filter panel for desktop category pages.
+ * Pairs with FilterMobileDrawer for responsive layouts.
+ */
+export function FilterSidebar({
+ filters,
+ activeFilters = [],
+ resultCount,
+ sortOptions = [],
+ currentSort,
+ onFilterChange,
+ onClearFilter,
+ onClearAll,
+ onSortChange,
+}: CategoryFiltersProps) {
+ return (
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// FilterMobileDrawer β sheet for mobile/tablet
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * FilterMobileDrawer
+ *
+ * Full-screen mobile filter sheet, triggered by a "Filters" button.
+ * Pairs with FilterSidebar in a responsive layout.
+ */
+export function FilterMobileDrawer({
+ filters,
+ activeFilters = [],
+ resultCount,
+ sortOptions = [],
+ currentSort,
+ onFilterChange,
+ onClearFilter,
+ onClearAll,
+ onSortChange,
+}: CategoryFiltersProps) {
+ const activeCount = (activeFilters ?? []).length;
+
+ return (
+
+
+
+
+
+
+
+
+
+ Filters
+
+
+
+
+
+
+
+
+ {resultCount !== undefined && (
+
+
+ {resultCount} result{resultCount !== 1 ? "s" : ""}
+
+
+ )}
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// CategoryFiltersBar β combined sort + mobile filter button
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * CategoryFiltersBar
+ *
+ * Top bar with combined mobile filter button + sort.
+ * Should sit above the product grid alongside FilterSidebar.
+ */
+export function CategoryFiltersBar({
+ filters,
+ activeFilters,
+ resultCount,
+ sortOptions,
+ currentSort,
+ onFilterChange,
+ onClearFilter,
+ onClearAll,
+ onSortChange,
+}: CategoryFiltersProps) {
+ return (
+
+ {/* Left: result count + mobile filters */}
+
+ {resultCount !== undefined && (
+
+ {resultCount} {resultCount === 1 ? "result" : "results"}
+
+ )}
+
+
+
+ {/* Right: sort */}
+ {sortOptions && sortOptions.length > 0 && (
+
+ )}
+
+ );
+}
+
+export type { CategoryFiltersProps, FilterGroup, ActiveFilter };
diff --git a/src/components/storefront/blocks/category/category-preview-block.tsx b/src/components/storefront/blocks/category/category-preview-block.tsx
new file mode 100644
index 00000000..50076ef6
--- /dev/null
+++ b/src/components/storefront/blocks/category/category-preview-block.tsx
@@ -0,0 +1,344 @@
+"use client";
+/**
+ * Category Preview Block Variants
+ *
+ * Ported from tailwind-css-prebuild-ui-themes ecommerce/category-previews.
+ * All colours use CSS theme variables (bg-primary, text-foreground, etc.)
+ *
+ * Variants:
+ * β image-backgrounds : three-column grid, images fill the card background
+ * β three-column : three-column with description text
+ * β split-images : alternating split image layout
+ * β scrolling-cards : horizontally-scrollable category cards
+ */
+
+import Link from "next/link";
+import Image from "next/image";
+import { cn } from "@/lib/utils";
+import { ArrowRight } from "lucide-react";
+import type { CategoryPreviewProps, CategoryPreviewVariant, BlockCategory } from "../types";
+
+// ββ Image Backgrounds Variant ββββββββββββββββββββββββββββββββββββββββββ
+function ImageBackgroundsVariant({
+ categories,
+ heading,
+ cta,
+}: Omit) {
+ return (
+
+
+ {heading && (
+
+ {heading}
+
+ )}
+
+
+ {categories.map((category) => (
+
+ ))}
+
+
+ {cta && (
+
+ )}
+
+
+ );
+}
+
+function CategoryImageCard({ category }: { category: BlockCategory }) {
+ return (
+
+ {category.image ? (
+
+ ) : (
+
+ )}
+ {/* Gradient overlay */}
+
+
+ {/* Text */}
+
+
+ {category.name}
+
+ {category.productCount !== undefined && (
+
+ {category.productCount} {category.productCount === 1 ? "product" : "products"}
+
+ )}
+
+
+
+ );
+}
+
+// ββ Three Column With Description Variant βββββββββββββββββββββββββββββ
+function ThreeColumnVariant({
+ categories,
+ heading,
+ cta,
+}: Omit) {
+ return (
+
+
+ {heading && (
+
+ {heading}
+
+ )}
+
+
+ {categories.map((category) => (
+
+
+ {category.image ? (
+
+ ) : (
+
+ π¦
+
+ )}
+
+
+
+
+ {category.name}
+
+ {category.description && (
+
+ {category.description}
+
+ )}
+
+ Shop the collection
+ →
+
+
+
+ ))}
+
+
+ {cta && (
+
+ )}
+
+
+ );
+}
+
+// ββ Split Images Variant βββββββββββββββββββββββββββββββββββββββββββββββ
+function SplitImagesVariant({
+ categories,
+ heading,
+}: Omit) {
+ const pairs = [];
+ for (let i = 0; i < categories.length; i += 2) {
+ pairs.push(categories.slice(i, i + 2));
+ }
+
+ return (
+
+
+ {heading && (
+
+ {heading}
+
+ )}
+
+
+ {pairs.map((pair, pairIdx) => (
+
+ {pair.map((category) => (
+
+ {category.image ? (
+
+ ) : (
+
+ )}
+
+
+
{category.name}
+
+ Shop now →
+
+
+
+ ))}
+
+ ))}
+
+
+
+ );
+}
+
+// ββ Scrolling Cards Variant ββββββββββββββββββββββββββββββββββββββββββββ
+function ScrollingCardsVariant({
+ categories,
+ heading,
+ cta,
+}: Omit) {
+ return (
+
+
+ {(heading || cta) && (
+
+ {heading && (
+
+ )}
+ {cta && (
+
+ {cta.text}
+
+
+ )}
+
+ )}
+
+ {/* Scrollable container */}
+
+ {categories.map((category) => (
+
+
+ {category.image ? (
+
+ ) : (
+
+ π¦
+
+ )}
+
+
+
+
{category.name}
+ {category.productCount !== undefined && (
+
{category.productCount} products
+ )}
+
+
+ ))}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Public API: CategoryPreviewBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const VARIANT_MAP: Record<
+ CategoryPreviewVariant,
+ React.ComponentType>
+> = {
+ "image-backgrounds": ImageBackgroundsVariant,
+ "three-column": ThreeColumnVariant,
+ "split-images": SplitImagesVariant,
+ "scrolling-cards": ScrollingCardsVariant,
+};
+
+/**
+ * CategoryPreviewBlock
+ *
+ * Flexible category preview section with multiple visual variants.
+ * All colours are theme-variable driven for live customisation.
+ */
+export function CategoryPreviewBlock(props: CategoryPreviewProps) {
+ const variant: CategoryPreviewVariant = props.variant ?? "image-backgrounds";
+ const Component = VARIANT_MAP[variant] ?? ImageBackgroundsVariant;
+ return ;
+}
+
+export {
+ ImageBackgroundsVariant,
+ ThreeColumnVariant,
+ SplitImagesVariant,
+ ScrollingCardsVariant,
+};
+export type { CategoryPreviewProps, CategoryPreviewVariant };
diff --git a/src/components/storefront/blocks/checkout/checkout-form-block.tsx b/src/components/storefront/blocks/checkout/checkout-form-block.tsx
new file mode 100644
index 00000000..2e61657c
--- /dev/null
+++ b/src/components/storefront/blocks/checkout/checkout-form-block.tsx
@@ -0,0 +1,530 @@
+"use client";
+/**
+ * Checkout Form Block
+ *
+ * Ported from tailwind-css-prebuild-ui-themes ecommerce/checkout-forms.
+ *
+ * A single component that renders:
+ * β Contact information
+ * β Shipping address
+ * β Payment method (credit card form placeholder)
+ * β Order summary sidebar
+ *
+ * All colours use CSS theme variables.
+ * Uses shadcn Input, Label, RadioGroup, Select for form controls.
+ */
+
+import { useState } from "react";
+import { CreditCard, Truck, CheckCircle2 } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Button } from "@/components/ui/button";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Separator } from "@/components/ui/separator";
+import { Checkbox } from "@/components/ui/checkbox";
+import type { CartItem } from "../types";
+
+// βββ Types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface ShippingOption {
+ id: string;
+ label: string;
+ description: string;
+ price: string;
+ estimatedDays: string;
+}
+
+export interface PaymentMethod {
+ id: string;
+ label: string;
+ icon?: React.ReactNode;
+}
+
+export interface CheckoutFormValues {
+ email: string;
+ firstName: string;
+ lastName: string;
+ company: string;
+ address: string;
+ apartment: string;
+ city: string;
+ state: string;
+ zip: string;
+ country: string;
+ phone: string;
+ shippingOption: string;
+ paymentMethod: string;
+ cardNumber: string;
+ cardExpiry: string;
+ cardCvc: string;
+ sameAsShipping: boolean;
+}
+
+export interface CheckoutFormBlockProps {
+ cartItems: CartItem[];
+ subtotal: string;
+ shippingOptions?: ShippingOption[];
+ paymentMethods?: PaymentMethod[];
+ onSubmit?: (values: CheckoutFormValues) => void | Promise;
+ /** Pre-fill form */
+ defaultValues?: Partial;
+ isSubmitting?: boolean;
+ /** Show compact single-column layout (for checkout page) */
+ variant?: "standard" | "two-column";
+}
+
+// βββ Order summary sidebar βββββββββββββββββββββββββββββββββββββββββββββ
+
+function OrderSummary({
+ items,
+ subtotal,
+ shippingCost,
+}: {
+ items: CartItem[];
+ subtotal: string;
+ shippingCost?: string;
+}) {
+ return (
+
+
Order summary
+
+ {items.map((item) => (
+ -
+
+

+
+ {item.quantity}
+
+
+
+
{item.name}
+ {(item.color || item.size) && (
+
+ {[item.color, item.size].filter(Boolean).join(" / ")}
+
+ )}
+
+ {item.price}
+
+ ))}
+
+
+
+
+ Subtotal
+ {subtotal}
+
+
+ Shipping
+ {shippingCost ?? "β"}
+
+
+
+
+ Total
+ {subtotal}
+
+
+ );
+}
+
+// βββ Form field wrapper ββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function Field({
+ id,
+ label,
+ children,
+ className,
+}: {
+ id: string;
+ label: string;
+ children: React.ReactNode;
+ className?: string;
+}) {
+ return (
+
+
+ {children}
+
+ );
+}
+
+// βββ Step indicator ββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const STEPS = ["Contact", "Shipping", "Payment"] as const;
+
+function StepIndicator({ active }: { active: number }) {
+ return (
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// CheckoutFormBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const DEFAULT_SHIPPING: ShippingOption[] = [
+ {
+ id: "standard",
+ label: "Standard shipping",
+ description: "USPS Ground Advantage",
+ price: "Free",
+ estimatedDays: "4β10 business days",
+ },
+ {
+ id: "express",
+ label: "Express shipping",
+ description: "USPS Priority Mail",
+ price: "$8.00",
+ estimatedDays: "2β5 business days",
+ },
+];
+
+const DEFAULT_PAYMENT: PaymentMethod[] = [
+ { id: "card", label: "Credit / Debit card", icon: },
+ { id: "cod", label: "Cash on delivery", icon: },
+];
+
+/**
+ * CheckoutFormBlock
+ *
+ * Multi-step checkout form with order summary sidebar.
+ *
+ * @example
+ * placeOrder(values)}
+ * />
+ */
+export function CheckoutFormBlock({
+ cartItems,
+ subtotal,
+ shippingOptions = DEFAULT_SHIPPING,
+ paymentMethods = DEFAULT_PAYMENT,
+ onSubmit,
+ defaultValues,
+ isSubmitting = false,
+ variant = "two-column",
+}: CheckoutFormBlockProps) {
+ const [step, setStep] = useState(0);
+ const [sameAsShipping, setSameAsShipping] = useState(true);
+ const [selectedShipping, setSelectedShipping] = useState(
+ shippingOptions[0]?.id ?? "standard",
+ );
+ const [selectedPayment, setSelectedPayment] = useState(
+ paymentMethods[0]?.id ?? "card",
+ );
+ const [submitted, setSubmitted] = useState(false);
+
+ const shippingCost = shippingOptions.find((s) => s.id === selectedShipping)?.price;
+
+ const handleContinue = () => setStep((s) => Math.min(s + 1, 2));
+ const handleBack = () => setStep((s) => Math.max(s - 1, 0));
+
+ const handleFinalSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ const data = new FormData(e.currentTarget);
+ const values: CheckoutFormValues = {
+ email: String(data.get("email") ?? ""),
+ firstName: String(data.get("firstName") ?? ""),
+ lastName: String(data.get("lastName") ?? ""),
+ company: String(data.get("company") ?? ""),
+ address: String(data.get("address") ?? ""),
+ apartment: String(data.get("apartment") ?? ""),
+ city: String(data.get("city") ?? ""),
+ state: String(data.get("state") ?? ""),
+ zip: String(data.get("zip") ?? ""),
+ country: String(data.get("country") ?? ""),
+ phone: String(data.get("phone") ?? ""),
+ cardNumber: String(data.get("cardNumber") ?? ""),
+ cardExpiry: String(data.get("cardExpiry") ?? ""),
+ cardCvc: String(data.get("cardCvc") ?? ""),
+ shippingOption: selectedShipping,
+ paymentMethod: selectedPayment,
+ sameAsShipping,
+ };
+ await onSubmit?.(values);
+ setSubmitted(true);
+ };
+
+ if (submitted) {
+ return (
+
+
+
Order Placed!
+
Thank you for your purchase. You'll receive a confirmation email shortly.
+
+ );
+ }
+
+ const isLastStep = step === 2;
+
+ const formContent = (
+
+ );
+
+ if (variant === "two-column") {
+ return (
+
+
+ {/* Form (left) */}
+
{formContent}
+
+ {/* Order summary (right, sticky on desktop) */}
+
+
+
+
+
+ );
+ }
+
+ // Standard (single column)
+ return (
+
+ );
+}
diff --git a/src/components/storefront/blocks/footer/footer-block.tsx b/src/components/storefront/blocks/footer/footer-block.tsx
new file mode 100644
index 00000000..56169f3a
--- /dev/null
+++ b/src/components/storefront/blocks/footer/footer-block.tsx
@@ -0,0 +1,436 @@
+"use client";
+/**
+ * Footer Block Variants
+ *
+ * Ported from tailwind-css-prebuild-ui-themes marketing/footers.
+ * Variants:
+ * β four-column : 4-column link grid + social + copyright
+ * β simple-centered : centred logo + links + copyright
+ * β with-newsletter : 4-column + inline newsletter form
+ * β dark : force dark background regardless of theme
+ *
+ * All colours use CSS theme variables.
+ */
+
+import Link from "next/link";
+import Image from "next/image";
+import { useState } from "react";
+import { cn } from "@/lib/utils";
+import { Facebook, Instagram, Twitter, Youtube, Music2 } from "lucide-react";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import type { FooterBlockProps, SocialLink, FooterColumn } from "../types";
+
+// ββ Social icon map ββββββββββββββββββββββββββββββββββββββββββββββββββββ
+const SOCIAL_ICONS: Record> = {
+ facebook: Facebook,
+ instagram: Instagram,
+ twitter: Twitter,
+ youtube: Youtube,
+ tiktok: Music2,
+ // pinterest: use tiktok as placeholder (Pinterest not in lucide-react)
+ pinterest: Music2,
+} as const;
+
+// ββ Social links row ββββββββββββββββββββββββββββββββββββββββββββββββββ
+function SocialLinks({
+ links,
+ dark,
+}: {
+ links: SocialLink[];
+ dark?: boolean;
+}) {
+ return (
+
+ {links.map((link) => {
+ const Icon = SOCIAL_ICONS[link.platform] ?? Facebook;
+ const platformName = link.platform.charAt(0).toUpperCase() + link.platform.slice(1);
+ return (
+
+
+
+ );
+ })}
+
+ );
+}
+
+// ββ Column link list ββββββββββββββββββββββββββββββββββββββββββββββββββ
+function FooterLinkColumn({
+ column,
+ dark,
+}: {
+ column: FooterColumn;
+ dark?: boolean;
+}) {
+ return (
+
+
+ {column.title}
+
+
+ {column.links.map((link) => (
+ -
+
+ {link.name}
+
+
+ ))}
+
+
+ );
+}
+
+// ββ Newsletter form (inline) ββββββββββββββββββββββββββββββββββββββββββ
+function NewsletterInline({
+ heading,
+ subheading,
+ dark,
+ formAction,
+}: {
+ heading?: string;
+ subheading?: string;
+ dark?: boolean;
+ formAction?: string;
+}) {
+ const [email, setEmail] = useState("");
+ const [submitted, setSubmitted] = useState(false);
+
+ return (
+
+ {heading && (
+
+ {heading}
+
+ )}
+ {subheading && (
+
+ {subheading}
+
+ )}
+ {submitted ? (
+
+ β You're subscribed!
+
+ ) : (
+
+ )}
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Four-column footer
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function FourColumnFooter({
+ storeName,
+ tagline,
+ logoSrc,
+ columns = [],
+ socialLinks = [],
+ copyrightText,
+}: FooterBlockProps) {
+ return (
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Simple centered footer
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function SimpleCenteredFooter({
+ storeName,
+ logoSrc,
+ columns = [],
+ socialLinks = [],
+ copyrightText,
+}: FooterBlockProps) {
+ const flatLinks = columns.flatMap((col) => col.links);
+
+ return (
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Footer with newsletter
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function WithNewsletterFooter({
+ storeName,
+ logoSrc,
+ tagline,
+ columns = [],
+ socialLinks = [],
+ copyrightText,
+ newsletterHeading,
+ newsletterSubheading,
+ newsletterFormAction,
+}: FooterBlockProps) {
+ return (
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Dark footer
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function DarkFooter({
+ storeName,
+ logoSrc,
+ tagline,
+ columns = [],
+ socialLinks = [],
+ copyrightText,
+}: FooterBlockProps) {
+ return (
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// FooterBlock β public API
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const FOOTER_MAP = {
+ "four-column": FourColumnFooter,
+ "simple-centered": SimpleCenteredFooter,
+ "with-newsletter": WithNewsletterFooter,
+ dark: DarkFooter,
+} as const;
+
+/**
+ * FooterBlock
+ *
+ * Drop-in store footer with multiple layout variants.
+ * @example
+ *
+ */
+export function FooterBlock(props: FooterBlockProps) {
+ const variant = props.variant ?? "four-column";
+ const Component = FOOTER_MAP[variant] ?? FourColumnFooter;
+ return ;
+}
+
+export { FourColumnFooter, SimpleCenteredFooter, WithNewsletterFooter, DarkFooter };
+export type { FooterBlockProps };
diff --git a/src/components/storefront/blocks/hero/hero-block.tsx b/src/components/storefront/blocks/hero/hero-block.tsx
new file mode 100644
index 00000000..0d2bd3e6
--- /dev/null
+++ b/src/components/storefront/blocks/hero/hero-block.tsx
@@ -0,0 +1,572 @@
+"use client";
+/**
+ * Hero Block Variants
+ *
+ * Multiple hero section designs ported from tailwind-css-prebuild-ui-themes.
+ * All colours are CSS-variable driven (bg-primary, text-primary, etc.)
+ * so they respond to the store owner's theme settings in real-time.
+ *
+ * Variants:
+ * β centered : centred text, optional badge, dual CTAs
+ * β split-left : content left, image right
+ * β split-right : image left, content right
+ * β image-tiles : full-bleed with decorative image tile grid
+ * β angled : content left, image right with diagonal clip
+ * β full-bleed : full-width background image with overlay
+ */
+
+import Link from "next/link";
+import Image from "next/image";
+import { cn } from "@/lib/utils";
+import type { HeroBlockProps, HeroVariant } from "../types";
+import { ArrowRight, ShoppingBag, Sparkles } from "lucide-react";
+
+// ββ Star rating helper (used in social-proof strip) ββββββββββββββββββββ
+function StarRating({ rating }: { rating: number }) {
+ return (
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+
+ );
+}
+
+// ββ Badge ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function AnnouncementBadge({
+ text,
+ href,
+}: {
+ text: string;
+ href?: string;
+}) {
+ const inner = (
+
+ {text}{" "}
+ {href && (
+
+
+ Read more →
+
+ )}
+
+ );
+ return (
+
+ {href ? {inner} : inner}
+
+ );
+}
+
+// ββ CTAs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function PrimaryCTAButton({ cta }: { cta: NonNullable }) {
+ return (
+
+
+ {cta.text}
+
+ );
+}
+
+function SecondaryCTAButton({ cta }: { cta: NonNullable }) {
+ return (
+
+ {cta.text}
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Variant: Centered
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function CenteredHero({
+ heading,
+ subheading,
+ announcement,
+ announcementHref,
+ primaryCTA,
+ secondaryCTA,
+ logoSrc,
+}: HeroBlockProps) {
+ return (
+
+ {/* Decorative blurred gradient circles */}
+
+
+
+
+ {logoSrc && (
+
+ )}
+
+ {announcement && (
+
+ )}
+
+
+ {heading}
+
+
+ {subheading && (
+
+ {subheading}
+
+ )}
+
+ {(primaryCTA || secondaryCTA) && (
+
+ {primaryCTA &&
}
+ {secondaryCTA &&
}
+
+ )}
+
+
+
+ {/* Bottom blur */}
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Variant: Split (left or right)
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function SplitHero({
+ heading,
+ subheading,
+ announcement,
+ announcementHref,
+ primaryCTA,
+ secondaryCTA,
+ image,
+ logoSrc,
+ variant = "split-left",
+}: HeroBlockProps) {
+ const imageRight = variant === "split-left";
+
+ return (
+
+
+ {/* Content col */}
+
+
+ {logoSrc && (
+
+ )}
+
+ {announcement && (
+
+ )}
+
+
+ {heading}
+
+
+ {subheading && (
+
+ {subheading}
+
+ )}
+
+ {(primaryCTA || secondaryCTA) && (
+
+ {primaryCTA &&
}
+ {secondaryCTA &&
}
+
+ )}
+
+
+
+ {/* Image col */}
+
+ {image ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Variant: Image Tiles
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function ImageTilesHero({
+ heading,
+ subheading,
+ announcement,
+ announcementHref,
+ primaryCTA,
+ secondaryCTA,
+ imageTiles = [],
+}: HeroBlockProps) {
+ const tiles =
+ imageTiles.length > 0
+ ? imageTiles
+ : Array.from({ length: 6 }, (_, i) => ({
+ src: "",
+ alt: `Tile image ${i + 1}`,
+ }));
+
+ return (
+
+ {/* Background grid pattern */}
+
+
+ {/* Gradient blob */}
+
+
+
+ {/* Content */}
+
+ {announcement && (
+
+ )}
+
+
+ {heading}
+
+
+ {subheading && (
+
+ {subheading}
+
+ )}
+
+ {(primaryCTA || secondaryCTA) && (
+
+ {primaryCTA &&
}
+ {secondaryCTA &&
}
+
+ )}
+
+ {/* Social proof */}
+
+
+
+ {[1, 2, 3].map((i) => (
+
+ {String.fromCharCode(64 + i)}
+
+ ))}
+
+
+
+ 500+ happy customers
+
+
+
+
+
+ {/* Image tiles grid */}
+
+
+
+
+ {tiles.slice(0, 6).map((tile, idx) => (
+
+ {tile.src ? (
+
+ ) : (
+
+ )}
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Variant: Full-bleed background image
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function FullBleedHero({
+ heading,
+ subheading,
+ announcement,
+ announcementHref,
+ primaryCTA,
+ secondaryCTA,
+ image,
+}: HeroBlockProps) {
+ return (
+
+ {/* Background image */}
+ {image ? (
+
+ ) : (
+
+ )}
+
+ {/* Overlay */}
+
+
+
+
+ {announcement && (
+
+
+ {announcement}
+ {announcementHref && (
+
+
+ Learn more →
+
+ )}
+
+
+ )}
+
+
+ {heading}
+
+
+ {subheading && (
+
+ {subheading}
+
+ )}
+
+ {(primaryCTA || secondaryCTA) && (
+
+ {primaryCTA && (
+
+ {primaryCTA.text}
+
+ )}
+ {secondaryCTA && (
+
+ {secondaryCTA.text}
+
+
+ )}
+
+ )}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Variant: Angled (content left, image right with clip-path)
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function AngledHero({
+ heading,
+ subheading,
+ announcement,
+ announcementHref,
+ primaryCTA,
+ secondaryCTA,
+ image,
+}: HeroBlockProps) {
+ return (
+
+ {/* Background gradient for the right half */}
+
+
+
+ {/* Content */}
+
+ {announcement && (
+
+ )}
+
+
+ {heading}
+
+
+ {subheading && (
+
+ {subheading}
+
+ )}
+
+
+ {primaryCTA &&
}
+ {secondaryCTA &&
}
+
+
+
+ {/* Image */}
+
+ {image ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Public API: HeroBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const VARIANT_MAP: Record> = {
+ centered: CenteredHero,
+ "split-left": SplitHero,
+ "split-right": SplitHero,
+ "image-tiles": ImageTilesHero,
+ angled: AngledHero,
+ "full-bleed": FullBleedHero,
+};
+
+/**
+ * HeroBlock
+ *
+ * Drop-in hero section with multiple visual variants.
+ * All colours respond to the active store theme.
+ *
+ * @example
+ *
+ */
+export function HeroBlock(props: HeroBlockProps) {
+ const variant: HeroVariant = props.variant ?? "centered";
+ const Component = VARIANT_MAP[variant] ?? CenteredHero;
+ return ;
+}
+
+export { CenteredHero, SplitHero, ImageTilesHero, FullBleedHero, AngledHero };
+export type { HeroBlockProps, HeroVariant };
diff --git a/src/components/storefront/blocks/index.ts b/src/components/storefront/blocks/index.ts
new file mode 100644
index 00000000..ade9d649
--- /dev/null
+++ b/src/components/storefront/blocks/index.ts
@@ -0,0 +1,256 @@
+/**
+ * Storefront UI Blocks β Barrel Export
+ *
+ * All pre-built, TypeScript-converted, theme-aware UI blocks.
+ * Ported from tailwind-css-prebuild-ui-themes and refactored for:
+ * β Next.js 16 App Router (Server + Client components)
+ * β React 19
+ * β Tailwind CSS v4
+ * β shadcn/ui (Sheet, Dialog, NavigationMenu, Tabs, RadioGroup, etc.)
+ * β CSS custom propertyβdriven theming (bg-primary, text-foregroundβ¦)
+ * β WCAG 2.2 AA accessibility
+ *
+ * Import individual blocks to keep bundle size minimal:
+ * @example
+ * import { HeroBlock } from "@/components/storefront/blocks";
+ * import { ProductCardBlock } from "@/components/storefront/blocks";
+ *
+ * Or all at once for dev / Storybook:
+ * @example
+ * import * as Blocks from "@/components/storefront/blocks";
+ */
+
+// ββ Types & Interfaces βββββββββββββββββββββββββββββββββββββββββββββββββ
+export type {
+ // Common
+ StorefrontImage,
+ StorefrontCTA,
+ StorefrontRating,
+ // Product
+ BlockProduct,
+ ProductCardProps,
+ ProductGridProps,
+ // Category
+ BlockCategory,
+ CategoryPreviewProps,
+ CategoryFiltersProps,
+ FilterGroup,
+ FilterOption,
+ ActiveFilter,
+ // Navigation
+ NavCategory,
+ NavSection,
+ NavPage,
+ StoreNavProps,
+ // Hero
+ HeroBlockProps,
+ HeroVariant,
+ // Cart
+ CartItem,
+ CartBlockProps,
+ // Promo
+ Incentive,
+ IncentivesProps,
+ PromoSectionProps,
+ // Reviews
+ Review,
+ ReviewsSummary,
+ ReviewsSectionProps,
+ // Footer
+ FooterColumn,
+ SocialLink,
+ FooterBlockProps,
+ // Marketing
+ Feature,
+ FeaturesBlockProps,
+ Testimonial,
+ TestimonialsBlockProps,
+ CTASectionProps,
+ NewsletterBlockProps,
+} from "./types";
+
+// ββ Hero βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+export { HeroBlock } from "./hero/hero-block";
+
+// ββ Navigation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+export { MegaNavBlock } from "./navigation/mega-nav-block";
+
+// ββ Category βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+export { CategoryPreviewBlock } from "./category/category-preview-block";
+export { CategoryFiltersBar, FilterSidebar, FilterMobileDrawer, ActiveFiltersBar, SortDropdown } from "./category/category-filters-block";
+
+// ββ Product ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+export { ProductCardBlock } from "./product/product-card-block";
+export { ProductOverviewBlock } from "./product/product-overview-block";
+export type { ProductOverviewBlockProps } from "./product/product-overview-block";
+export { ProductQuickviewBlock } from "./product/product-quickview-block";
+export type { ProductQuickviewBlockProps } from "./product/product-quickview-block";
+
+// ββ Cart βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+export { CartSlideoverBlock } from "./cart/cart-slideover-block";
+
+// ββ Promo & Incentives βββββββββββββββββββββββββββββββββββββββββββββββββ
+export { IncentivesBlock, PromoSectionBlock } from "./promo/promo-blocks";
+
+// ββ Reviews ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+export { ReviewsSectionBlock } from "./reviews/reviews-section-block";
+
+// ββ Checkout βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+export { CheckoutFormBlock } from "./checkout/checkout-form-block";
+export type {
+ CheckoutFormBlockProps,
+ CheckoutFormValues,
+ ShippingOption as CheckoutShippingOption,
+ PaymentMethod as CheckoutPaymentMethod,
+} from "./checkout/checkout-form-block";
+
+// ββ Order ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+export { OrderSummaryBlock } from "./order/order-summary-block";
+export type {
+ OrderSummaryBlockProps,
+ OrderLine,
+} from "./order/order-summary-block";
+
+// ββ Footer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+export { FooterBlock } from "./footer/footer-block";
+
+// ββ Marketing βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+export {
+ FeaturesBlock,
+ TestimonialsBlock,
+ CTASectionBlock,
+ NewsletterBlock,
+ StatsBlock,
+} from "./marketing/marketing-blocks";
+export type { Stat, StatsBlockProps } from "./marketing/marketing-blocks";
+
+// ββ Block registry (for Theme Editor "Add Section" modal) ββββββββββββββ
+/**
+ * BLOCK_REGISTRY
+ *
+ * A flat list of all available storefront blocks, used to populate
+ * the Theme Editor "Add Section" modal.
+ *
+ * Each entry has:
+ * β id : stable identifier (used in storefrontConfig.sections)
+ * β category : UI grouping in the modal
+ * β name : Display name
+ * β variants : Available layout variants for the block
+ * β component : The React component to render
+ */
+export const BLOCK_REGISTRY = [
+ {
+ id: "hero",
+ category: "Hero",
+ name: "Hero Section",
+ variants: ["centered", "split-left", "split-right", "image-tiles", "angled", "full-bleed"],
+ description: "Full-width introductory banner with headline, CTAs, and image.",
+ thumbnailEmoji: "π
",
+ },
+ {
+ id: "category-preview",
+ category: "Navigation & Categories",
+ name: "Category Preview",
+ variants: ["three-column", "image-backgrounds", "split-images", "scrolling-cards"],
+ description: "Showcase your product categories with images.",
+ thumbnailEmoji: "ποΈ",
+ },
+ {
+ id: "mega-nav",
+ category: "Navigation & Categories",
+ name: "Mega Navigation",
+ variants: ["default"],
+ description: "Full-featured navigation with mega dropdown menus and mobile drawer.",
+ thumbnailEmoji: "π§",
+ },
+ {
+ id: "product-card-grid",
+ category: "Products",
+ name: "Product Grid",
+ variants: ["standard", "detailed", "overlay", "minimal", "horizontal", "tall-image"],
+ description: "Responsive product card grid with add-to-cart and quick-view.",
+ thumbnailEmoji: "ποΈ",
+ },
+ {
+ id: "product-overview",
+ category: "Products",
+ name: "Product Detail",
+ variants: ["split-with-image", "with-image-grid", "with-tabs"],
+ description: "Full product page section with image gallery, pricing, and purchase form.",
+ thumbnailEmoji: "π¦",
+ },
+ {
+ id: "incentives",
+ category: "Trust & Promotions",
+ name: "Trust Incentives",
+ variants: ["strip", "cards"],
+ description: "Free shipping, easy returns, and secure payment trust signals.",
+ thumbnailEmoji: "β
",
+ },
+ {
+ id: "promo-section",
+ category: "Trust & Promotions",
+ name: "Promo Section",
+ variants: ["centered", "with-image", "split-image"],
+ description: "Promotional full-width section with headline, description and CTAs.",
+ thumbnailEmoji: "π―",
+ },
+ {
+ id: "reviews",
+ category: "Social Proof",
+ name: "Reviews",
+ variants: ["simple", "grid", "with-summary"],
+ description: "Product or store review display with star ratings.",
+ thumbnailEmoji: "β",
+ },
+ {
+ id: "testimonials",
+ category: "Social Proof",
+ name: "Testimonials",
+ variants: ["grid", "carousel", "with-image"],
+ description: "Customer testimonials to build trust.",
+ thumbnailEmoji: "π¬",
+ },
+ {
+ id: "features",
+ category: "Content",
+ name: "Features",
+ variants: ["grid", "list", "offset"],
+ description: "Highlight store or product features with icons.",
+ thumbnailEmoji: "β¨",
+ },
+ {
+ id: "stats",
+ category: "Content",
+ name: "Stats",
+ variants: ["default"],
+ description: "Showcase key metrics and milestones.",
+ thumbnailEmoji: "π",
+ },
+ {
+ id: "cta-section",
+ category: "Content",
+ name: "Call to Action",
+ variants: ["simple-centered", "banner"],
+ description: "Compelling call-to-action section to drive conversions.",
+ thumbnailEmoji: "π£",
+ },
+ {
+ id: "newsletter",
+ category: "Content",
+ name: "Newsletter",
+ variants: ["centered", "split", "inline"],
+ description: "Email newsletter sign-up form.",
+ thumbnailEmoji: "π§",
+ },
+ {
+ id: "footer",
+ category: "Footer",
+ name: "Footer",
+ variants: ["four-column", "simple-centered", "with-newsletter", "dark"],
+ description: "Store footer with navigation links, social media, and copyright.",
+ thumbnailEmoji: "π",
+ },
+] as const;
+
+export type BlockId = (typeof BLOCK_REGISTRY)[number]["id"];
diff --git a/src/components/storefront/blocks/marketing/marketing-blocks.tsx b/src/components/storefront/blocks/marketing/marketing-blocks.tsx
new file mode 100644
index 00000000..6ae31ddb
--- /dev/null
+++ b/src/components/storefront/blocks/marketing/marketing-blocks.tsx
@@ -0,0 +1,660 @@
+"use client";
+/**
+ * Marketing Section Blocks
+ *
+ * Ported from tailwind-css-prebuild-ui-themes/react/components/marketing.
+ * Includes:
+ * β FeaturesBlock : feature grid / list / offset (with or without image)
+ * β TestimonialsBlock : testimonials grid / carousel / with-image
+ * β CTASectionBlock : CTA sections (centered, split, banner)
+ * β NewsletterBlock : newsletter sign-up (centered, split, inline)
+ * β StatsBlock : key stat numbers
+ *
+ * All colours use CSS theme variables.
+ */
+
+import { useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import * as LucideIcons from "lucide-react";
+import { Star, ArrowRight, CheckCircle } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { Input } from "@/components/ui/input";
+import { Button } from "@/components/ui/button";
+import type {
+ FeaturesBlockProps,
+ TestimonialsBlockProps,
+ CTASectionProps,
+ NewsletterBlockProps,
+ Feature,
+ Testimonial,
+} from "../types";
+
+// ββ Dynamic lucide icon βββββββββββββββββββββββββββββββββββββββββββββββ
+function DynamicIcon({
+ name,
+ className,
+}: {
+ name: string;
+ className?: string;
+}) {
+ const pascal = name
+ .split("-")
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
+ .join("");
+ const Icon =
+ (LucideIcons as unknown as Record>)[pascal] ??
+ CheckCircle;
+ return ;
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// FeaturesBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function FeatureItem({ feature }: { feature: Feature }) {
+ return (
+
+
+
+
+
+
{feature.name}
+
+ {feature.description}
+
+
+
+ );
+}
+
+/**
+ * FeaturesBlock
+ *
+ * Feature highlight section: grid, list, or offset with image.
+ */
+export function FeaturesBlock({
+ heading,
+ subheading,
+ features,
+ variant = "grid",
+ image,
+}: FeaturesBlockProps) {
+ if (variant === "offset") {
+ return (
+
+
+ {/* Left content */}
+
+
+ {heading}
+
+ {subheading && (
+
{subheading}
+ )}
+ {image && (
+
+
+
+ )}
+
+
+ {/* Right feature list */}
+
+
+ {features.map((feature) => (
+
+ ))}
+
+
+
+
+ );
+ }
+
+ if (variant === "list") {
+ return (
+
+
+
+
+
+ {heading}
+
+ {subheading && (
+
{subheading}
+ )}
+
+
+ {features.map((feature) => (
+
+
+
+ ))}
+
+
+
+
+ );
+ }
+
+ // grid (default)
+ return (
+
+
+
+
+ {heading}
+
+ {subheading && (
+
{subheading}
+ )}
+
+
+ {features.map((feature) => (
+
+ ))}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// TestimonialsBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function TestimonialCard({ t }: { t: Testimonial }) {
+ return (
+
+ {t.rating !== undefined && (
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+
+ )}
+
+ “{t.content}”
+
+
+ {t.avatar ? (
+
+ ) : (
+
+ {t.author.charAt(0)}
+
+ )}
+
+
{t.author}
+ {(t.role || t.company) && (
+
+ {[t.role, t.company].filter(Boolean).join(", ")}
+
+ )}
+
+
+
+ );
+}
+
+/**
+ * TestimonialsBlock
+ *
+ * Customer testimonials in grid or with-image layout.
+ */
+export function TestimonialsBlock({
+ heading,
+ subheading,
+ testimonials,
+ variant = "grid",
+}: TestimonialsBlockProps) {
+ const [current, setCurrent] = useState(0);
+
+ if (variant === "carousel") {
+ const t = testimonials[current];
+ return (
+
+
+ {heading && (
+
+ {heading}
+
+ )}
+
+
+ {/* dots */}
+
+ {testimonials.map((_, idx) => (
+
+
+
+
+ );
+ }
+
+ if (variant === "with-image" && testimonials[0]) {
+ const featured = testimonials[0];
+ return (
+
+
+
+
+ {heading && (
+
+ {heading}
+
+ )}
+ {subheading && (
+
{subheading}
+ )}
+
+ {featured.rating !== undefined && (
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+
+ )}
+
+ “{featured.content}”
+
+
+ {featured.avatar && (
+
+ )}
+
+
{featured.author}
+ {(featured.role || featured.company) && (
+
+ {[featured.role, featured.company].filter(Boolean).join(", ")}
+
+ )}
+
+
+
+
+ {/* Additional cards */}
+
+ {testimonials.slice(1, 3).map((t) => (
+
+ ))}
+
+
+
+
+ );
+ }
+
+ // grid (default)
+ return (
+
+
+ {(heading || subheading) && (
+
+ {heading && (
+
+ {heading}
+
+ )}
+ {subheading && (
+
{subheading}
+ )}
+
+ )}
+
+ {testimonials.map((t) => (
+
+ ))}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// CTASectionBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * CTASectionBlock
+ *
+ * Call-to-action section.
+ */
+export function CTASectionBlock({
+ heading,
+ subheading,
+ primaryCTA,
+ secondaryCTA,
+ backgroundClass = "bg-primary",
+ variant = "simple-centered",
+ image,
+}: CTASectionProps) {
+ if (variant === "banner") {
+ return (
+
+
+
+
+ {heading}
+
+ {subheading && (
+
{subheading}
+ )}
+ {(primaryCTA || secondaryCTA) && (
+
+ {primaryCTA && (
+
+ {primaryCTA.text}
+
+ )}
+ {secondaryCTA && (
+
+ {secondaryCTA.text}
+
+
+ )}
+
+ )}
+
+
+ {/* Decorative blobs */}
+
+
+ );
+ }
+
+ // simple-centered (default)
+ return (
+
+
+
+
+ {heading}
+
+ {subheading && (
+
{subheading}
+ )}
+ {(primaryCTA || secondaryCTA) && (
+
+ {primaryCTA && (
+
+ {primaryCTA.text}
+
+ )}
+ {secondaryCTA && (
+
+ {secondaryCTA.text}
+
+
+ )}
+
+ )}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// NewsletterBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * NewsletterBlock
+ *
+ * Email newsletter sign-up section.
+ */
+export function NewsletterBlock({
+ heading,
+ subheading,
+ placeholderText = "Enter your email",
+ buttonText = "Subscribe",
+ formAction,
+ variant = "centered",
+ privacyText,
+}: NewsletterBlockProps) {
+ const [email, setEmail] = useState("");
+ const [submitted, setSubmitted] = useState(false);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ setSubmitted(true);
+ };
+
+ if (variant === "split") {
+ return (
+
+
+
+
+ {heading}
+
+ {subheading && (
+
{subheading}
+ )}
+
+
+ {submitted ? (
+
+
+ Thanks for subscribing!
+
+ ) : (
+
+ )}
+
+
+
+ );
+ }
+
+ if (variant === "inline") {
+ return (
+
+ {(heading || subheading) && (
+
+ {heading &&
{heading}
}
+ {subheading &&
{subheading}
}
+
+ )}
+ {submitted ? (
+
+ Subscribed!
+
+ ) : (
+
+ )}
+
+ );
+ }
+
+ // centered
+ return (
+
+
+
+
+ {heading}
+
+ {subheading && (
+
{subheading}
+ )}
+ {submitted ? (
+
+ Thanks for subscribing!
+
+ ) : (
+
+ )}
+ {privacyText && (
+
{privacyText}
+ )}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// StatsBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface Stat {
+ value: string;
+ label: string;
+ description?: string;
+}
+
+export interface StatsBlockProps {
+ heading?: string;
+ subheading?: string;
+ stats: Stat[];
+ backgroundClass?: string;
+}
+
+/**
+ * StatsBlock
+ *
+ * Key numbers / statistics section.
+ */
+export function StatsBlock({ heading, subheading, stats, backgroundClass }: StatsBlockProps) {
+ return (
+
+
+ {(heading || subheading) && (
+
+ {heading && (
+
+ {heading}
+
+ )}
+ {subheading && (
+
{subheading}
+ )}
+
+ )}
+
+ {stats.map((stat, idx) => (
+
+
- {stat.label}
+
-
+ {stat.value}
+
+ {stat.description && (
+
{stat.description}
+ )}
+
+ ))}
+
+
+
+ );
+}
+
+export type {
+ FeaturesBlockProps,
+ TestimonialsBlockProps,
+ CTASectionProps,
+ NewsletterBlockProps,
+};
diff --git a/src/components/storefront/blocks/navigation/mega-nav-block.tsx b/src/components/storefront/blocks/navigation/mega-nav-block.tsx
new file mode 100644
index 00000000..90b310d9
--- /dev/null
+++ b/src/components/storefront/blocks/navigation/mega-nav-block.tsx
@@ -0,0 +1,431 @@
+"use client";
+/**
+ * Mega Navigation Block
+ *
+ * Ported from tailwind-css-prebuild-ui-themes
+ * ecommerce/store-navigation/with_centered_logo_and_featured_categories.jsx
+ *
+ * Replaces:
+ * - @headlessui/react Popover β shadcn NavigationMenu
+ * - @headlessui/react Dialog β shadcn Sheet (mobile)
+ * - @heroicons/react β lucide-react
+ *
+ * All colours use CSS theme variables.
+ */
+
+import { useState } from "react";
+import Link from "next/link";
+import Image from "next/image";
+import { ShoppingBag, Search, User, ChevronDown, Menu, X, ArrowRight } from "lucide-react";
+import {
+ NavigationMenu,
+ NavigationMenuContent,
+ NavigationMenuItem,
+ NavigationMenuLink,
+ NavigationMenuList,
+ NavigationMenuTrigger,
+} from "@/components/ui/navigation-menu";
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet";
+import { Button } from "@/components/ui/button";
+import { Separator } from "@/components/ui/separator";
+import { cn } from "@/lib/utils";
+import type { StoreNavProps, NavCategory } from "../types";
+
+// ββ Featured item inside mega menu ββββββββββββββββββββββββββββββββββββ
+function FeaturedItem({
+ item,
+}: {
+ item: NonNullable[number];
+}) {
+ return (
+
+
+
+
+
+
+
+ {item.name}
+
+
+ Shop now
+
+
+
+
+ );
+}
+
+// ββ Mega menu category panel βββββββββββββββββββββββββββββββββββββββββββ
+function CategoryMegaMenu({ category }: { category: NavCategory }) {
+ const hasFeatured = (category.featured?.length ?? 0) > 0;
+ const sections = category.sections ?? [];
+
+ return (
+
+
+ {/* Featured images */}
+ {hasFeatured && (
+
+ {category.featured!.map((item) => (
+
+ ))}
+
+ )}
+
+ {/* Section links */}
+ {sections.map((sectionGroup, groupIdx) => (
+
+ {sectionGroup.map((section) => (
+
+
+ {section.name}
+
+
+ {section.items.map((item) => (
+ -
+
+
+ {item.name}
+
+
+
+ ))}
+
+
+ ))}
+
+ ))}
+
+
+ );
+}
+
+// ββ Mobile nav drawer βββββββββββββββββββββββββββββββββββββββββββββββββ
+function MobileNav({
+ open,
+ onClose,
+ categories = [],
+ pages = [],
+ storeName,
+ storeLogo,
+ homeHref,
+}: Pick & {
+ open: boolean;
+ onClose: () => void;
+}) {
+ const [openCategory, setOpenCategory] = useState(null);
+
+ return (
+ !v && onClose()}>
+
+
+
+
+
+ {storeLogo ? (
+
+ ) : (
+ {storeName}
+ )}
+
+
+
+
+
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// MegaNavBlock β full storefront navigation header
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * MegaNavBlock
+ *
+ * Responsive storefront mega navigation header.
+ * Desktop: shadcn NavigationMenu with hover mega panels
+ * Mobile: Sheet drawer (left slide-in)
+ * All colours theme-variable driven.
+ *
+ * @example
+ * setCartOpen(true)}
+ * />
+ */
+export function MegaNavBlock({
+ storeName,
+ storeLogo,
+ homeHref,
+ categories = [],
+ pages = [],
+ cartCount = 0,
+ onCartOpen,
+ onSearchOpen,
+ announcement,
+ announcementHref,
+}: StoreNavProps) {
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ return (
+ <>
+
+ {/* Announcement bar */}
+ {announcement && (
+
+
+
+ {announcementHref ? (
+ <>
+ {announcement}{" "}
+
+ Shop now →
+
+ >
+ ) : (
+ announcement
+ )}
+
+
+
+ )}
+
+
+
+
+ {/* Mobile drawer */}
+ setMobileOpen(false)}
+ categories={categories}
+ pages={pages}
+ storeName={storeName}
+ storeLogo={storeLogo}
+ homeHref={homeHref}
+ />
+ >
+ );
+}
+
+export type { StoreNavProps };
diff --git a/src/components/storefront/blocks/order/order-summary-block.tsx b/src/components/storefront/blocks/order/order-summary-block.tsx
new file mode 100644
index 00000000..fa725cbb
--- /dev/null
+++ b/src/components/storefront/blocks/order/order-summary-block.tsx
@@ -0,0 +1,343 @@
+"use client";
+/**
+ * Order Summary Block
+ *
+ * Ported from tailwind-css-prebuild-ui-themes ecommerce/order-summaries.
+ * Three variants:
+ * β simple : flat list + totals
+ * β with-image : items with product images
+ * β two-column : order items + totals side-by-side (wider screens)
+ *
+ * All colours use CSS theme variables.
+ */
+
+import Image from "next/image";
+import Link from "next/link";
+import { CheckCircle2, Package } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { Separator } from "@/components/ui/separator";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import type { CartItem } from "../types";
+
+// βββ Types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface OrderLine {
+ id: string;
+ name: string;
+ description?: string;
+ quantity: number;
+ price: string;
+ imageSrc?: string;
+ imageAlt?: string;
+ href?: string;
+ status?: "processing" | "shipped" | "delivered" | "cancelled";
+}
+
+export interface OrderSummaryBlockProps {
+ orderNumber?: string;
+ orderDate?: string;
+ items: OrderLine[];
+ subtotal: string;
+ discount?: string;
+ shipping?: string;
+ tax?: string;
+ total: string;
+ paymentMethod?: string;
+ shippingAddress?: {
+ name: string;
+ line1: string;
+ line2?: string;
+ city: string;
+ state: string;
+ zip: string;
+ country: string;
+ };
+ variant?: "simple" | "with-image" | "two-column";
+ continueShoppingHref?: string;
+ trackOrderHref?: string;
+}
+
+// βββ Totals block βββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function TotalsBlock({
+ subtotal,
+ discount,
+ shipping,
+ tax,
+ total,
+ paymentMethod,
+}: Pick) {
+ const rows: Array<{ label: string; value: string; highlight?: boolean }> = [
+ { label: "Subtotal", value: subtotal },
+ ];
+ if (discount) rows.push({ label: "Discount", value: `-${discount}` });
+ if (shipping) rows.push({ label: "Shipping", value: shipping });
+ if (tax) rows.push({ label: "Tax", value: tax });
+
+ return (
+
+ {rows.map((row) => (
+
+ {row.label}
+
+ {row.value}
+
+
+ ))}
+
+
+ Total
+ {total}
+
+ {paymentMethod && (
+
+ Paid via {paymentMethod}
+
+ )}
+
+ );
+}
+
+// βββ Status badge βββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function StatusBadge({ status }: { status: OrderLine["status"] }) {
+ if (!status) return null;
+ const map = {
+ processing: { label: "Processing", className: "bg-yellow-100 text-yellow-800 border-yellow-200" },
+ shipped: { label: "Shipped", className: "bg-blue-100 text-blue-800 border-blue-200" },
+ delivered: { label: "Delivered", className: "bg-green-100 text-green-800 border-green-200" },
+ cancelled: { label: "Cancelled", className: "bg-red-100 text-red-800 border-red-200" },
+ };
+ const { label, className } = map[status];
+ return (
+
+ {label}
+
+ );
+}
+
+// βββ Simple order line ββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function SimpleOrderLine({ item }: { item: OrderLine }) {
+ return (
+
+
+
{item.name}
+ {item.description && (
+
{item.description}
+ )}
+
Qty: {item.quantity}
+
+
+
{item.price}
+ {item.status &&
}
+
+
+ );
+}
+
+// βββ Image order line βββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function ImageOrderLine({ item }: { item: OrderLine }) {
+ const content = (
+
+
+ {item.imageSrc ? (
+
+ ) : (
+
+ )}
+
+
+
{item.name}
+ {item.description && (
+
{item.description}
+ )}
+
Qty: {item.quantity}
+ {item.status && (
+
+
+
+ )}
+
+
+
+ );
+
+ return item.href ? {content} : content;
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// OrderSummaryBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * OrderSummaryBlock
+ *
+ * Post-checkout order confirmation / order details display.
+ *
+ * @example
+ *
+ */
+export function OrderSummaryBlock({
+ orderNumber,
+ orderDate,
+ items,
+ subtotal,
+ discount,
+ shipping,
+ tax,
+ total,
+ paymentMethod,
+ shippingAddress,
+ variant = "with-image",
+ continueShoppingHref,
+ trackOrderHref,
+}: OrderSummaryBlockProps) {
+ const renderItems = (LineComp: typeof SimpleOrderLine | typeof ImageOrderLine) =>
+ items.map((item) => );
+
+ return (
+
+
+ {/* Confirmation header */}
+ {orderNumber && (
+
+
+
+
+
+ Order confirmed
+
+
+
+ Order{" "}
+ #{orderNumber}
+ {orderDate && (
+ <>
+ {" "}Β·{" "}
+
+ >
+ )}
+
+
+
+ )}
+
+ {variant === "two-column" ? (
+
+ {/* Items (left) */}
+
+
Items
+ {renderItems(ImageOrderLine)}
+
+
+ {/* Summary (right) */}
+
+
+
Order summary
+
+
+
+ {shippingAddress && (
+
+
Shipping to
+
+ {shippingAddress.name}
+ {shippingAddress.line1}
+ {shippingAddress.line2 && {shippingAddress.line2}
}
+
+ {shippingAddress.city}, {shippingAddress.state} {shippingAddress.zip}
+
+ {shippingAddress.country}
+
+
+ )}
+
+
+ ) : (
+ <>
+ {/* Items */}
+ {variant === "with-image"
+ ? renderItems(ImageOrderLine)
+ : renderItems(SimpleOrderLine)}
+
+ {/* Totals */}
+
+
+
+
+ {shippingAddress && (
+
+
Shipping to
+
+ {shippingAddress.name}
+ {shippingAddress.line1}
+ {shippingAddress.line2 && {shippingAddress.line2}
}
+
+ {shippingAddress.city}, {shippingAddress.state} {shippingAddress.zip}
+
+ {shippingAddress.country}
+
+
+ )}
+ >
+ )}
+
+ {/* Actions */}
+ {(continueShoppingHref || trackOrderHref) && (
+
+ {continueShoppingHref && (
+
+ )}
+ {trackOrderHref && (
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/storefront/blocks/product/product-card-block.tsx b/src/components/storefront/blocks/product/product-card-block.tsx
new file mode 100644
index 00000000..bbde5d67
--- /dev/null
+++ b/src/components/storefront/blocks/product/product-card-block.tsx
@@ -0,0 +1,647 @@
+"use client";
+/**
+ * Product Card Block Variants
+ *
+ * Multiple product card/grid designs ported from tailwind-css-prebuild-ui-themes.
+ * Theme-variable driven β all hard-coded Tailwind palette utilities replaced
+ * with CSS variable-backed classes (bg-primary, text-foreground, etc.)
+ *
+ * Card Variants:
+ * β standard : Image + name + price (clean, universal)
+ * β detailed : Card with description, options line, full price
+ * β overlay : Image fills card; name/price appear on hover
+ * β minimal : Just image + subtle name + price (luxury feel)
+ * β horizontal : Thumbnail left, details right (list view)
+ * β tall-image : Portrait-aspect image, minimal footer
+ */
+
+import Link from "next/link";
+import Image from "next/image";
+import { cn } from "@/lib/utils";
+import { ShoppingCart, Heart, Eye, Star } from "lucide-react";
+import type { ProductCardProps, ProductCardVariant, ProductGridProps, BlockProduct } from "../types";
+
+// ββ Star rating ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function StarRating({
+ rating,
+ count,
+}: {
+ rating: number;
+ count: number;
+}) {
+ return (
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+ ({count})
+
+ );
+}
+
+// ββ Discount badge βββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function DiscountBadge({ text }: { text: string }) {
+ return (
+
+ {text}
+
+ );
+}
+
+// ββ "New" badge ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function NewBadge() {
+ return (
+
+ New
+
+ );
+}
+
+// ββ AddToCart icon button ββββββββββββββββββββββββββββββββββββββββββββββ
+function AddToCartBtn({
+ productId,
+ name,
+ onAddToCart,
+}: {
+ productId: string;
+ name: string;
+ onAddToCart?: (id: string) => void;
+}) {
+ if (!onAddToCart) return null;
+ return (
+
+ );
+}
+
+// ββ Quick-view button ββββββββββββββββββββββββββββββββββββββββββββββββββ
+function QuickViewBtn({
+ productId,
+ name,
+ onQuickView,
+}: {
+ productId: string;
+ name: string;
+ onQuickView?: (id: string) => void;
+}) {
+ if (!onQuickView) return null;
+ return (
+
+ );
+}
+
+// ββ Price display ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function PriceDisplay({
+ price,
+ compareAtPrice,
+ className,
+}: {
+ price: string;
+ compareAtPrice?: string;
+ className?: string;
+}) {
+ if (compareAtPrice) {
+ return (
+
+
+ {price}
+
+
+ {compareAtPrice}
+
+
+ );
+ }
+ return (
+
+ {price}
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Variant: Standard
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function StandardCard({
+ id,
+ name,
+ href,
+ price,
+ compareAtPrice,
+ badge,
+ isNew,
+ image,
+ rating,
+ onAddToCart,
+ onQuickView,
+}: ProductCardProps) {
+ return (
+
+ {/* Image container */}
+
+
+ {image.src ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ {/* Badges */}
+ {badge &&
}
+ {isNew && !badge &&
}
+
+ {/* Action buttons (appear on hover) */}
+
+
+
+ {/* Details */}
+
+
+
+
+ {name}
+
+
+ {rating &&
}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Variant: Detailed (with description + options)
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function DetailedCard({
+ id,
+ name,
+ href,
+ price,
+ compareAtPrice,
+ badge,
+ isNew,
+ description,
+ options,
+ image,
+ rating,
+ onAddToCart,
+ onQuickView,
+}: ProductCardProps) {
+ return (
+
+ {/* Image */}
+
+
+ {image.src ? (
+
+ ) : (
+
+
+
+ )}
+
+ {badge &&
}
+ {isNew && !badge &&
}
+
+
+ {/* Content */}
+
+
+
+
+ {name}
+
+
+ {description && (
+
{description}
+ )}
+ {rating &&
}
+
+
+ {options && (
+
{options}
+ )}
+
+
+
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Variant: Overlay (image fills card, text on hover overlay)
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function OverlayCard({
+ id,
+ name,
+ href,
+ price,
+ compareAtPrice,
+ badge,
+ isNew,
+ image,
+ onAddToCart,
+}: ProductCardProps) {
+ return (
+
+ {/* Full-bleed image */}
+
+
+ {image.src ? (
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {badge &&
}
+ {isNew && !badge &&
}
+
+ {/* Overlay panel */}
+
+
+ {onAddToCart && (
+
+ )}
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Variant: Minimal (luxury/editorial feel)
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function MinimalCard({
+ id,
+ name,
+ href,
+ price,
+ compareAtPrice,
+ badge,
+ image,
+ onAddToCart,
+}: ProductCardProps) {
+ return (
+
+ {/* Image */}
+
+
+ {image.src ? (
+
+ ) : (
+
+ )}
+
+ {badge && (
+
+ {badge}
+
+ )}
+ {onAddToCart && (
+
+ )}
+
+
+ {/* Text β minimal */}
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Variant: Horizontal (list view)
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function HorizontalCard({
+ id,
+ name,
+ href,
+ price,
+ compareAtPrice,
+ badge,
+ description,
+ options,
+ image,
+ rating,
+ onAddToCart,
+}: ProductCardProps) {
+ return (
+
+ {/* Thumbnail */}
+
+
+ {image.src ? (
+
+ ) : (
+
+ )}
+
+ {badge &&
}
+
+
+ {/* Details */}
+
+
+
+
+ {name}
+
+
+ {description && (
+
{description}
+ )}
+ {options && (
+
{options}
+ )}
+ {rating &&
}
+
+
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Variant: Tall image
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function TallImageCard({
+ id,
+ name,
+ href,
+ price,
+ compareAtPrice,
+ badge,
+ isNew,
+ image,
+ onAddToCart,
+}: ProductCardProps) {
+ return (
+
+
+
+ {image.src ? (
+
+ ) : (
+
+ )}
+
+ {badge &&
}
+ {isNew && !badge &&
}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Map of variants
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const CARD_MAP: Record> = {
+ standard: StandardCard,
+ detailed: DetailedCard,
+ overlay: OverlayCard,
+ minimal: MinimalCard,
+ horizontal: HorizontalCard,
+ "tall-image": TallImageCard,
+};
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// ProductCardBlock β renders a single card in the chosen variant
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * ProductCardBlock
+ *
+ * Renders a single product card in one of the available visual styles.
+ * All colours respond to the active store theme.
+ */
+export function ProductCardBlock(props: ProductCardProps) {
+ const variant: ProductCardVariant = props.variant ?? "standard";
+ const Component = CARD_MAP[variant] ?? StandardCard;
+ return ;
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// ProductGridBlock β responsive grid of ProductCardBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+const GRID_CLASS: Record<2 | 3 | 4, string> = {
+ 2: "grid-cols-1 sm:grid-cols-2",
+ 3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
+ 4: "grid-cols-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
+};
+
+/**
+ * ProductGridBlock
+ *
+ * Responsive product grid with optional heading, CTA link and multi-variant cards.
+ *
+ * @example
+ *
+ */
+export function ProductGridBlock({
+ products,
+ variant = "standard",
+ columns = 4,
+ onAddToCart,
+ onQuickView,
+ heading,
+ cta,
+}: ProductGridProps) {
+ const isHorizontal = variant === "horizontal";
+
+ return (
+
+ {(heading || cta) && (
+
+ {heading && (
+
{heading}
+ )}
+ {cta && (
+
+ {cta.text}
+ β
+
+ )}
+
+ )}
+
+ {isHorizontal ? (
+
+ {products.map((product) => (
+
+ ))}
+
+ ) : (
+
+ {products.map((product) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+export {
+ StandardCard,
+ DetailedCard,
+ OverlayCard,
+ MinimalCard,
+ HorizontalCard,
+ TallImageCard,
+};
+export type { ProductCardProps, ProductCardVariant, ProductGridProps };
diff --git a/src/components/storefront/blocks/product/product-overview-block.tsx b/src/components/storefront/blocks/product/product-overview-block.tsx
new file mode 100644
index 00000000..d0fbcfd6
--- /dev/null
+++ b/src/components/storefront/blocks/product/product-overview-block.tsx
@@ -0,0 +1,464 @@
+"use client";
+/**
+ * Product Overview Block
+ *
+ * Ported from tailwind-css-prebuild-ui-themes ecommerce/product-overviews.
+ * Three layout variants:
+ * β split-with-image : left image gallery + right product details
+ * β with-image-grid : image mosaic + product details below
+ * β with-tiered-images : alternating large+small image grid + details
+ *
+ * All colours use CSS theme variables.
+ */
+
+import { useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { Star, Heart, Share2, ChevronLeft, ChevronRight, Minus, Plus } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "@/components/ui/tabs";
+import type { BlockProduct, StorefrontImage } from "../types";
+
+// ββ Star row ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function StarRow({ value, count }: { value: number; count: number }) {
+ return (
+
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+
+
+ {count.toLocaleString()} reviews
+
+
+ );
+}
+
+// ββ Thumbnail strip βββββββββββββββββββββββββββββββββββββββββββββββββββ
+function ThumbnailStrip({
+ images,
+ activeIdx,
+ onSelect,
+}: {
+ images: StorefrontImage[];
+ activeIdx: number;
+ onSelect: (idx: number) => void;
+}) {
+ return (
+
+ {images.map((img, idx) => (
+
+ ))}
+
+ );
+}
+
+// ββ Product details panel (shared across variants) βββββββββββββββββββββ
+interface ProductDetailsPanelProps {
+ product: BlockProduct;
+ onAddToCart?: (id: string, quantity: number) => void;
+ onWishlist?: (id: string) => void;
+}
+
+function ProductDetailsPanel({
+ product,
+ onAddToCart,
+ onWishlist,
+}: ProductDetailsPanelProps) {
+ const [qty, setQty] = useState(1);
+ const [wishlisted, setWishlisted] = useState(false);
+
+ const handleWishlist = () => {
+ setWishlisted((v) => !v);
+ onWishlist?.(product.id);
+ };
+
+ return (
+
+ {/* Heading + badges */}
+
+ {(product.isNew || product.isBestSeller || product.isSale) && (
+
+ {product.isNew && New}
+ {product.isBestSeller && Best Seller}
+ {product.isSale && Sale}
+
+ )}
+
+ {product.name}
+
+
+
+ {/* Ratings */}
+ {product.rating && (
+
+ )}
+
+ {/* Pricing */}
+
+ {product.price}
+ {product.compareAtPrice && (
+
+ {product.compareAtPrice}
+
+ )}
+ {product.badge && (
+ {product.badge}
+ )}
+
+
+ {/* Description */}
+ {product.description && (
+
+ {product.description}
+
+ )}
+
+ {/* Category */}
+ {product.category && (
+
+ Category:{" "}
+ {product.category}
+
+ )}
+
+ {/* Stock status */}
+ {product.inStock === false ? (
+
Out of stock
+ ) : (
+
In stock β ready to ship
+ )}
+
+ {/* Quantity */}
+
+
+
+
+
+ {qty}
+
+
+
+
+
+ {/* Actions */}
+
+
+
+
+
+
+ );
+}
+
+// ββ Image gallery carousel βββββββββββββββββββββββββββββββββββββββββββββ
+function ImageGallery({ images }: { images: StorefrontImage[] }) {
+ const [activeIdx, setActiveIdx] = useState(0);
+
+ const prev = () => setActiveIdx((i) => (i - 1 + images.length) % images.length);
+ const next = () => setActiveIdx((i) => (i + 1) % images.length);
+
+ return (
+
+ {/* Main image */}
+
+
+ {images.length > 1 && (
+ <>
+
+
+ >
+ )}
+
+
+ {/* Thumbnails */}
+ {images.length > 1 && (
+
+ )}
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// SplitWithImageOverview
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function SplitWithImageOverview({
+ product,
+ images,
+ onAddToCart,
+ onWishlist,
+}: ProductOverviewBlockProps) {
+ const allImages = images?.length ? images : [product.image];
+
+ return (
+
+
+ {/* Image gallery (left) */}
+
+
+ {/* Product details (right) */}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// WithImageGridOverview
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function WithImageGridOverview({
+ product,
+ images,
+ onAddToCart,
+ onWishlist,
+}: ProductOverviewBlockProps) {
+ const allImages = images?.length ? images : [product.image];
+ const [main, ...extras] = allImages;
+
+ return (
+
+
+ {/* Image mosaic */}
+
+
+
+
+ {extras.slice(0, 4).map((img, idx) => (
+
+
+
+ ))}
+
+
+ {/* Details below */}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// WithTabsOverview
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function WithTabsOverview({
+ product,
+ images,
+ onAddToCart,
+ onWishlist,
+ specs,
+ shippingInfo,
+}: ProductOverviewBlockProps) {
+ const allImages = images?.length ? images : [product.image];
+
+ return (
+
+
+
+
+
+
+
+ {/* Extra info tabs */}
+
+
+ Description
+ {specs && Specs}
+ {shippingInfo && Shipping}
+
+
+
+ {product.description ?? "No description available."}
+
+
+ {specs && (
+
+
+ {specs.map((s) => (
+
+
- {s.label}
+ - {s.value}
+
+ ))}
+
+
+ )}
+ {shippingInfo && (
+
+ {shippingInfo}
+
+ )}
+
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// ProductOverviewBlock β public API
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface ProductOverviewBlockProps {
+ product: BlockProduct;
+ images?: StorefrontImage[];
+ variant?: "split-with-image" | "with-image-grid" | "with-tabs";
+ onAddToCart?: (productId: string, quantity: number) => void;
+ onWishlist?: (productId: string) => void;
+ specs?: Array<{ label: string; value: string }>;
+ shippingInfo?: string;
+}
+
+/**
+ * ProductOverviewBlock
+ *
+ * Full product detail page section with image gallery and purchase form.
+ * @example
+ * addToCart(id, qty)}
+ * />
+ */
+export function ProductOverviewBlock(props: ProductOverviewBlockProps) {
+ const { variant = "split-with-image" } = props;
+
+ switch (variant) {
+ case "with-image-grid":
+ return ;
+ case "with-tabs":
+ return ;
+ case "split-with-image":
+ default:
+ return ;
+ }
+}
diff --git a/src/components/storefront/blocks/product/product-quickview-block.tsx b/src/components/storefront/blocks/product/product-quickview-block.tsx
new file mode 100644
index 00000000..fe8d37d0
--- /dev/null
+++ b/src/components/storefront/blocks/product/product-quickview-block.tsx
@@ -0,0 +1,233 @@
+"use client";
+/**
+ * Product Quickview Block
+ *
+ * Ported from tailwind-css-prebuild-ui-themes ecommerce/product-quickviews.
+ * Opens the product details in a modal (shadcn Dialog) without navigating
+ * away from the current page.
+ *
+ * All colours use CSS theme variables.
+ */
+
+import { useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { Star, X, Heart, Minus, Plus } from "lucide-react";
+import { cn } from "@/lib/utils";
+import {
+ Dialog,
+ DialogContent,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import type { BlockProduct } from "../types";
+
+// ββ Star row (tiny) βββββββββββββββββββββββββββββββββββββββββββββββββββ
+function MiniStars({ value, count }: { value: number; count: number }) {
+ return (
+
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+
+
+ {count.toLocaleString()} reviews
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// ProductQuickviewBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface ProductQuickviewBlockProps {
+ product: BlockProduct;
+ open: boolean;
+ onClose: () => void;
+ onAddToCart?: (productId: string, quantity: number) => void;
+}
+
+/**
+ * ProductQuickviewBlock
+ *
+ * Modal quickview dialog for a product. Designed to be triggered from
+ * product card "Quick View" buttons.
+ *
+ * @example
+ * setQuickviewOpen(false)}
+ * onAddToCart={(id, qty) => addToCart(id, qty)}
+ * />
+ */
+export function ProductQuickviewBlock({
+ product,
+ open,
+ onClose,
+ onAddToCart,
+}: ProductQuickviewBlockProps) {
+ const [qty, setQty] = useState(1);
+ const [wishlisted, setWishlisted] = useState(false);
+
+ return (
+
+ );
+}
diff --git a/src/components/storefront/blocks/promo/promo-blocks.tsx b/src/components/storefront/blocks/promo/promo-blocks.tsx
new file mode 100644
index 00000000..079f1c96
--- /dev/null
+++ b/src/components/storefront/blocks/promo/promo-blocks.tsx
@@ -0,0 +1,332 @@
+"use client";
+/**
+ * Promo & Incentives Blocks
+ *
+ * Ported from tailwind-css-prebuild-ui-themes:
+ * - ecommerce/incentives (trust/incentive strip + cards)
+ * - ecommerce/promo-sections
+ *
+ * All colours use CSS theme variables.
+ */
+
+import Image from "next/image";
+import Link from "next/link";
+import { Truck, RefreshCw, Gift, Lock, Headphones, Heart } from "lucide-react";
+import { ArrowRight } from "lucide-react";
+import { cn } from "@/lib/utils";
+import type { IncentivesProps, Incentive, PromoSectionProps } from "../types";
+
+// ββ Icon mapper ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+const INCENTIVE_ICONS = {
+ shipping: Truck,
+ exchange: RefreshCw,
+ gift: Gift,
+ lock: Lock,
+ support: Headphones,
+ heart: Heart,
+} as const;
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Incentives: Strip Variant
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function IncentiveStrip({ incentives, headline }: IncentivesProps) {
+ return (
+
+
+ {headline && (
+
{headline}
+ )}
+
= 4 && "sm:grid-cols-2 lg:grid-cols-4",
+ )}
+ >
+ {incentives.map((incentive, idx) => (
+
+ ))}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// Incentives: Cards Variant
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function IncentiveCards({ incentives, headline }: IncentivesProps) {
+ return (
+
+
+ {headline && (
+
+ {headline}
+
+ )}
+
= 4 && "sm:grid-cols-2 lg:grid-cols-4",
+ )}
+ >
+ {incentives.map((incentive, idx) => (
+
+ ))}
+
+
+
+ );
+}
+
+// ββ Shared incentive item ββββββββββββββββββββββββββββββββββββββββββββββ
+function IncentiveItem({
+ incentive,
+ variant,
+}: {
+ incentive: Incentive;
+ variant: "strip" | "cards";
+}) {
+ const Icon = INCENTIVE_ICONS[incentive.icon] ?? Truck;
+
+ if (variant === "strip") {
+ return (
+
+
+
+
+
+
{incentive.name}
+ {incentive.description && (
+
{incentive.description}
+ )}
+
+
+ );
+ }
+
+ // cards variant
+ return (
+
+
+
+
+
+
{incentive.name}
+ {incentive.description && (
+
+ {incentive.description}
+
+ )}
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// IncentivesBlock β public API
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * IncentivesBlock
+ *
+ * Trust / incentive strip or card grid.
+ *
+ * @example
+ *
+ */
+export function IncentivesBlock({ variant = "strip", ...props }: IncentivesProps) {
+ return variant === "cards" ? (
+
+ ) : (
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// PromoSectionBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * PromoSectionBlock
+ *
+ * Full-width promotional section with heading, description and CTAs.
+ * Three layout variants: 'centered', 'with-image', 'split-image'.
+ */
+export function PromoSectionBlock({
+ heading,
+ description,
+ primaryCTA,
+ secondaryCTA,
+ backgroundImage,
+ overlayOpacity = 60,
+ variant = "centered",
+}: PromoSectionProps) {
+ if (variant === "centered") {
+ return (
+
+
+
+
+ {heading}
+
+
+ {description}
+
+ {(primaryCTA || secondaryCTA) && (
+
+ {primaryCTA && (
+
+ {primaryCTA.text}
+
+ )}
+ {secondaryCTA && (
+
+ {secondaryCTA.text}
+
+
+ )}
+
+ )}
+
+
+
+ );
+ }
+
+ if (variant === "with-image") {
+ return (
+
+ {backgroundImage ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {heading}
+
+
{description}
+ {(primaryCTA || secondaryCTA) && (
+
+ {primaryCTA && (
+
+ {primaryCTA.text}
+
+ )}
+ {secondaryCTA && (
+
+ {secondaryCTA.text}
+
+
+ )}
+
+ )}
+
+
+
+ );
+ }
+
+ // split-image variant
+ return (
+
+
+
+
+
+
+
+
+ {heading}
+
+
{description}
+
+ {primaryCTA && (
+
+ {primaryCTA.text}
+
+ )}
+ {secondaryCTA && (
+
+ {secondaryCTA.text}
+
+
+ )}
+
+
+
+
+
+
+
+ {backgroundImage ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+export type { IncentivesProps, Incentive, PromoSectionProps };
diff --git a/src/components/storefront/blocks/reviews/reviews-section-block.tsx b/src/components/storefront/blocks/reviews/reviews-section-block.tsx
new file mode 100644
index 00000000..647fb911
--- /dev/null
+++ b/src/components/storefront/blocks/reviews/reviews-section-block.tsx
@@ -0,0 +1,280 @@
+"use client";
+/**
+ * Reviews Section Block
+ *
+ * Ported from tailwind-css-prebuild-ui-themes ecommerce/reviews.
+ * Three variants:
+ * - simple : list with avatars and star ratings
+ * - grid : multi-column card grid
+ * - with-summary : aggregate score + breakdown on the left
+ *
+ * All colours use CSS theme variables.
+ */
+
+import { Star } from "lucide-react";
+import { cn } from "@/lib/utils";
+import type { ReviewsSectionProps, Review, ReviewsSummary } from "../types";
+
+// ββ Star row ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function StarRow({
+ rating,
+ size = "sm",
+}: {
+ rating: number;
+ size?: "xs" | "sm" | "md";
+}) {
+ const cls = {
+ xs: "size-3",
+ sm: "size-4",
+ md: "size-5",
+ }[size];
+
+ return (
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+
+ );
+}
+
+// ββ Avatar placeholder ββββββββββββββββββββββββββββββββββββββββββββββββ
+function Avatar({ name }: { name: string }) {
+ const initials = name
+ .split(" ")
+ .map((n) => n[0])
+ .slice(0, 2)
+ .join("")
+ .toUpperCase();
+
+ return (
+
+ {initials}
+
+ );
+}
+
+// ββ Breakdown bar (for with-summary variant) ββββββββββββββββββββββββββ
+function BreakdownBar({
+ label,
+ count,
+ total,
+}: {
+ label: string;
+ count: number;
+ total: number;
+}) {
+ const pct = total > 0 ? Math.round((count / total) * 100) : 0;
+ return (
+
+ );
+}
+
+// ββ Rating summary ββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function RatingSummary({ summary }: { summary: ReviewsSummary }) {
+ const labels = ["5 stars", "4 stars", "3 stars", "2 stars", "1 star"];
+ return (
+
+
+
+
+ {summary.average.toFixed(1)}
+
+
out of 5
+
+
+
+
+ {summary.total.toLocaleString()} reviews
+
+
+
+
+ {summary.breakdown.map((count, idx) => (
+
+ ))}
+
+
+ );
+}
+
+// ββ Review card βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+function ReviewCard({
+ review,
+ variant,
+}: {
+ review: Review;
+ variant: "simple" | "grid";
+}) {
+ if (variant === "simple") {
+ return (
+
+ {review.avatar ? (
+

+ ) : (
+
+ )}
+
+
+
{review.author}
+
+
+
+ {review.title && (
+
{review.title}
+ )}
+
+ {review.content}
+
+ {review.verified && (
+
β Verified Purchase
+ )}
+
+
+ );
+ }
+
+ // grid card
+ return (
+
+
+
+
+
+ {review.title && (
+ {review.title}
+ )}
+
+ {review.content}
+
+
+ {review.avatar ? (
+

+ ) : (
+
+ {review.author.charAt(0).toUpperCase()}
+
+ )}
+
+
{review.author}
+ {review.verified && (
+
Verified Purchase
+ )}
+
+
+
+ );
+}
+
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+// ReviewsSectionBlock
+// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * ReviewsSectionBlock
+ *
+ * Product / store reviews display with multiple layout variants.
+ * @example
+ *
+ */
+export function ReviewsSectionBlock({
+ reviews,
+ summary,
+ heading,
+ variant = "simple",
+}: ReviewsSectionProps) {
+ return (
+
+
+ {heading && (
+
+ {heading}
+
+ )}
+
+ {variant === "with-summary" && summary ? (
+
+ {/* Summary sidebar */}
+
+
+
+ {/* Review list */}
+
+
+ {reviews.map((review) => (
+
+ ))}
+
+
+
+ ) : variant === "grid" ? (
+
+ {reviews.map((review) => (
+
+ ))}
+
+ ) : (
+ /* simple list */
+
+ {reviews.map((review) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+export type { ReviewsSectionProps, Review, ReviewsSummary };
diff --git a/src/components/storefront/blocks/types.ts b/src/components/storefront/blocks/types.ts
new file mode 100644
index 00000000..ad2140a9
--- /dev/null
+++ b/src/components/storefront/blocks/types.ts
@@ -0,0 +1,368 @@
+/**
+ * Storefront UI Blocks β Shared Types
+ *
+ * All ported blocks from the tailwind-css-prebuild-ui-themes library.
+ * Blocks are theme-aware: they use CSS variable-based Tailwind classes
+ * (bg-primary, text-primary, etc.) instead of hard-coded palette utilities.
+ *
+ * Design principles:
+ * β CSS variable-driven (bg-primary, text-muted-foreground, border-border, etc.)
+ * β Accessible (WCAG 2.2 AA): descriptive alt text, aria-* attributes, focus rings
+ * β Responsive (mobile-first)
+ * β RTL-ready (avoid left/right-specific spacings where possible)
+ * β Next.js 16 App Router compatible (Server + Client components as appropriate)
+ * β React 19 patterns (no forwardRef, use() hook for context)
+ */
+
+// ββ Common prop shapes βββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface StorefrontImage {
+ src: string;
+ alt: string;
+ width?: number;
+ height?: number;
+}
+
+export interface StorefrontCTA {
+ text: string;
+ href: string;
+ /** External link β opens in new tab with rel="noopener noreferrer" */
+ external?: boolean;
+}
+
+export interface StorefrontRating {
+ /** Out of 5 */
+ value: number;
+ count: number;
+}
+
+// ββ Product ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface BlockProduct {
+ id: string;
+ name: string;
+ href: string;
+ price: string;
+ compareAtPrice?: string;
+ /** Original / sale percentage: e.g. "-20%" */
+ badge?: string;
+ description?: string;
+ options?: string;
+ rating?: StorefrontRating;
+ image: StorefrontImage;
+ /** Additional images for gallery view */
+ images?: StorefrontImage[];
+ category?: string;
+ isNew?: boolean;
+ isBestSeller?: boolean;
+ isSale?: boolean;
+ inStock?: boolean;
+}
+
+// ββ Category βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface BlockCategory {
+ id: string;
+ name: string;
+ href: string;
+ description?: string;
+ productCount?: number;
+ image?: StorefrontImage;
+}
+
+// ββ Navigation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface NavSection {
+ id: string;
+ name: string;
+ items: Array<{ name: string; href: string }>;
+}
+
+export interface NavCategory {
+ id: string;
+ name: string;
+ href?: string;
+ featured?: Array<{
+ name: string;
+ href: string;
+ imageSrc: string;
+ imageAlt: string;
+ }>;
+ sections?: NavSection[][];
+}
+
+export interface NavPage {
+ name: string;
+ href: string;
+}
+
+export interface StoreNavProps {
+ storeName: string;
+ storeLogo?: string | null;
+ homeHref: string;
+ categories?: NavCategory[];
+ pages?: NavPage[];
+ cartCount?: number;
+ onCartOpen?: () => void;
+ onSearchOpen?: () => void;
+ announcement?: string | null;
+ announcementHref?: string | null;
+}
+
+// ββ Hero βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export type HeroVariant = 'centered' | 'split-left' | 'split-right' | 'image-tiles' | 'angled' | 'full-bleed';
+
+export interface HeroBlockProps {
+ variant?: HeroVariant;
+ heading: string;
+ subheading?: string;
+ announcement?: string;
+ announcementHref?: string;
+ primaryCTA?: StorefrontCTA;
+ secondaryCTA?: StorefrontCTA;
+ image?: StorefrontImage;
+ /** Array of images for the 'image-tiles' variant */
+ imageTiles?: StorefrontImage[];
+ /** Background colour token. E.g. "bg-primary" or "bg-muted". Defaults to "bg-background" */
+ backgroundClass?: string;
+ logoSrc?: string;
+}
+
+// ββ Category previews ββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export type CategoryPreviewVariant = 'three-column' | 'image-backgrounds' | 'split-images' | 'scrolling-cards';
+
+export interface CategoryPreviewProps {
+ variant?: CategoryPreviewVariant;
+ categories: BlockCategory[];
+ heading?: string;
+ cta?: StorefrontCTA;
+}
+
+// ββ Product list / grid ββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export type ProductCardVariant =
+ | 'standard' // Image + title + price below
+ | 'detailed' // Full card with description, options, price
+ | 'overlay' // Image with dark overlay on hover + add-to-cart
+ | 'minimal' // Just image + subtle price below
+ | 'horizontal' // Thumbnail left, details right (list view)
+ | 'tall-image' // Tall/portrait image, name + price below;
+
+export interface ProductCardProps extends BlockProduct {
+ variant?: ProductCardVariant;
+ onAddToCart?: (productId: string) => void;
+ /** Show quick-view button on hover */
+ showQuickView?: boolean;
+ onQuickView?: (productId: string) => void;
+ columns?: 2 | 3 | 4;
+}
+
+export interface ProductGridProps {
+ products: BlockProduct[];
+ variant?: ProductCardVariant;
+ columns?: 2 | 3 | 4;
+ onAddToCart?: (productId: string) => void;
+ onQuickView?: (productId: string) => void;
+ /** Section heading above the grid */
+ heading?: string;
+ cta?: StorefrontCTA;
+}
+
+// ββ Category filters βββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface FilterOption {
+ value: string;
+ label: string;
+ count?: number;
+ checked?: boolean;
+}
+
+export interface FilterGroup {
+ id: string;
+ name: string;
+ type: 'checkbox' | 'radio' | 'range' | 'color';
+ options: FilterOption[];
+ min?: number;
+ max?: number;
+ currentMin?: number;
+ currentMax?: number;
+}
+
+export interface ActiveFilter {
+ id: string;
+ label: string;
+ value: string;
+}
+
+export interface CategoryFiltersProps {
+ filters: FilterGroup[];
+ activeFilters?: ActiveFilter[];
+ resultCount?: number;
+ sortOptions?: Array<{ value: string; label: string }>;
+ currentSort?: string;
+ onFilterChange?: (groupId: string, value: string, checked: boolean) => void;
+ onClearFilter?: (filter: ActiveFilter) => void;
+ onClearAll?: () => void;
+ onSortChange?: (value: string) => void;
+}
+
+// ββ Shopping cart ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface CartItem {
+ id: string;
+ name: string;
+ href: string;
+ color?: string;
+ size?: string;
+ price: string;
+ quantity: number;
+ imageSrc: string;
+ imageAlt: string;
+ inStock?: boolean;
+}
+
+export interface CartBlockProps {
+ items: CartItem[];
+ subtotal: string;
+ shippingNote?: string;
+ checkoutHref: string;
+ continueShoppingHref: string;
+ onUpdateQuantity?: (itemId: string, quantity: number) => void;
+ onRemoveItem?: (itemId: string) => void;
+ /** For slide-over variant */
+ open?: boolean;
+ onClose?: () => void;
+}
+
+// ββ Promo / incentives βββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface Incentive {
+ name: string;
+ description: string;
+ icon: 'shipping' | 'exchange' | 'gift' | 'lock' | 'support' | 'heart';
+}
+
+export interface IncentivesProps {
+ headline?: string;
+ incentives: Incentive[];
+ /** 'strip' = horizontal strip; 'cards' = card grid */
+ variant?: 'strip' | 'cards';
+}
+
+export interface PromoSectionProps {
+ heading: string;
+ description: string;
+ primaryCTA?: StorefrontCTA;
+ secondaryCTA?: StorefrontCTA;
+ backgroundImage?: string;
+ /** Overlay opacity 0-100 */
+ overlayOpacity?: number;
+ variant?: 'with-image' | 'split-image' | 'centered';
+}
+
+// ββ Reviews ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface Review {
+ id: string;
+ author: string;
+ date: string;
+ rating: number;
+ title?: string;
+ content: string;
+ avatar?: string;
+ verified?: boolean;
+}
+
+export interface ReviewsSummary {
+ average: number;
+ total: number;
+ /** Count per star level, index 0 = 5 stars, index 4 = 1 star */
+ breakdown: number[];
+}
+
+export interface ReviewsSectionProps {
+ reviews: Review[];
+ summary?: ReviewsSummary;
+ heading?: string;
+ variant?: 'simple' | 'grid' | 'with-summary';
+}
+
+// ββ Footer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface FooterColumn {
+ title: string;
+ links: Array<{ name: string; href: string }>;
+}
+
+export interface SocialLink {
+ platform: 'facebook' | 'instagram' | 'twitter' | 'youtube' | 'tiktok' | 'pinterest';
+ href: string;
+}
+
+export interface FooterBlockProps {
+ storeName: string;
+ tagline?: string;
+ logoSrc?: string;
+ columns?: FooterColumn[];
+ socialLinks?: SocialLink[];
+ copyrightText?: string;
+ newsletterHeading?: string;
+ newsletterSubheading?: string;
+ newsletterFormAction?: string;
+ variant?: 'four-column' | 'simple-centered' | 'with-newsletter' | 'dark';
+}
+
+// ββ Marketing sections βββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface Feature {
+ name: string;
+ description: string;
+ icon: string; // lucide icon name
+}
+
+export interface FeaturesBlockProps {
+ heading: string;
+ subheading?: string;
+ features: Feature[];
+ variant?: 'grid' | 'list' | 'offset';
+ image?: StorefrontImage;
+}
+
+export interface Testimonial {
+ id: string;
+ content: string;
+ author: string;
+ role?: string;
+ company?: string;
+ avatar?: string;
+ rating?: number;
+}
+
+export interface TestimonialsBlockProps {
+ heading?: string;
+ subheading?: string;
+ testimonials: Testimonial[];
+ variant?: 'grid' | 'carousel' | 'with-image';
+}
+
+export interface CTASectionProps {
+ heading: string;
+ subheading?: string;
+ primaryCTA?: StorefrontCTA;
+ secondaryCTA?: StorefrontCTA;
+ backgroundClass?: string;
+ variant?: 'simple-centered' | 'split-image' | 'with-image-tiles' | 'banner';
+ image?: StorefrontImage;
+}
+
+export interface NewsletterBlockProps {
+ heading: string;
+ subheading?: string;
+ placeholderText?: string;
+ buttonText?: string;
+ formAction?: string;
+ variant?: 'centered' | 'split' | 'inline';
+ privacyText?: string;
+}
diff --git a/src/components/storefront/content-section.tsx b/src/components/storefront/content-section.tsx
new file mode 100644
index 00000000..4d79f2f2
--- /dev/null
+++ b/src/components/storefront/content-section.tsx
@@ -0,0 +1,363 @@
+/**
+ * Content Section Renderer
+ *
+ * Renders a flexible content section composed of block primitives
+ * (text, image, video, CTA, spacer, divider, feature-grid, icon-list).
+ * Server Component β no client interactivity needed for static rendering.
+ */
+
+import Image from "next/image";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import type {
+ ContentSection as ContentSectionConfig,
+ Block,
+ TextBlock,
+ ImageBlock,
+ ImageWithTextBlock,
+ VideoBlock,
+ CTAButtonBlock,
+ SpacerBlock,
+ DividerBlock,
+ FeatureGridBlock,
+ IconListBlock,
+ TrustBadgeIcon,
+} from "@/lib/storefront/types";
+import {
+ Truck,
+ Shield,
+ Star,
+ Clock,
+ Heart,
+ Check,
+ RefreshCw,
+ Award,
+} from "lucide-react";
+
+// ββ Icon mapping (shared with trust-badges) ββββββββββββββββββββββββββββ
+
+const ICON_MAP: Record = {
+ truck: Truck,
+ shield: Shield,
+ star: Star,
+ clock: Clock,
+ heart: Heart,
+ check: Check,
+ refresh: RefreshCw,
+ award: Award,
+};
+
+// ββ Padding / max-width utility maps βββββββββββββββββββββββββββββββββββ
+
+const PADDING_MAP: Record, string> = {
+ none: "py-0",
+ sm: "py-6",
+ md: "py-12",
+ lg: "py-20",
+};
+
+const MAX_WIDTH_MAP: Record, string> = {
+ full: "max-w-full",
+ xl: "max-w-xl",
+ "2xl": "max-w-2xl",
+ "4xl": "max-w-4xl",
+ "6xl": "max-w-6xl",
+};
+
+// ββ Block renderers ββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function TextBlockRenderer({ block }: { block: TextBlock }) {
+ const sizeMap: Record = {
+ sm: "text-sm",
+ base: "text-base",
+ lg: "text-lg",
+ xl: "text-xl",
+ };
+
+ const alignMap: Record = {
+ left: "text-left",
+ center: "text-center",
+ right: "text-right",
+ };
+
+ return (
+
+ );
+}
+
+function ImageBlockRenderer({ block }: { block: ImageBlock }) {
+ const aspectMap: Record = {
+ "16:9": "aspect-video",
+ "4:3": "aspect-4/3",
+ "1:1": "aspect-square",
+ auto: "",
+ };
+
+ const image = (
+
+
+
+
+ {block.caption && (
+
+ {block.caption}
+
+ )}
+
+ );
+
+ if (block.link) {
+ return (
+
+ {image}
+
+ );
+ }
+
+ return image;
+}
+
+function ImageWithTextBlockRenderer({ block }: { block: ImageWithTextBlock }) {
+ return (
+ :first-child]:order-2",
+ )}
+ >
+
+
+
+
+
{block.title}
+
+ {block.ctaText && block.ctaLink && (
+
+ )}
+
+
+ );
+}
+
+function VideoBlockRenderer({ block }: { block: VideoBlock }) {
+ const aspectMap: Record = {
+ "16:9": "aspect-video",
+ "4:3": "aspect-4/3",
+ "1:1": "aspect-square",
+ };
+
+ // Convert YouTube / Vimeo watch URLs to embed URLs
+ const getEmbedUrl = (url: string): string => {
+ const ytMatch = url.match(
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/)([A-Za-z0-9_-]+)/,
+ );
+ if (ytMatch) return `https://www.youtube-nocookie.com/embed/${ytMatch[1]}`;
+
+ const vimeoMatch = url.match(/vimeo\.com\/(\d+)/);
+ if (vimeoMatch) return `https://player.vimeo.com/video/${vimeoMatch[1]}`;
+
+ return url;
+ };
+
+ return (
+
+
+
+
+ {block.title && (
+
{block.title}
+ )}
+
+ );
+}
+
+function CTAButtonBlockRenderer({ block }: { block: CTAButtonBlock }) {
+ const alignMap: Record = {
+ left: "justify-start",
+ center: "justify-center",
+ right: "justify-end",
+ };
+
+ const sizeMap: Record = {
+ sm: "sm",
+ md: "default",
+ lg: "lg",
+ };
+
+ const variantMap: Record = {
+ primary: "default",
+ secondary: "secondary",
+ outline: "outline",
+ };
+
+ return (
+
+
+
+ );
+}
+
+function SpacerBlockRenderer({ block }: { block: SpacerBlock }) {
+ return ;
+}
+
+function DividerBlockRenderer({ block }: { block: DividerBlock }) {
+ return (
+
+ );
+}
+
+function FeatureGridBlockRenderer({ block }: { block: FeatureGridBlock }) {
+ const colMap: Record = {
+ 2: "grid-cols-1 sm:grid-cols-2",
+ 3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
+ 4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
+ };
+
+ return (
+
+ {block.features.map((feature) => {
+ const Icon = ICON_MAP[feature.icon] ?? Check;
+ return (
+
+
+
+
+
{feature.title}
+
{feature.description}
+
+ );
+ })}
+
+ );
+}
+
+function IconListBlockRenderer({ block }: { block: IconListBlock }) {
+ return (
+
+ {block.items.map((item) => {
+ const Icon = ICON_MAP[item.icon] ?? Check;
+ return (
+ -
+
+ {item.text}
+
+ );
+ })}
+
+ );
+}
+
+// ββ Block dispatcher βββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function BlockRenderer({ block }: { block: Block }) {
+ if (!block.enabled) return null;
+
+ switch (block.type) {
+ case "text":
+ return ;
+ case "image":
+ return ;
+ case "image-with-text":
+ return ;
+ case "video":
+ return ;
+ case "cta-button":
+ return ;
+ case "spacer":
+ return ;
+ case "divider":
+ return ;
+ case "feature-grid":
+ return ;
+ case "icon-list":
+ return ;
+ default:
+ return null;
+ }
+}
+
+// ββ Main section component βββββββββββββββββββββββββββββββββββββββββββββ
+
+interface ContentSectionProps {
+ config: ContentSectionConfig;
+}
+
+export function ContentSectionRenderer({ config }: ContentSectionProps) {
+ if (!config.enabled) return null;
+
+ const sortedBlocks = [...config.blocks].sort((a, b) => a.order - b.order);
+ const padding = PADDING_MAP[config.padding ?? "md"];
+ const maxWidth = MAX_WIDTH_MAP[config.maxWidth ?? "6xl"];
+
+ return (
+
+
+ {/* Section heading */}
+ {(config.title || config.subtitle) && (
+
+ {config.title && (
+
{config.title}
+ )}
+ {config.subtitle && (
+
{config.subtitle}
+ )}
+
+ )}
+
+ {/* Block list */}
+ {sortedBlocks.map((block) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/storefront/enhanced-categories-section.tsx b/src/components/storefront/enhanced-categories-section.tsx
new file mode 100644
index 00000000..b0b0fa21
--- /dev/null
+++ b/src/components/storefront/enhanced-categories-section.tsx
@@ -0,0 +1,141 @@
+"use client";
+
+import Image from "next/image";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { ArrowRight } from "lucide-react";
+import {
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+} from "@/components/ui/carousel";
+import { cn } from "@/lib/utils";
+import type { CategoriesSection } from "@/lib/storefront/types";
+import { useStoreUrl } from "@/components/storefront/store-url-provider";
+
+// βββ Types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+export interface CategoryItem {
+ id: string;
+ name: string;
+ slug: string;
+ image?: string | null;
+ _count: { products: number };
+}
+
+interface CategoriesSectionProps {
+ config: CategoriesSection;
+ categories: CategoryItem[];
+ className?: string;
+}
+
+// βββ Category Card (shared) ββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function CategoryCard({ category }: { category: CategoryItem }) {
+ const { storeUrl } = useStoreUrl();
+ return (
+
+
+ {category.image ? (
+
+ ) : (
+
+ π¦
+
+ )}
+
+
+
+
+
+
+ {category.name}
+
+
+ {category._count.products}{" "}
+ {category._count.products === 1 ? "Item" : "Items"}
+
+
+
+ {category._count.products > 0 && (
+
+ {category._count.products}
+
+ )}
+
+ );
+}
+
+// βββ Main export βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+/**
+ * Categories section with:
+ * - `grid` mode (default): responsive CSS grid
+ * - `carousel` mode: horizontal swipeable Embla carousel
+ */
+export function EnhancedCategoriesSection({ config, categories, className }: CategoriesSectionProps) {
+ const { storeUrl } = useStoreUrl();
+ if (!config.enabled || categories.length === 0) return null;
+
+ const displayed = categories.slice(0, config.maxCategories);
+ const isCarousel = config.displayStyle === "carousel";
+
+ return (
+
+ {/* Header */}
+
+
+
{config.title}
+ {config.subtitle &&
{config.subtitle}
}
+
+ {config.showViewAll && (
+
+ )}
+
+
+ {/* Content */}
+ {isCarousel ? (
+ 4 }}
+ className="w-full"
+ >
+
+ {displayed.map((cat) => (
+
+
+
+ ))}
+
+
+
+
+ ) : (
+
+ {displayed.map((cat) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/storefront/newsletter-section.tsx b/src/components/storefront/newsletter-section.tsx
index f60fc1d0..f9134b58 100644
--- a/src/components/storefront/newsletter-section.tsx
+++ b/src/components/storefront/newsletter-section.tsx
@@ -39,17 +39,26 @@ export function NewsletterSection({ config, storeSlug }: NewsletterSectionProps)
setStatus("loading");
setError("");
- // TODO: Integrate with newsletter API (e.g., Mailchimp, ConvertKit)
- // For now, simulate successful subscription
try {
- // In production, call: POST /api/stores/${storeSlug}/newsletter
- await new Promise((resolve) => setTimeout(resolve, 1000));
- console.log(`Newsletter subscription for store: ${storeSlug}, email: ${email}`);
+ const res = await fetch(`/api/storefront/${storeSlug}/newsletter`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, source: "storefront_footer" }),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ throw new Error(data.error ?? "Subscription failed");
+ }
+
setStatus("success");
setEmail("");
- } catch {
+ } catch (err) {
setStatus("error");
- setError("Something went wrong. Please try again.");
+ setError(
+ err instanceof Error ? err.message : "Something went wrong. Please try again.",
+ );
}
};
@@ -58,7 +67,7 @@ export function NewsletterSection({ config, storeSlug }: NewsletterSectionProps)
data-section-id="newsletter"
className={cn(
"py-16 lg:py-24",
- config.backgroundColor ? "" : "bg-gradient-to-br from-primary/5 via-primary/10 to-secondary/5"
+ config.backgroundColor ? "" : "bg-linear-to-br from-primary/5 via-primary/10 to-secondary/5"
)}
style={config.backgroundColor ? { backgroundColor: config.backgroundColor } : undefined}
>
diff --git a/src/components/storefront/order-view-client.tsx b/src/components/storefront/order-view-client.tsx
index be2a4657..83d0e067 100644
--- a/src/components/storefront/order-view-client.tsx
+++ b/src/components/storefront/order-view-client.tsx
@@ -100,7 +100,9 @@ export function OrderViewClient({ storeSlug: _storeSlug }: OrderViewClientProps)
setIsDownloading(true);
try {
- const response = await fetch(storeApiUrl(`/orders/${order.id}/invoice`));
+ const response = await fetch(storeApiUrl(`/orders/${order.id}/invoice`), {
+ credentials: 'include',
+ });
if (!response.ok) {
throw new Error('Failed to generate invoice');
diff --git a/src/components/storefront/preview-bridge.tsx b/src/components/storefront/preview-bridge.tsx
index daa7fd0c..8da4a165 100644
--- a/src/components/storefront/preview-bridge.tsx
+++ b/src/components/storefront/preview-bridge.tsx
@@ -16,7 +16,8 @@
*/
import { useEffect, useState, useCallback } from 'react';
-import type { StorefrontConfig } from '@/lib/storefront/types';
+import type { StorefrontConfig, ThemeColors } from '@/lib/storefront/types';
+import { FONT_MAP, RADIUS_MAP, getContrastForeground } from '@/lib/storefront/theme-constants';
export function PreviewBridge() {
const [inspectorMode, setInspectorMode] = useState(false);
@@ -128,23 +129,13 @@ export function PreviewBridge() {
const THEME_STYLE_ID = 'stormcom-theme-overrides';
/**
- * Derive a high-contrast foreground color from a hex background.
- * Uses relative luminance to pick white or near-black.
+ * Build CSS variable declarations for a color palette.
+ * Returns an array of `--var: value;` strings.
*/
-function getContrastForeground(hex: string): string {
- const r = parseInt(hex.slice(1, 3), 16);
- const g = parseInt(hex.slice(3, 5), 16);
- const b = parseInt(hex.slice(5, 7), 16);
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
- return luminance > 0.5 ? '#020617' : '#fafafa';
-}
-
-function applyThemeVariables(theme: StorefrontConfig['theme']) {
- const root = document.documentElement;
- const { colors } = theme;
+function buildColorCSSVars(colors: ThemeColors): string[] {
+ const vars: string[] = [];
- // ββ 1. Set base + mapped color variables via inline style βββββββββββ
- const colorEntries: Array<{ key: keyof typeof colors; cssName: string }> = [
+ const colorEntries: Array<{ key: keyof ThemeColors; cssName: string }> = [
{ key: 'primary', cssName: 'primary' },
{ key: 'secondary', cssName: 'secondary' },
{ key: 'accent', cssName: 'accent' },
@@ -153,26 +144,15 @@ function applyThemeVariables(theme: StorefrontConfig['theme']) {
{ key: 'muted', cssName: 'muted' },
];
- const cssOverrides: string[] = [];
-
for (const { key, cssName } of colorEntries) {
const value = colors[key];
if (!value) continue;
-
- // Set on documentElement for immediate cascade
- root.style.setProperty(`--${cssName}`, value);
- root.style.setProperty(`--color-${cssName}`, value);
-
- // Collect for
+ )}
+ >
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Apply theme CSS variables to the document root
+// ---------------------------------------------------------------------------
+// Tailwind CSS v4 `@theme inline` maps `--color-primary: var(--primary)`.
+// We set BOTH the base variable (`--primary`) AND the Tailwind-mapped
+// variable (`--color-primary`) so the cascade works regardless of
+// how Tailwind resolves `bg-primary` / `text-primary` utilities.
+// A `