Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dae559a42b | ||
|
|
205eee3c6c |
185
frontend/src/components/MadeDreamsCartDrawer.tsx
Normal file
185
frontend/src/components/MadeDreamsCartDrawer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
frontend/src/components/MadeDreamsStoreHeader.tsx
Normal file
66
frontend/src/components/MadeDreamsStoreHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useRef} from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
import BaseDivider from './BaseDivider'
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
|
|||||||
240
frontend/src/helpers/madeDreamsCart.ts
Normal file
240
frontend/src/helpers/madeDreamsCart.ts
Normal 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([]);
|
||||||
70
frontend/src/hooks/useMadeDreamsCart.ts
Normal file
70
frontend/src/hooks/useMadeDreamsCart.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react'
|
import React, { ReactNode, useEffect, useState } from 'react'
|
||||||
import { useState } from 'react'
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
|
|||||||
167
frontend/src/pages/cart.tsx
Normal file
167
frontend/src/pages/cart.tsx
Normal 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>;
|
||||||
|
};
|
||||||
@ -1,166 +1,622 @@
|
|||||||
|
import React, { ReactElement, useEffect, useMemo, useState } from 'react';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import type { ReactElement } from 'react';
|
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import BaseButton from '../components/BaseButton';
|
import MadeDreamsStoreHeader from '../components/MadeDreamsStoreHeader';
|
||||||
import CardBox from '../components/CardBox';
|
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import BaseButtons from '../components/BaseButtons';
|
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
import useMadeDreamsCart from '../hooks/useMadeDreamsCart';
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
import { formatCurrency, madeDreamsProducts, MadeDreamsProduct } from '../helpers/madeDreamsCart';
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
|
||||||
|
|
||||||
|
type WorkflowStep = 'upload' | 'configure' | 'checkout';
|
||||||
|
|
||||||
export default function Starter() {
|
const categories = [
|
||||||
const [illustrationImage, setIllustrationImage] = useState({
|
{
|
||||||
src: undefined,
|
title: 'Adult Paint By Numbers',
|
||||||
photographer: undefined,
|
copy: 'Museum-inspired canvases with layered color maps for mindful evenings.',
|
||||||
photographer_url: undefined,
|
accent: 'from-stone-200 to-amber-100',
|
||||||
})
|
},
|
||||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
{
|
||||||
const [contentType, setContentType] = useState('image');
|
title: 'Anime Collection',
|
||||||
const [contentPosition, setContentPosition] = useState('left');
|
copy: 'Cinematic linework, expressive palettes, and premium numbered detail.',
|
||||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
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.',
|
||||||
|
];
|
||||||
|
|
||||||
|
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.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchData() {
|
if (!uploadedFile) {
|
||||||
const image = await getPexelsImage();
|
setPreviewUrl('');
|
||||||
const video = await getPexelsVideo();
|
return undefined;
|
||||||
setIllustrationImage(image);
|
|
||||||
setIllustrationVideo(video);
|
|
||||||
}
|
}
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const imageBlock = (image) => (
|
const objectUrl = URL.createObjectURL(uploadedFile);
|
||||||
<div
|
setPreviewUrl(objectUrl);
|
||||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
|
||||||
style={{
|
return () => URL.revokeObjectURL(objectUrl);
|
||||||
backgroundImage: `${
|
}, [uploadedFile]);
|
||||||
image
|
|
||||||
? `url(${image?.src?.original})`
|
const kitPrice = useMemo(() => {
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
const base = size === '12 × 16 in' ? 64 : size === '16 × 20 in' ? 89 : 124;
|
||||||
}`,
|
const difficultyPremium = difficulty === 'Masterpiece' ? 18 : difficulty === 'Simple' ? 0 : 9;
|
||||||
backgroundSize: 'cover',
|
return base + difficultyPremium;
|
||||||
backgroundPosition: 'left center',
|
}, [difficulty, size]);
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}}
|
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
>
|
const file = event.target.files?.[0];
|
||||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
setSuccess('');
|
||||||
<a
|
|
||||||
className='text-[8px]'
|
if (!file) return;
|
||||||
href={image?.photographer_url}
|
|
||||||
target='_blank'
|
if (!file.type.startsWith('image/')) {
|
||||||
rel='noreferrer'
|
setError('Please upload a JPG, PNG, or WEBP image so we can create your custom canvas preview.');
|
||||||
>
|
setUploadedFile(null);
|
||||||
Photo by {image?.photographer} on Pexels
|
setStep('upload');
|
||||||
</a>
|
return;
|
||||||
</div>
|
}
|
||||||
</div>
|
|
||||||
|
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');
|
||||||
const videoBlock = (video) => {
|
} catch (addError) {
|
||||||
if (video?.video_files?.length > 0) {
|
console.error('Failed to add Made Dreams custom kit to cart:', addError);
|
||||||
return (
|
setError('We could not save your custom kit to the cart. Please try again.');
|
||||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
throw addError;
|
||||||
<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>)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="min-h-screen bg-[#f8f1e8] text-[#201713] antialiased">
|
||||||
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',
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Head>
|
<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>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<MadeDreamsStoreHeader />
|
||||||
|
|
||||||
|
{success && (
|
||||||
<div
|
<div
|
||||||
className={`flex ${
|
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"
|
||||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
aria-live="polite"
|
||||||
} min-h-screen w-full`}
|
|
||||||
>
|
>
|
||||||
{contentType === 'image' && contentPosition !== 'background'
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
? imageBlock(illustrationImage)
|
<p className="text-sm leading-6">{success}</p>
|
||||||
: null}
|
<div className="flex shrink-0 items-center gap-3">
|
||||||
{contentType === 'video' && contentPosition !== 'background'
|
<Link href="/cart" className="rounded-full bg-[#d7a25d] px-4 py-2 text-xs font-bold uppercase tracking-[0.18em] text-[#201713]">
|
||||||
? videoBlock(illustrationVideo)
|
View cart
|
||||||
: null}
|
</Link>
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
<button type="button" className="text-xs font-semibold uppercase tracking-[0.18em]" onClick={() => setSuccess('')}>
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
Dismiss
|
||||||
<CardBoxComponentTitle title="Welcome to your Made Dreams app!"/>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-3">
|
<main>
|
||||||
<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>
|
<section className="relative overflow-hidden">
|
||||||
<p className='text-center '>For guides and documentation please check
|
<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%)]" />
|
||||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
<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>
|
||||||
|
|
||||||
<BaseButtons>
|
<div className="relative min-h-[520px]">
|
||||||
<BaseButton
|
<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">
|
||||||
href='/login'
|
<div className="h-full rounded-[1.5rem] border border-white/30 bg-white/10 p-4 backdrop-blur-sm">
|
||||||
label='Login'
|
<div className="grid h-full grid-cols-4 gap-2 opacity-80">
|
||||||
color='info'
|
{Array.from({ length: 24 }).map((_, index) => (
|
||||||
className='w-full'
|
<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'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>
|
||||||
|
|
||||||
</BaseButtons>
|
<div className="flex flex-col">
|
||||||
</CardBox>
|
<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>
|
</div>
|
||||||
</SectionFullScreen>
|
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
<div className="rounded-3xl border border-[#e3d0bc] p-4">
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
<p className="font-semibold">Brush information</p>
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
<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
|
Privacy Policy
|
||||||
</Link>
|
</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>
|
||||||
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
MadeDreamsHome.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user