From 41f5c245d46e3ae1acd04d02b8b913143aaa68bc Mon Sep 17 00:00:00 2001 From: Mathieu Date: Wed, 24 Jun 2026 14:20:42 +0200 Subject: [PATCH] feat: mockup modal accessible depuis la liste produits --- src/components/mockup/MockupModal.tsx | 210 +++++++++++++++++++++++ src/components/products/ProductTable.tsx | 20 ++- 2 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 src/components/mockup/MockupModal.tsx diff --git a/src/components/mockup/MockupModal.tsx b/src/components/mockup/MockupModal.tsx new file mode 100644 index 0000000..5216615 --- /dev/null +++ b/src/components/mockup/MockupModal.tsx @@ -0,0 +1,210 @@ +'use client' +import { useState, useEffect, useRef } from 'react' +import type { CatalogProduct } from '@/types/products' +import type { MockupTemplate } from '@/types/mockup' + +interface Props { + product: CatalogProduct + onClose: () => void +} + +const FAMILIES = ['Menuiserie', 'Baies vitrées', 'Carrelage', 'Tous'] + +export function MockupModal({ product, onClose }: Props) { + const [templates, setTemplates] = useState([]) + const [selectedTemplate, setSelectedTemplate] = useState(null) + const [family, setFamily] = useState('Menuiserie') + const [dimensions, setDimensions] = useState('') + const [cutoutB64, setCutoutB64] = useState(null) + const [instruction, setInstruction] = useState('') + const [generating, setGenerating] = useState(false) + const [resultB64, setResultB64] = useState(null) + const [uploading, setUploading] = useState(false) + const [uploadedUrl, setUploadedUrl] = useState(null) + const [error, setError] = useState(null) + const fileRef = useRef(null) + const templateFileRef = useRef(null) + const [uploadingTemplate, setUploadingTemplate] = useState(false) + + useEffect(() => { + fetch('/api/mockup/templates') + .then(r => r.json()) + .then((data: MockupTemplate[]) => setTemplates(data)) + }, []) + + const filtered = templates.filter(t => t.family === family || t.family === 'Tous') + + const handleCutout = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + const reader = new FileReader() + reader.onload = ev => setCutoutB64((ev.target?.result as string).split(',')[1]) + reader.readAsDataURL(file) + } + + const uploadTemplate = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + const name = file.name.replace(/\.[^.]+$/, '') + setUploadingTemplate(true) + const form = new FormData() + form.append('name', name) + form.append('family', family) + form.append('description', '') + form.append('image', file) + const res = await fetch('/api/mockup/templates', { method: 'POST', body: form }) + const tpl = await res.json() as MockupTemplate + setTemplates(prev => [...prev, tpl]) + setSelectedTemplate(tpl.id) + setUploadingTemplate(false) + } + + const generate = async () => { + if (!selectedTemplate || !cutoutB64) return + setGenerating(true) + setError(null) + setResultB64(null) + try { + const res = await fetch('/api/mockup/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + templateId: selectedTemplate, + productImageBase64: cutoutB64, + productName: product.title, + family, + dimensions: dimensions || undefined, + instruction: instruction || undefined, + }), + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error) + setResultB64(data.imageBase64) + } catch (e) { + setError(e instanceof Error ? e.message : 'Erreur') + } finally { + setGenerating(false) + } + } + + const upload = async () => { + if (!resultB64) return + setUploading(true) + try { + const filename = `${product.ref}-mockup.jpg` + const res = await fetch('/api/images/upload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ shopifyProductId: product.shopifyId, imageBase64: resultB64, filename }), + }) + const data = await res.json() + if (!res.ok) throw new Error(data.error) + setUploadedUrl(data.url) + } catch (e) { + setError(e instanceof Error ? e.message : 'Erreur upload') + } finally { + setUploading(false) + } + } + + return ( +
{ if (e.target === e.currentTarget) onClose() }}> +
+ {/* Header */} +
+
+

Créer un mockup

+

{product.title} {product.ref}

+
+ +
+ +
+ {/* Family + dimensions */} +
+
+ + +
+
+ + 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" /> +
+
+ + {/* Template selection */} +
+
+ + + +
+ {filtered.length === 0 ? ( +

Aucun template pour cette famille. Uploadez-en un ci-dessus.

+ ) : ( +
+ {filtered.map(t => ( + + ))} +
+ )} +
+ + {/* Product cutout */} +
+ + + {cutoutB64 &&

✓ Image chargée

} +
+ + {/* Generate */} + + + {error &&

{error}

} + + {/* Result */} + {resultB64 && ( +
+ Mockup +
+ setInstruction(e.target.value)} + placeholder="Instruction pour regénérer (optionnel)…" + className="flex-1 bg-slate-700 border border-slate-600 rounded px-2 py-1.5 text-xs text-white focus:outline-none focus:ring-1 focus:ring-indigo-500" /> + +
+ {uploadedUrl ? ( +

✓ Uploadé sur Shopify

+ ) : ( + + )} +
+ )} +
+
+
+ ) +} diff --git a/src/components/products/ProductTable.tsx b/src/components/products/ProductTable.tsx index 05d91dc..eaaa038 100644 --- a/src/components/products/ProductTable.tsx +++ b/src/components/products/ProductTable.tsx @@ -1,5 +1,7 @@ 'use client' +import { useState } from 'react' import type { CatalogProduct } from '@/types/products' +import { MockupModal } from '@/components/mockup/MockupModal' interface Props { products: CatalogProduct[] @@ -9,6 +11,7 @@ interface Props { const PAGE_SIZE = 50 export function ProductTable({ products, loading }: Props) { + const [mockupProduct, setMockupProduct] = useState(null) if (loading) { return (
@@ -37,7 +40,8 @@ export function ProductTable({ products, loading }: Props) { Prix Stock Statut - Lien + Lien + @@ -56,7 +60,7 @@ export function ProductTable({ products, loading }: Props) { {p.status === 'active' ? 'Actif' : 'Inactif'} - + + + + ))} + {mockupProduct && ( + setMockupProduct(null)} /> + )} {products.length > PAGE_SIZE && (

Affichage des {PAGE_SIZE} premiers résultats sur {products.length}.