Compare commits

...

2 Commits

Author SHA1 Message Date
Flatlogic Bot
dae559a42b S.1.2.2 2026-06-11 10:26:20 +00:00
Flatlogic Bot
205eee3c6c S.1.1.1 2026-06-11 08:48:10 +00:00
8 changed files with 1328 additions and 146 deletions

View File

@ -0,0 +1,185 @@
import React from 'react';
import Link from 'next/link';
import {
formatCurrency,
MadeDreamsCartItem,
MadeDreamsCartSummary,
} from '../helpers/madeDreamsCart';
type Props = {
open: boolean;
items: MadeDreamsCartItem[];
totals: MadeDreamsCartSummary;
onClose: () => void;
onQuantityChange: (itemId: string, quantity: number) => void;
onRemove: (itemId: string) => void;
onClear: () => void;
};
export default function MadeDreamsCartDrawer({
open,
items,
totals,
onClose,
onQuantityChange,
onRemove,
onClear,
}: Props) {
return (
<div className={`fixed inset-0 z-50 ${open ? '' : 'pointer-events-none'}`} aria-hidden={!open}>
<button
type="button"
aria-label="Close cart drawer"
className={`absolute inset-0 bg-[#130f0d]/60 transition-opacity duration-300 ${
open ? 'opacity-100' : 'opacity-0'
}`}
onClick={onClose}
/>
<aside
className={`absolute right-0 top-0 flex h-full w-full max-w-md transform flex-col bg-[#fffaf3] shadow-2xl transition-transform duration-300 ${
open ? 'translate-x-0' : 'translate-x-full'
}`}
role="dialog"
aria-modal="true"
aria-label="Shopping cart"
>
<div className="flex items-start justify-between border-b border-[#ead8c3] px-6 py-5">
<div>
<p className="text-xs font-bold uppercase tracking-[0.35em] text-[#a0672a]">Made Dreams</p>
<h2 className="mt-1 font-serif text-3xl text-[#201713]">Your cart</h2>
</div>
<button
type="button"
className="rounded-full border border-[#d8b88f] px-4 py-2 text-sm font-semibold text-[#4e3b31] transition hover:bg-[#f5eadc]"
onClick={onClose}
>
Close
</button>
</div>
<div className="flex-1 overflow-y-auto px-6 py-5">
{items.length === 0 ? (
<div className="rounded-[2rem] border border-dashed border-[#d8b88f] bg-white/70 p-8 text-center">
<p className="font-serif text-2xl text-[#201713]">Your cart is empty.</p>
<p className="mt-3 text-sm leading-6 text-[#6d5546]">
Add a best seller or create a custom paint by numbers kit to start your order.
</p>
<Link
href="/#products"
className="mt-6 inline-flex rounded-full bg-[#201713] px-5 py-3 text-sm font-bold uppercase tracking-[0.18em] text-white"
onClick={onClose}
>
Shop products
</Link>
</div>
) : (
<div className="space-y-4">
{items.map((item) => (
<article key={item.id} className="rounded-[1.6rem] border border-[#ead8c3] bg-white p-4 shadow-sm">
<div className="grid grid-cols-[5rem_1fr] gap-4">
<div className={`h-20 rounded-2xl bg-gradient-to-br ${item.imageClass} p-2`}>
<div className="grid h-full grid-cols-3 gap-1 opacity-70">
{Array.from({ length: 9 }).map((_, index) => (
<span key={index} className="rounded bg-white/20 text-center text-[9px] text-white">
{index + 1}
</span>
))}
</div>
</div>
<div>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs font-bold uppercase tracking-[0.22em] text-[#a0672a]">
{item.category}
</p>
<h3 className="mt-1 font-serif text-xl leading-6 text-[#201713]">{item.name}</h3>
</div>
<p className="font-semibold text-[#201713]">{formatCurrency(item.price)}</p>
</div>
{item.options && (
<p className="mt-2 text-xs leading-5 text-[#6d5546]">
{item.options.uploadedFileName && `Photo: ${item.options.uploadedFileName} · `}
{item.options.size && `${item.options.size} · `}
{item.options.difficulty && `${item.options.difficulty} difficulty`}
</p>
)}
</div>
</div>
<div className="mt-4 flex items-center justify-between gap-3">
<div className="inline-flex items-center rounded-full border border-[#d8b88f] bg-[#fffaf3]">
<button
type="button"
className="px-3 py-2 text-lg text-[#4e3b31] disabled:opacity-40"
onClick={() => onQuantityChange(item.id, item.quantity - 1)}
disabled={item.quantity <= 1}
aria-label={`Decrease quantity for ${item.name}`}
>
</button>
<span className="min-w-8 px-2 text-center text-sm font-bold text-[#201713]">{item.quantity}</span>
<button
type="button"
className="px-3 py-2 text-lg text-[#4e3b31] disabled:opacity-40"
onClick={() => onQuantityChange(item.id, item.quantity + 1)}
disabled={Boolean(item.maxInventory && item.quantity >= item.maxInventory)}
aria-label={`Increase quantity for ${item.name}`}
>
+
</button>
</div>
<button
type="button"
className="text-sm font-semibold text-[#8c3d2b] underline underline-offset-4"
onClick={() => onRemove(item.id)}
>
Remove
</button>
</div>
</article>
))}
</div>
)}
</div>
<div className="border-t border-[#ead8c3] bg-[#f8f1e8] px-6 py-5">
<div className="space-y-2 text-sm text-[#6d5546]">
<div className="flex justify-between">
<span>Subtotal</span>
<span className="font-semibold text-[#201713]">{formatCurrency(totals.subtotal)}</span>
</div>
<div className="flex justify-between">
<span>Shipping</span>
<span className="font-semibold text-[#201713]">
{totals.shipping === 0 ? 'Free' : formatCurrency(totals.shipping)}
</span>
</div>
<div className="flex justify-between border-t border-[#e3d0bc] pt-3 text-base font-bold text-[#201713]">
<span>Total</span>
<span>{formatCurrency(totals.total)}</span>
</div>
</div>
<div className="mt-5 grid gap-3">
<Link
href="/cart"
className="rounded-full bg-[#201713] px-5 py-3 text-center text-sm font-bold uppercase tracking-[0.18em] text-white transition hover:bg-[#3a2921]"
onClick={onClose}
>
View cart
</Link>
{items.length > 0 && (
<button
type="button"
className="text-sm font-semibold text-[#6d5546] underline underline-offset-4"
onClick={onClear}
>
Clear cart
</button>
)}
</div>
</div>
</aside>
</div>
);
}

View File

@ -0,0 +1,66 @@
import React, { useState } from 'react';
import Link from 'next/link';
import MadeDreamsCartDrawer from './MadeDreamsCartDrawer';
import useMadeDreamsCart from '../hooks/useMadeDreamsCart';
export default function MadeDreamsStoreHeader() {
const [isCartOpen, setIsCartOpen] = useState(false);
const { items, totals, updateQuantity, removeItem, clearCart } = useMadeDreamsCart();
return (
<header className="sticky top-0 z-30 border-b border-[#e5d5c4]/80 bg-[#f8f1e8]/90 backdrop-blur-xl">
<div className="mx-auto flex max-w-7xl items-center justify-between px-5 py-4 lg:px-8">
<Link href="/" className="group flex items-center gap-3" aria-label="Made Dreams home">
<span className="grid h-10 w-10 place-items-center rounded-full bg-[#201713] text-lg font-semibold text-[#f8f1e8] shadow-lg shadow-[#201713]/20">
MD
</span>
<span>
<span className="block font-serif text-xl tracking-[0.18em] text-[#201713]">Made Dreams</span>
<span className="text-xs uppercase tracking-[0.32em] text-[#8b6f57]">Painted personally</span>
</span>
</Link>
<nav className="hidden items-center gap-8 text-sm font-medium text-[#4e3b31] md:flex">
<Link className="transition hover:text-[#a0672a]" href="/#collections">
Collections
</Link>
<Link className="transition hover:text-[#a0672a]" href="/#generator">
Custom generator
</Link>
<Link className="transition hover:text-[#a0672a]" href="/#products">
Products
</Link>
<Link className="transition hover:text-[#a0672a]" href="/#faq">
FAQ
</Link>
</nav>
<div className="flex items-center gap-3">
<button
type="button"
className="rounded-full border border-[#dcc5ad] px-4 py-2 text-xs font-bold uppercase tracking-[0.2em] text-[#6d5546] transition hover:border-[#a0672a] hover:text-[#201713]"
onClick={() => setIsCartOpen(true)}
>
Cart {totals.itemCount}
</button>
<Link
href="/login"
className="hidden rounded-full bg-[#201713] px-4 py-2 text-sm font-semibold text-[#fffaf3] shadow-lg shadow-[#201713]/15 transition hover:-translate-y-0.5 hover:bg-[#3a2921] focus:outline-none focus:ring-2 focus:ring-[#c99455] focus:ring-offset-2 focus:ring-offset-[#f8f1e8] sm:inline-flex"
>
Admin login
</Link>
</div>
</div>
<MadeDreamsCartDrawer
open={isCartOpen}
items={items}
totals={totals}
onClose={() => setIsCartOpen(false)}
onQuantityChange={updateQuantity}
onRemove={removeItem}
onClear={clearCart}
/>
</header>
);
}

View File

@ -1,6 +1,5 @@
import React, {useEffect, useRef} from 'react'
import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'

View File

@ -0,0 +1,240 @@
export const MADE_DREAMS_CART_STORAGE_KEY = 'madeDreamsCart';
export const MADE_DREAMS_CART_EVENT = 'made-dreams-cart-updated';
export type MadeDreamsProduct = {
id: string;
name: string;
category: string;
price: number;
badge: string;
palette: string[];
imageClass: string;
description: string;
reviews: number;
inventory: number;
};
export type MadeDreamsCartItem = {
id: string;
productId: string;
type: 'product' | 'custom';
name: string;
category: string;
price: number;
quantity: number;
badge: string;
palette: string[];
imageClass: string;
description: string;
maxInventory?: number;
options?: {
size?: string;
difficulty?: string;
uploadedFileName?: string;
};
};
export type MadeDreamsCartSummary = {
itemCount: number;
subtotal: number;
shipping: number;
total: number;
};
export const madeDreamsProducts: MadeDreamsProduct[] = [
{
id: 'golden-hour-atelier-kit',
name: 'Golden Hour Atelier Kit',
category: 'Best seller',
price: 68,
badge: 'Ships in 3 days',
palette: ['#1f2937', '#b45309', '#f59e0b', '#fde68a', '#fff7ed'],
imageClass: 'from-[#19130f] via-[#9a5b22] to-[#f6d287]',
description: 'A warm architectural canvas with 28 rich acrylic colors and three fine-detail brushes.',
reviews: 184,
inventory: 18,
},
{
id: 'moonlit-anime-portrait',
name: 'Moonlit Anime Portrait',
category: 'New arrival',
price: 74,
badge: 'Limited drop',
palette: ['#111827', '#4c1d95', '#7c3aed', '#c4b5fd', '#f5f3ff'],
imageClass: 'from-[#111827] via-[#6d28d9] to-[#ddd6fe]',
description: 'A dramatic anime-inspired design with crisp numbered shapes and luminous violet hues.',
reviews: 92,
inventory: 12,
},
{
id: 'forever-family-custom-kit',
name: 'Forever Family Custom Kit',
category: 'Personalized',
price: 89,
badge: 'Upload-ready',
palette: ['#292524', '#7f1d1d', '#fb7185', '#fed7aa', '#fff7ed'],
imageClass: 'from-[#292524] via-[#be123c] to-[#fed7aa]',
description: 'Our premium custom kit for treasured photos, hand-balanced for a painterly finish.',
reviews: 267,
inventory: 25,
},
];
export const formatCurrency = (amount: number) =>
new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
const isBrowser = () => typeof window !== 'undefined';
const clampQuantity = (quantity: number, maxInventory = 99) => {
const nextQuantity = Number.isFinite(quantity) ? Math.floor(quantity) : 1;
return Math.max(1, Math.min(nextQuantity, maxInventory));
};
const isCartItem = (item: MadeDreamsCartItem) =>
Boolean(item?.id && item?.name && item?.price >= 0 && item?.quantity > 0);
export const readMadeDreamsCart = (): MadeDreamsCartItem[] => {
if (!isBrowser()) return [];
const rawCart = window.localStorage.getItem(MADE_DREAMS_CART_STORAGE_KEY);
if (!rawCart) return [];
try {
const parsedCart = JSON.parse(rawCart);
if (!Array.isArray(parsedCart)) return [];
return parsedCart.filter(isCartItem).map((item) => ({
...item,
quantity: clampQuantity(item.quantity, item.maxInventory),
}));
} catch (error) {
console.error('Failed to read Made Dreams cart from localStorage:', error);
return [];
}
};
const emitCartUpdated = () => {
if (!isBrowser()) return;
window.dispatchEvent(new Event(MADE_DREAMS_CART_EVENT));
};
export const writeMadeDreamsCart = (items: MadeDreamsCartItem[]) => {
if (!isBrowser()) return items;
window.localStorage.setItem(MADE_DREAMS_CART_STORAGE_KEY, JSON.stringify(items));
emitCartUpdated();
return items;
};
export const getMadeDreamsCartSummary = (items: MadeDreamsCartItem[]): MadeDreamsCartSummary => {
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const shipping = subtotal === 0 || subtotal >= 100 ? 0 : 9;
return {
itemCount,
subtotal,
shipping,
total: subtotal + shipping,
};
};
export const addMadeDreamsProductToCart = (product: MadeDreamsProduct, quantity = 1) => {
const items = readMadeDreamsCart();
const existingItem = items.find((item) => item.productId === product.id && item.type === 'product');
if (existingItem) {
const updatedItems = items.map((item) =>
item.id === existingItem.id
? {
...item,
quantity: clampQuantity(item.quantity + quantity, product.inventory),
}
: item,
);
return writeMadeDreamsCart(updatedItems);
}
return writeMadeDreamsCart([
...items,
{
id: product.id,
productId: product.id,
type: 'product',
name: product.name,
category: product.category,
price: product.price,
quantity: clampQuantity(quantity, product.inventory),
badge: product.badge,
palette: product.palette,
imageClass: product.imageClass,
description: product.description,
maxInventory: product.inventory,
},
]);
};
export const addMadeDreamsCustomKitToCart = ({
uploadedFileName,
size,
difficulty,
price,
}: {
uploadedFileName: string;
size: string;
difficulty: string;
price: number;
}) => {
const items = readMadeDreamsCart();
const cartLineId = `custom-kit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
return writeMadeDreamsCart([
...items,
{
id: cartLineId,
productId: 'custom-paint-by-numbers-kit',
type: 'custom',
name: 'Custom Paint By Numbers Kit',
category: 'Custom generator',
price,
quantity: 1,
badge: 'Personalized',
palette: ['#201713', '#a0672a', '#d7a25d', '#f0dfca', '#fff8ef'],
imageClass: 'from-[#201713] via-[#a0672a] to-[#f5d29a]',
description: 'Photo-to-canvas custom kit with numbered pattern, curated paints, and premium brushes.',
options: {
uploadedFileName,
size,
difficulty,
},
},
]);
};
export const updateMadeDreamsCartQuantity = (itemId: string, quantity: number) => {
const items = readMadeDreamsCart();
if (quantity < 1) {
return writeMadeDreamsCart(items.filter((item) => item.id !== itemId));
}
return writeMadeDreamsCart(
items.map((item) =>
item.id === itemId
? {
...item,
quantity: clampQuantity(quantity, item.maxInventory),
}
: item,
),
);
};
export const removeMadeDreamsCartItem = (itemId: string) =>
writeMadeDreamsCart(readMadeDreamsCart().filter((item) => item.id !== itemId));
export const clearMadeDreamsCart = () => writeMadeDreamsCart([]);

View File

@ -0,0 +1,70 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
addMadeDreamsCustomKitToCart,
addMadeDreamsProductToCart,
clearMadeDreamsCart,
getMadeDreamsCartSummary,
MADE_DREAMS_CART_EVENT,
MadeDreamsCartItem,
MadeDreamsProduct,
readMadeDreamsCart,
removeMadeDreamsCartItem,
updateMadeDreamsCartQuantity,
} from '../helpers/madeDreamsCart';
export default function useMadeDreamsCart() {
const [items, setItems] = useState<MadeDreamsCartItem[]>([]);
const refreshCart = useCallback(() => {
setItems(readMadeDreamsCart());
}, []);
useEffect(() => {
refreshCart();
const handleCartUpdate = () => refreshCart();
window.addEventListener('storage', handleCartUpdate);
window.addEventListener(MADE_DREAMS_CART_EVENT, handleCartUpdate);
return () => {
window.removeEventListener('storage', handleCartUpdate);
window.removeEventListener(MADE_DREAMS_CART_EVENT, handleCartUpdate);
};
}, [refreshCart]);
const addProduct = useCallback((product: MadeDreamsProduct, quantity = 1) => {
setItems(addMadeDreamsProductToCart(product, quantity));
}, []);
const addCustomKit = useCallback(
(customKit: { uploadedFileName: string; size: string; difficulty: string; price: number }) => {
setItems(addMadeDreamsCustomKitToCart(customKit));
},
[],
);
const updateQuantity = useCallback((itemId: string, quantity: number) => {
setItems(updateMadeDreamsCartQuantity(itemId, quantity));
}, []);
const removeItem = useCallback((itemId: string) => {
setItems(removeMadeDreamsCartItem(itemId));
}, []);
const clearCart = useCallback(() => {
setItems(clearMadeDreamsCart());
}, []);
const totals = useMemo(() => getMadeDreamsCartSummary(items), [items]);
return {
items,
totals,
addProduct,
addCustomKit,
updateQuantity,
removeItem,
clearCart,
};
}

View File

@ -1,5 +1,4 @@
import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'

167
frontend/src/pages/cart.tsx Normal file
View File

@ -0,0 +1,167 @@
import React, { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import MadeDreamsStoreHeader from '../components/MadeDreamsStoreHeader';
import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config';
import useMadeDreamsCart from '../hooks/useMadeDreamsCart';
import { formatCurrency } from '../helpers/madeDreamsCart';
export default function MadeDreamsCartPage() {
const { items, totals, updateQuantity, removeItem, clearCart } = useMadeDreamsCart();
return (
<div className="min-h-screen bg-[#f8f1e8] text-[#201713] antialiased">
<Head>
<title>{getPageTitle('Made Dreams Cart')}</title>
<meta name="description" content="Review your Made Dreams paint by numbers cart before checkout." />
</Head>
<MadeDreamsStoreHeader />
<main className="mx-auto max-w-7xl px-5 py-12 lg:px-8 lg:py-16">
<div className="flex flex-col justify-between gap-5 md:flex-row md:items-end">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-[#a0672a]">Shopping cart</p>
<h1 className="mt-3 font-serif text-5xl tracking-[-0.04em] md:text-7xl">Review your art kits.</h1>
</div>
<Link
href="/#products"
className="w-fit rounded-full border border-[#cbb49b] bg-white/45 px-6 py-3 text-sm font-bold uppercase tracking-[0.18em] text-[#2b201a] transition hover:bg-white"
>
Continue shopping
</Link>
</div>
{items.length === 0 ? (
<section className="mt-10 rounded-[2.5rem] border border-dashed border-[#d8b88f] bg-[#fffaf3] p-10 text-center shadow-sm">
<p className="font-serif text-4xl">Your cart is empty.</p>
<p className="mx-auto mt-4 max-w-xl leading-7 text-[#6d5546]">
Choose a ready-made canvas or upload a photo to create a personalized paint by numbers kit.
</p>
<div className="mt-8 flex flex-col justify-center gap-3 sm:flex-row">
<Link href="/#products" className="rounded-full bg-[#201713] px-6 py-4 text-sm font-bold uppercase tracking-[0.18em] text-white">
Shop products
</Link>
<Link href="/#generator" className="rounded-full border border-[#cbb49b] px-6 py-4 text-sm font-bold uppercase tracking-[0.18em] text-[#201713]">
Create custom kit
</Link>
</div>
</section>
) : (
<section className="mt-10 grid gap-8 lg:grid-cols-[1fr_24rem]">
<div className="space-y-5">
{items.map((item) => (
<article key={item.id} className="rounded-[2rem] border border-[#ead8c3] bg-[#fffaf3] p-5 shadow-sm">
<div className="grid gap-5 md:grid-cols-[9rem_1fr_auto] md:items-start">
<div className={`h-36 rounded-[1.5rem] bg-gradient-to-br ${item.imageClass} p-4`}>
<div className="grid h-full grid-cols-4 gap-1 opacity-70">
{Array.from({ length: 16 }).map((_, index) => (
<span key={index} className="rounded bg-white/20 text-center text-[9px] text-white">
{index + 1}
</span>
))}
</div>
</div>
<div>
<p className="text-xs font-bold uppercase tracking-[0.25em] text-[#a0672a]">{item.category}</p>
<h2 className="mt-2 font-serif text-3xl tracking-[-0.02em]">{item.name}</h2>
<p className="mt-3 max-w-2xl text-sm leading-6 text-[#6d5546]">{item.description}</p>
{item.options && (
<div className="mt-4 rounded-2xl bg-[#f5eadc] p-4 text-sm text-[#5e493d]">
{item.options.uploadedFileName && <p>Photo: {item.options.uploadedFileName}</p>}
{item.options.size && <p>Canvas size: {item.options.size}</p>}
{item.options.difficulty && <p>Difficulty: {item.options.difficulty}</p>}
</div>
)}
</div>
<div className="md:text-right">
<p className="font-serif text-3xl">{formatCurrency(item.price)}</p>
<p className="mt-1 text-xs uppercase tracking-[0.22em] text-[#8b6f57]">each</p>
<div className="mt-5 inline-flex items-center rounded-full border border-[#d8b88f] bg-white">
<button
type="button"
className="px-4 py-2 text-lg text-[#4e3b31] disabled:opacity-40"
onClick={() => updateQuantity(item.id, item.quantity - 1)}
disabled={item.quantity <= 1}
aria-label={`Decrease quantity for ${item.name}`}
>
</button>
<span className="min-w-10 px-2 text-center text-sm font-bold text-[#201713]">{item.quantity}</span>
<button
type="button"
className="px-4 py-2 text-lg text-[#4e3b31] disabled:opacity-40"
onClick={() => updateQuantity(item.id, item.quantity + 1)}
disabled={Boolean(item.maxInventory && item.quantity >= item.maxInventory)}
aria-label={`Increase quantity for ${item.name}`}
>
+
</button>
</div>
<button
type="button"
className="mt-4 block text-sm font-semibold text-[#8c3d2b] underline underline-offset-4 md:ml-auto"
onClick={() => removeItem(item.id)}
>
Remove
</button>
</div>
</div>
</article>
))}
</div>
<aside className="h-fit rounded-[2rem] border border-[#ead8c3] bg-[#201713] p-6 text-[#fff8ef] shadow-xl shadow-[#201713]/15">
<p className="text-sm font-bold uppercase tracking-[0.3em] text-[#d7a25d]">Order summary</p>
<div className="mt-6 space-y-3 text-sm text-[#d9c9bc]">
<div className="flex justify-between">
<span>Products</span>
<span>{totals.itemCount}</span>
</div>
<div className="flex justify-between">
<span>Subtotal</span>
<span>{formatCurrency(totals.subtotal)}</span>
</div>
<div className="flex justify-between">
<span>Shipping cost</span>
<span>{totals.shipping === 0 ? 'Free' : formatCurrency(totals.shipping)}</span>
</div>
<div className="flex justify-between border-t border-white/10 pt-4 text-lg font-bold text-white">
<span>Total price</span>
<span>{formatCurrency(totals.total)}</span>
</div>
</div>
<div className="mt-6 rounded-[1.5rem] border border-[#d7a25d]/30 bg-[#d7a25d]/10 p-4 text-sm leading-6 text-[#f4ddbf]">
Checkout, Stripe, PayPal, order creation, inventory reduction, and confirmation emails are the next production
payment phase. This cart is now saved and ready for that connection.
</div>
<button
type="button"
className="mt-5 w-full cursor-not-allowed rounded-full bg-[#d7a25d]/60 px-5 py-4 text-sm font-bold uppercase tracking-[0.18em] text-[#201713]"
disabled
>
Checkout coming next
</button>
<button
type="button"
className="mt-4 w-full text-sm font-semibold text-[#d9c9bc] underline underline-offset-4"
onClick={clearCart}
>
Clear cart
</button>
</aside>
</section>
)}
</main>
</div>
);
}
MadeDreamsCartPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -1,166 +1,622 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import MadeDreamsStoreHeader from '../components/MadeDreamsStoreHeader';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
import useMadeDreamsCart from '../hooks/useMadeDreamsCart';
import { formatCurrency, madeDreamsProducts, MadeDreamsProduct } from '../helpers/madeDreamsCart';
type WorkflowStep = 'upload' | 'configure' | 'checkout';
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('left');
const textColor = useAppSelector((state) => state.style.linkColor);
const categories = [
{
title: 'Adult Paint By Numbers',
copy: 'Museum-inspired canvases with layered color maps for mindful evenings.',
accent: 'from-stone-200 to-amber-100',
},
{
title: 'Anime Collection',
copy: 'Cinematic linework, expressive palettes, and premium numbered detail.',
accent: 'from-violet-200 to-pink-100',
},
{
title: 'Celebrity Collection',
copy: 'Iconic portraits translated into statement art kits for grown-up spaces.',
accent: 'from-zinc-200 to-neutral-100',
},
{
title: 'Family Memories Collection',
copy: 'Turn weddings, vacations, and heirloom photos into keepsake canvases.',
accent: 'from-rose-100 to-orange-100',
},
{
title: 'Custom Paint By Numbers Generator',
copy: 'Upload your photo, preview the conversion, then choose your kit finish.',
accent: 'from-emerald-100 to-cyan-100',
},
];
const title = 'Made Dreams'
const reviews = [
'The kit felt like opening a luxury gift. My wedding photo turned into a beautiful weekend project.',
'The numbered canvas was detailed but relaxing, and the paints were much richer than craft-store kits.',
'I bought one for my mom and one for myself. The custom preview made ordering feel effortless.',
];
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const faqs = [
{
question: 'How does the custom generator work?',
answer:
'Upload a clear photo, choose your canvas size and difficulty, and Made Dreams prepares a numbered canvas kit with acrylic paints and brushes.',
},
{
question: 'What makes these kits premium?',
answer:
'Each kit is designed for adults with elevated palettes, gallery-style packaging, pre-numbered canvas, curated paints, and detail brushes.',
},
{
question: 'Can I use a family or pet photo?',
answer:
'Yes. Family portraits, travel memories, pets, and celebration photos are ideal for the custom workflow.',
},
];
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
export default function MadeDreamsHome() {
const [step, setStep] = useState<WorkflowStep>('upload');
const [selectedProduct, setSelectedProduct] = useState<MadeDreamsProduct>(madeDreamsProducts[0]);
const [uploadedFile, setUploadedFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState('');
const [size, setSize] = useState('16 × 20 in');
const [difficulty, setDifficulty] = useState('Balanced');
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const { addProduct, addCustomKit } = useMadeDreamsCart();
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
useEffect(() => {
if (!uploadedFile) {
setPreviewUrl('');
return undefined;
}
const objectUrl = URL.createObjectURL(uploadedFile);
setPreviewUrl(objectUrl);
return () => URL.revokeObjectURL(objectUrl);
}, [uploadedFile]);
const kitPrice = useMemo(() => {
const base = size === '12 × 16 in' ? 64 : size === '16 × 20 in' ? 89 : 124;
const difficultyPremium = difficulty === 'Masterpiece' ? 18 : difficulty === 'Simple' ? 0 : 9;
return base + difficultyPremium;
}, [difficulty, size]);
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
setSuccess('');
if (!file) return;
if (!file.type.startsWith('image/')) {
setError('Please upload a JPG, PNG, or WEBP image so we can create your custom canvas preview.');
setUploadedFile(null);
setStep('upload');
return;
}
setError('');
setUploadedFile(file);
setStep('configure');
};
const handleAddProductToCart = (product: MadeDreamsProduct) => {
try {
setSelectedProduct(product);
addProduct(product);
setError('');
setSuccess(`${product.name} added to your Made Dreams cart.`);
} catch (addError) {
console.error('Failed to add Made Dreams product to cart:', addError);
setError('We could not save this product to your cart. Please try again.');
throw addError;
}
};
const handleAddCustomKitToCart = () => {
if (!uploadedFile) {
setError('Upload a photo first to create your custom Made Dreams kit.');
setStep('upload');
return;
}
try {
addCustomKit({
uploadedFileName: uploadedFile.name,
size,
difficulty,
price: kitPrice,
});
setError('');
setSuccess(
`Custom kit added: ${uploadedFile.name} · ${size} · ${difficulty} difficulty · ${formatCurrency(
kitPrice,
)}`,
);
setStep('checkout');
} catch (addError) {
console.error('Failed to add Made Dreams custom kit to cart:', addError);
setError('We could not save your custom kit to the cart. Please try again.');
throw addError;
}
};
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<div className="min-h-screen bg-[#f8f1e8] text-[#201713] antialiased">
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('Made Dreams Premium Paint By Numbers')}</title>
<meta
name="description"
content="Made Dreams creates premium adult paint by numbers canvas kits and custom photo-to-canvas painting experiences."
/>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Made Dreams app!"/>
<div className="space-y-3">
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center '>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
<MadeDreamsStoreHeader />
{success && (
<div
className="fixed bottom-5 left-1/2 z-40 w-[calc(100%-2rem)] max-w-2xl -translate-x-1/2 rounded-[1.5rem] border border-[#d7a25d] bg-[#201713] p-4 text-[#fff8ef] shadow-2xl shadow-[#201713]/25"
aria-live="polite"
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p className="text-sm leading-6">{success}</p>
<div className="flex shrink-0 items-center gap-3">
<Link href="/cart" className="rounded-full bg-[#d7a25d] px-4 py-2 text-xs font-bold uppercase tracking-[0.18em] text-[#201713]">
View cart
</Link>
<button type="button" className="text-xs font-semibold uppercase tracking-[0.18em]" onClick={() => setSuccess('')}>
Dismiss
</button>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</BaseButtons>
</CardBox>
</div>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
)}
<main>
<section className="relative overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(208,158,95,0.35),_transparent_34%),radial-gradient(circle_at_80%_20%,_rgba(75,43,32,0.18),_transparent_28%)]" />
<div className="relative mx-auto grid max-w-7xl gap-12 px-5 py-16 lg:grid-cols-[1.02fr_0.98fr] lg:px-8 lg:py-24">
<div className="flex flex-col justify-center">
<p className="mb-5 inline-flex w-fit rounded-full border border-[#d5b98f] bg-white/45 px-4 py-2 text-xs font-semibold uppercase tracking-[0.35em] text-[#9a6332]">
Premium custom canvas kits
</p>
<h1 className="max-w-3xl font-serif text-5xl leading-[0.95] tracking-[-0.04em] text-[#201713] md:text-7xl">
Turn your most meaningful photos into frame-worthy paint by numbers art.
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-[#6d5546]">
Made Dreams sells adult paint by numbers kits for collectors, gift-givers, and creative nights in. Upload a
photo, preview the numbered artwork, choose your finish, and start painting something personal.
</p>
<div className="mt-9 flex flex-col gap-3 sm:flex-row">
<a
href="#generator"
className="rounded-full bg-[#a0672a] px-7 py-4 text-center text-sm font-bold uppercase tracking-[0.18em] text-white shadow-2xl shadow-[#a0672a]/30 transition hover:-translate-y-1 hover:bg-[#8c5520] focus:outline-none focus:ring-2 focus:ring-[#201713] focus:ring-offset-2 focus:ring-offset-[#f8f1e8]"
>
Create custom kit
</a>
<a
href="#products"
className="rounded-full border border-[#cbb49b] bg-white/40 px-7 py-4 text-center text-sm font-bold uppercase tracking-[0.18em] text-[#2b201a] transition hover:-translate-y-1 hover:bg-white focus:outline-none focus:ring-2 focus:ring-[#c99455] focus:ring-offset-2 focus:ring-offset-[#f8f1e8]"
>
Shop best sellers
</a>
</div>
</div>
<div className="relative min-h-[520px]">
<div className="absolute left-4 top-8 h-64 w-52 rounded-[2rem] bg-gradient-to-br from-[#201713] via-[#7b5030] to-[#f0c171] p-4 shadow-2xl shadow-[#3b261d]/30 md:left-10">
<div className="h-full rounded-[1.5rem] border border-white/30 bg-white/10 p-4 backdrop-blur-sm">
<div className="grid h-full grid-cols-4 gap-2 opacity-80">
{Array.from({ length: 24 }).map((_, index) => (
<span key={index} className="rounded-full bg-white/20 text-center text-[10px] text-white">
{index + 1}
</span>
))}
</div>
</div>
</div>
<div className="absolute right-0 top-0 h-[430px] w-[78%] rounded-[3rem] bg-[#fff8ef] p-5 shadow-2xl shadow-[#533621]/20 md:w-[68%]">
<div className="relative h-full overflow-hidden rounded-[2.4rem] bg-gradient-to-br from-[#271b16] via-[#9d6737] to-[#f3d4a3]">
<div className="absolute inset-0 bg-[linear-gradient(115deg,_transparent_0_18%,_rgba(255,255,255,0.28)_18%_19%,_transparent_19%_42%,_rgba(255,255,255,0.18)_42%_43%,_transparent_43%)]" />
<div className="absolute bottom-5 left-5 right-5 rounded-3xl border border-white/30 bg-white/20 p-5 text-white backdrop-blur-md">
<p className="text-xs uppercase tracking-[0.3em]">Custom preview</p>
<p className="mt-2 font-serif text-3xl">AI-numbered canvas artwork</p>
</div>
</div>
</div>
<div className="absolute bottom-6 left-0 max-w-xs rounded-[2rem] border border-[#ead8c3] bg-white/80 p-5 shadow-xl backdrop-blur-md">
<p className="text-xs font-bold uppercase tracking-[0.3em] text-[#a0672a]">Kit includes</p>
<p className="mt-3 text-sm leading-6 text-[#5e493d]">
Numbered canvas, acrylic paints, brush set, reference guide, and gift-ready packaging.
</p>
</div>
</div>
</div>
</section>
<section id="collections" className="mx-auto max-w-7xl px-5 py-16 lg:px-8">
<div className="flex flex-col justify-between gap-5 md:flex-row md:items-end">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-[#a0672a]">Shop by mood</p>
<h2 className="mt-3 font-serif text-4xl tracking-[-0.03em] md:text-5xl">Curated collections for adult artists.</h2>
</div>
<p className="max-w-xl leading-7 text-[#6d5546]">
Browse ready-made canvases or start from a personal photo. Every kit is designed to feel premium from
checkout to final brushstroke.
</p>
</div>
<div className="mt-10 grid gap-4 md:grid-cols-2 lg:grid-cols-5">
{categories.map((category) => (
<a
key={category.title}
href={category.title.includes('Generator') ? '#generator' : '#products'}
className="group rounded-[2rem] border border-[#ead8c3] bg-white/55 p-4 shadow-sm transition hover:-translate-y-1 hover:bg-white hover:shadow-xl"
>
<div className={`h-40 rounded-[1.5rem] bg-gradient-to-br ${category.accent}`} />
<h3 className="mt-5 font-serif text-2xl leading-7 text-[#201713]">{category.title}</h3>
<p className="mt-3 text-sm leading-6 text-[#6d5546]">{category.copy}</p>
<span className="mt-5 inline-flex text-xs font-bold uppercase tracking-[0.22em] text-[#a0672a]">
Explore
</span>
</a>
))}
</div>
</section>
<section id="products" className="bg-[#fffaf3] px-5 py-16 lg:px-8">
<div className="mx-auto max-w-7xl">
<div className="flex flex-col justify-between gap-5 md:flex-row md:items-end">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-[#a0672a]">Best sellers & new arrivals</p>
<h2 className="mt-3 font-serif text-4xl tracking-[-0.03em] md:text-5xl">Premium kits ready for your cart.</h2>
</div>
<p className="max-w-xl leading-7 text-[#6d5546]">
Every product now has a working add-to-cart button with saved quantities, inventory limits, and a cart drawer.
</p>
</div>
<div className="mt-10 grid gap-6 lg:grid-cols-3">
{madeDreamsProducts.map((product) => (
<article key={product.id} className="rounded-[2rem] border border-[#ead8c3] bg-[#f8f1e8] p-4 shadow-sm">
<button
type="button"
className={`h-64 w-full rounded-[1.7rem] bg-gradient-to-br ${product.imageClass} p-5 text-left shadow-inner`}
onClick={() => setSelectedProduct(product)}
>
<span className="rounded-full bg-white/20 px-3 py-1 text-xs font-bold uppercase tracking-[0.22em] text-white">
{product.category}
</span>
<div className="mt-16 grid grid-cols-5 gap-2 opacity-70">
{Array.from({ length: 15 }).map((_, index) => (
<span key={index} className="rounded bg-white/25 text-center text-[10px] text-white">
{index + 1}
</span>
))}
</div>
</button>
<div className="p-3">
<div className="flex items-start justify-between gap-3">
<h3 className="font-serif text-3xl leading-8">{product.name}</h3>
<span className="font-bold text-[#a0672a]">{formatCurrency(product.price)}</span>
</div>
<p className="mt-3 text-sm leading-6 text-[#6d5546]">{product.description}</p>
<div className="mt-5 flex items-center justify-between gap-3">
<button
type="button"
onClick={() => setSelectedProduct(product)}
className="text-xs font-bold uppercase tracking-[0.2em] text-[#6d5546] underline underline-offset-4"
>
Preview
</button>
<button
type="button"
onClick={() => handleAddProductToCart(product)}
className="rounded-full bg-[#201713] px-5 py-3 text-xs font-bold uppercase tracking-[0.18em] text-white transition hover:-translate-y-0.5 hover:bg-[#3a2921]"
>
Add to cart
</button>
</div>
</div>
</article>
))}
</div>
</div>
</section>
<section id="generator" className="bg-[#201713] px-5 py-16 text-[#fff8ef] lg:px-8">
<div className="mx-auto grid max-w-7xl gap-10 lg:grid-cols-[0.95fr_1.05fr]">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-[#d7a25d]">Custom Paint By Numbers Generator</p>
<h2 className="mt-3 font-serif text-4xl tracking-[-0.03em] md:text-6xl">
Upload image AI pattern choose size add to cart.
</h2>
<p className="mt-5 leading-8 text-[#d9c9bc]">
This workflow now creates a saved cart item for the customer&apos;s personalized kit. The next phase will
convert it into a paid backend order after Stripe or PayPal confirms payment.
</p>
<div className="mt-8 grid gap-3 sm:grid-cols-3">
{['Upload photo', 'Select finish', 'Review cart'].map((label, index) => (
<div
key={label}
className={`rounded-3xl border p-4 ${
step === (index === 0 ? 'upload' : index === 1 ? 'configure' : 'checkout')
? 'border-[#d7a25d] bg-[#d7a25d]/10'
: 'border-white/10 bg-white/5'
}`}
>
<span className="text-xs font-bold uppercase tracking-[0.3em] text-[#d7a25d]">Step {index + 1}</span>
<p className="mt-2 font-serif text-2xl">{label}</p>
</div>
))}
</div>
</div>
<div className="rounded-[2.5rem] border border-white/10 bg-white/5 p-5 shadow-2xl shadow-black/20">
<label className="flex min-h-[280px] cursor-pointer flex-col items-center justify-center rounded-[2rem] border border-dashed border-[#d7a25d]/50 bg-[#130f0d]/50 p-6 text-center transition hover:border-[#d7a25d]">
{previewUrl ? (
<img src={previewUrl} alt="Uploaded custom kit preview" className="max-h-64 rounded-[1.5rem] object-contain shadow-2xl" />
) : (
<div>
<p className="font-serif text-3xl">Upload your photo</p>
<p className="mt-3 max-w-md text-sm leading-6 text-[#d9c9bc]">
JPG, PNG, or WEBP works best. High-contrast family, pet, portrait, travel, and wedding images are ideal.
</p>
</div>
)}
<input className="sr-only" type="file" accept="image/*" onChange={handleUpload} />
</label>
{error && <p className="mt-4 rounded-2xl bg-red-500/15 p-4 text-sm text-red-100">{error}</p>}
<div className="mt-5 grid gap-4 sm:grid-cols-2">
<label className="block">
<span className="text-xs font-bold uppercase tracking-[0.25em] text-[#d7a25d]">Canvas size</span>
<select
value={size}
onChange={(event) => setSize(event.target.value)}
className="mt-2 w-full rounded-2xl border border-white/10 bg-[#fff8ef] px-4 py-3 text-[#201713]"
>
<option>12 × 16 in</option>
<option>16 × 20 in</option>
<option>24 × 36 in</option>
</select>
</label>
<label className="block">
<span className="text-xs font-bold uppercase tracking-[0.25em] text-[#d7a25d]">Difficulty</span>
<select
value={difficulty}
onChange={(event) => setDifficulty(event.target.value)}
className="mt-2 w-full rounded-2xl border border-white/10 bg-[#fff8ef] px-4 py-3 text-[#201713]"
>
<option>Simple</option>
<option>Balanced</option>
<option>Masterpiece</option>
</select>
</label>
</div>
<div className="mt-5 rounded-[2rem] bg-[#fff8ef] p-5 text-[#201713]">
<div className="flex items-start justify-between gap-4">
<div>
<p className="text-xs font-bold uppercase tracking-[0.25em] text-[#a0672a]">Canvas preview</p>
<p className="mt-2 font-serif text-3xl">{uploadedFile ? uploadedFile.name : 'Waiting for upload'}</p>
<p className="mt-2 text-sm text-[#6d5546]">
{size} · {difficulty} difficulty · numbered pattern kit
</p>
</div>
<p className="font-serif text-3xl">{formatCurrency(kitPrice)}</p>
</div>
<button
type="button"
onClick={handleAddCustomKitToCart}
className="mt-5 w-full rounded-full bg-[#a0672a] px-6 py-4 text-sm font-bold uppercase tracking-[0.18em] text-white transition hover:bg-[#8c5520]"
>
Add custom kit to cart
</button>
</div>
</div>
</div>
</section>
<section className="mx-auto max-w-7xl px-5 py-16 lg:px-8">
<div className="rounded-[2.5rem] border border-[#ead8c3] bg-[#fffaf3] p-5 shadow-xl shadow-[#533621]/10 md:p-8">
<div className="grid gap-8 lg:grid-cols-[1fr_0.9fr]">
<div className="rounded-[2rem] bg-[#201713] p-4">
<div className={`h-72 rounded-[1.6rem] bg-gradient-to-br ${selectedProduct.imageClass} p-5`}>
<div className="grid h-full grid-cols-6 gap-2 opacity-75">
{Array.from({ length: 36 }).map((_, index) => (
<span key={index} className="rounded bg-white/20 text-center text-[10px] text-white">
{index + 1}
</span>
))}
</div>
</div>
<div className="mt-3 grid grid-cols-3 gap-3">
{[1, 2, 3].map((item) => (
<div
key={item}
className={`h-20 rounded-2xl bg-gradient-to-br ${selectedProduct.imageClass} ${
item === 1 ? 'opacity-100' : 'opacity-70'
}`}
/>
))}
</div>
</div>
<div className="flex flex-col">
<span className="w-fit rounded-full bg-[#f0dfca] px-3 py-1 text-xs font-bold uppercase tracking-[0.22em] text-[#8c5520]">
{selectedProduct.badge}
</span>
<h3 className="mt-4 font-serif text-4xl tracking-[-0.03em]">{selectedProduct.name}</h3>
<p className="mt-4 leading-7 text-[#6d5546]">{selectedProduct.description}</p>
<div className="mt-6 rounded-3xl bg-[#f5eadc] p-5">
<p className="text-sm font-semibold uppercase tracking-[0.25em] text-[#a0672a]">Paint colors</p>
<div className="mt-4 flex gap-2">
{selectedProduct.palette.map((color) => (
<span key={color} className="h-10 w-10 rounded-full border border-white shadow" style={{ backgroundColor: color }} />
))}
</div>
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-2">
<div className="rounded-3xl border border-[#e3d0bc] p-4">
<p className="font-semibold">Brush information</p>
<p className="mt-2 text-sm text-[#725b4c]">Includes wide, medium, and micro-detail nylon brushes.</p>
</div>
<div className="rounded-3xl border border-[#e3d0bc] p-4">
<p className="font-semibold">Reviews</p>
<p className="mt-2 text-sm text-[#725b4c]"> {selectedProduct.reviews} verified painters</p>
</div>
</div>
<button
type="button"
onClick={() => handleAddProductToCart(selectedProduct)}
className="mt-6 rounded-full bg-[#201713] px-6 py-4 text-sm font-bold uppercase tracking-[0.2em] text-white transition hover:-translate-y-1 hover:bg-[#3a2921] focus:outline-none focus:ring-2 focus:ring-[#a0672a] focus:ring-offset-2 focus:ring-offset-[#fffaf3]"
>
Add to cart · {formatCurrency(selectedProduct.price)}
</button>
</div>
</div>
</div>
</section>
<section className="bg-[#fffaf3] px-5 py-16 lg:px-8">
<div className="mx-auto grid max-w-7xl gap-6 lg:grid-cols-[0.8fr_1.2fr]">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-[#a0672a]">Customer reviews</p>
<h2 className="mt-3 font-serif text-4xl tracking-[-0.03em] md:text-5xl">Made to become a memory.</h2>
</div>
<div className="grid gap-4 md:grid-cols-3">
{reviews.map((review) => (
<blockquote key={review} className="rounded-[2rem] border border-[#ead8c3] bg-white p-6 shadow-sm">
<p className="text-lg leading-8 text-[#4e3b31]">{review}</p>
<footer className="mt-5 text-sm font-semibold uppercase tracking-[0.22em] text-[#a0672a]">Verified customer</footer>
</blockquote>
))}
</div>
</div>
</section>
<section id="faq" className="mx-auto grid max-w-7xl gap-8 px-5 py-16 lg:grid-cols-[0.8fr_1.2fr] lg:px-8">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-[#a0672a]">FAQ</p>
<h2 className="mt-3 font-serif text-4xl tracking-[-0.03em] md:text-5xl">Questions before you paint?</h2>
<div className="mt-8 rounded-[2rem] bg-[#201713] p-6 text-[#fff8ef]">
<p className="font-serif text-3xl">Join the studio list</p>
<p className="mt-3 text-sm leading-6 text-[#d9c9bc]">Get new arrivals, custom kit tips, and launch offers.</p>
<form className="mt-5 flex flex-col gap-3 sm:flex-row" onSubmit={(event) => event.preventDefault()}>
<input
className="min-w-0 flex-1 rounded-full border border-white/10 bg-white/10 px-5 py-3 text-white placeholder:text-[#d9c9bc] focus:border-[#d7a25d] focus:outline-none focus:ring-2 focus:ring-[#d7a25d]"
placeholder="you@example.com"
type="email"
aria-label="Email address"
/>
<button className="rounded-full bg-[#d7a25d] px-5 py-3 text-sm font-bold uppercase tracking-[0.18em] text-[#201713]" type="submit">
Subscribe
</button>
</form>
</div>
</div>
<div className="space-y-3">
{faqs.map((faq) => (
<details key={faq.question} className="group rounded-[1.5rem] border border-[#e3d0bc] bg-white/65 p-5 open:bg-white">
<summary className="cursor-pointer list-none font-serif text-2xl text-[#201713] marker:hidden">{faq.question}</summary>
<p className="mt-3 leading-7 text-[#6d5546]">{faq.answer}</p>
</details>
))}
</div>
</section>
</main>
<footer className="bg-[#130f0d] px-5 py-14 text-[#fff8ef] lg:px-8">
<div className="mx-auto grid max-w-7xl gap-10 md:grid-cols-[1.2fr_repeat(3,1fr)]">
<div>
<p className="font-serif text-3xl tracking-[0.18em]">Made Dreams</p>
<p className="mt-4 max-w-sm leading-7 text-[#cdbbac]">
A premium personalized art marketplace for adult paint by numbers canvas kits.
</p>
</div>
<div>
<h4 className="text-sm font-bold uppercase tracking-[0.3em] text-[#d7a25d]">Shop</h4>
<ul className="mt-4 space-y-3 text-[#d9c9bc]">
<li>
<a href="#collections" className="hover:text-white">
Adult Paint By Numbers
</a>
</li>
<li>
<a href="#collections" className="hover:text-white">
Anime Collection
</a>
</li>
<li>
<a href="#generator" className="hover:text-white">
Custom Generator
</a>
</li>
<li>
<Link href="/cart" className="hover:text-white">
Cart
</Link>
</li>
</ul>
</div>
<div>
<h4 className="text-sm font-bold uppercase tracking-[0.3em] text-[#d7a25d]">Customer Support</h4>
<ul className="mt-4 space-y-3 text-[#d9c9bc]">
<li>
<a href="#faq" className="hover:text-white">
FAQ
</a>
</li>
<li>
<a href="#faq" className="hover:text-white">
Returns Policy
</a>
</li>
<li>
<a href="mailto:hello@madedreams.example" className="hover:text-white">
Contact Us
</a>
</li>
</ul>
</div>
<div>
<h4 className="text-sm font-bold uppercase tracking-[0.3em] text-[#d7a25d]">Policies</h4>
<ul className="mt-4 space-y-3 text-[#d9c9bc]">
<li>
<Link href="/privacy-policy" className="hover:text-white">
Privacy Policy
</Link>
</li>
<li>
<a href="#faq" className="hover:text-white">
Terms Of Service
</a>
</li>
<li>
<Link href="/login" className="hover:text-white">
Admin interface
</Link>
</li>
</ul>
</div>
</div>
<div className="mx-auto mt-10 max-w-7xl border-t border-white/10 pt-6 text-sm text-[#a89587]">
© 2026 Made Dreams. All rights reserved.
</div>
</footer>
</div>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
MadeDreamsHome.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};