From 4811b73c69d88efdc8da8bca24d1a01caa3a4647 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Mon, 22 Jun 2026 12:27:35 +0200 Subject: [PATCH] feat: mockup page with template library and generator UI --- src/app/(dashboard)/mockup/page.tsx | 100 +++++++++++++++ src/components/layout/Sidebar.tsx | 1 + src/components/mockup/TemplateLibrary.tsx | 144 ++++++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 src/app/(dashboard)/mockup/page.tsx create mode 100644 src/components/mockup/TemplateLibrary.tsx diff --git a/src/app/(dashboard)/mockup/page.tsx b/src/app/(dashboard)/mockup/page.tsx new file mode 100644 index 0000000..35ecbf1 --- /dev/null +++ b/src/app/(dashboard)/mockup/page.tsx @@ -0,0 +1,100 @@ +'use client' +import { useState } from 'react' +import { Header } from '@/components/layout/Header' +import { TemplateLibrary } from '@/components/mockup/TemplateLibrary' +import { MockupGenerator } from '@/components/mockup/MockupGenerator' + +type Tab = 'generate' | 'library' + +export default function MockupPage() { + const [tab, setTab] = useState('generate') + + const [shopifyId, setShopifyId] = useState('') + const [productName, setProductName] = useState('') + const [family, setFamily] = useState('Menuiserie') + const [dimensions, setDimensions] = useState('') + const [cutoutB64, setCutoutB64] = useState(null) + + const handleFile = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + const reader = new FileReader() + reader.onload = ev => { + const result = ev.target?.result as string + setCutoutB64(result.split(',')[1]) + } + reader.readAsDataURL(file) + } + + return ( + <> +
+ {(['generate', 'library'] as Tab[]).map(t => ( + + ))} + + } /> + +
+ {tab === 'library' ? ( + + ) : ( +
+
+

Produit

+
+
+ + setProductName(e.target.value)} + className="w-full bg-slate-700 border border-slate-600 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" /> +
+
+ + setShopifyId(e.target.value)} + className="w-full bg-slate-700 border border-slate-600 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" /> +
+
+ + +
+
+ + setDimensions(e.target.value)} + placeholder="optionnel" + className="w-full bg-slate-700 border border-slate-600 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" /> +
+
+
+ + +
+
+ + {cutoutB64 ? ( + + ) : ( +

Sélectionnez une image découpée pour commencer.

+ )} +
+ )} +
+ + ) +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 916d7e9..b698544 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -16,6 +16,7 @@ const NAV_ITEMS: NavItem[] = [ { href: '/images', label: 'Images', icon: '🖼️' }, { href: '/sync', label: 'Sync Sheets', icon: '🔄' }, { href: '/creation', label: 'Création', icon: '✨' }, + { href: '/mockup', label: 'Mockups', icon: '🖼️' }, { href: '/parametres', label: 'Paramètres', icon: '⚙️', adminOnly: true }, ] diff --git a/src/components/mockup/TemplateLibrary.tsx b/src/components/mockup/TemplateLibrary.tsx new file mode 100644 index 0000000..52f1f25 --- /dev/null +++ b/src/components/mockup/TemplateLibrary.tsx @@ -0,0 +1,144 @@ +'use client' +import { useState, useEffect, useRef } from 'react' +import type { MockupTemplate } from '@/types/mockup' + +const FAMILIES = ['Menuiserie', 'Baies vitrées', 'Carrelage', 'Tous'] + +export function TemplateLibrary() { + const [templates, setTemplates] = useState([]) + const [loading, setLoading] = useState(true) + const [uploading, setUploading] = useState(false) + const [error, setError] = useState(null) + const [name, setName] = useState('') + const [family, setFamily] = useState('Menuiserie') + const [description, setDescription] = useState('') + const fileRef = useRef(null) + + const load = () => { + setLoading(true) + fetch('/api/mockup/templates') + .then(r => r.json()) + .then(setTemplates) + .finally(() => setLoading(false)) + } + + useEffect(load, []) + + const upload = async (e: React.FormEvent) => { + e.preventDefault() + const file = fileRef.current?.files?.[0] + if (!file || !name || !family) return + setUploading(true) + setError(null) + try { + const form = new FormData() + form.append('name', name) + form.append('family', family) + form.append('description', description) + form.append('image', file) + const res = await fetch('/api/mockup/templates', { method: 'POST', body: form }) + if (!res.ok) throw new Error((await res.json()).error) + setName(''); setDescription('') + if (fileRef.current) fileRef.current.value = '' + load() + } catch (e) { + setError(e instanceof Error ? e.message : 'Erreur') + } finally { + setUploading(false) + } + } + + const remove = async (id: string) => { + await fetch(`/api/mockup/templates/${id}`, { method: 'DELETE' }) + load() + } + + const byFamily = FAMILIES.reduce>((acc, f) => { + acc[f] = templates.filter(t => t.family === f) + return acc + }, {}) + + return ( +
+
+

Ajouter un template

+
+
+
+ + setName(e.target.value)} + placeholder="ex: Entrée moderne" + className="w-full bg-slate-700 border border-slate-600 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" + /> +
+
+ + +
+
+
+ + setDescription(e.target.value)} + placeholder="ex: Pièce lumineuse avec parquet" + className="w-full bg-slate-700 border border-slate-600 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" + /> +
+
+ + +
+ {error &&

{error}

} + +
+
+ + {loading ? ( +

Chargement…

+ ) : ( + FAMILIES.map(f => byFamily[f]?.length > 0 ? ( +
+

{f}

+
+ {byFamily[f].map(t => ( +
+ {t.name} +
+

{t.name}

+ {t.description &&

{t.description}

} +
+ +
+ ))} +
+
+ ) : null) + )} +
+ ) +}