feat: MockupGenerator UI component with generate/regenerate/upload flow
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import type { MockupTemplate } from '@/types/mockup'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
productCutoutBase64: string
|
||||||
|
productName: string
|
||||||
|
shopifyId?: string
|
||||||
|
family: string
|
||||||
|
dimensions?: string
|
||||||
|
onUploaded?: (url: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MockupGenerator({ productCutoutBase64, productName, shopifyId, family, dimensions, onUploaded }: Props) {
|
||||||
|
const [templates, setTemplates] = useState<MockupTemplate[]>([])
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null)
|
||||||
|
const [instruction, setInstruction] = useState('')
|
||||||
|
const [generating, setGenerating] = useState(false)
|
||||||
|
const [resultB64, setResultB64] = useState<string | null>(null)
|
||||||
|
const [uploading, setUploading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [uploadedUrl, setUploadedUrl] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/mockup/templates')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then((data: MockupTemplate[]) => {
|
||||||
|
const filtered = data.filter(t => !family || t.family === family || t.family === 'Tous')
|
||||||
|
setTemplates(filtered.length > 0 ? filtered : data)
|
||||||
|
})
|
||||||
|
}, [family])
|
||||||
|
|
||||||
|
const generate = async () => {
|
||||||
|
if (!selectedTemplate) 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: productCutoutBase64,
|
||||||
|
productName,
|
||||||
|
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 || !shopifyId) return
|
||||||
|
setUploading(true)
|
||||||
|
try {
|
||||||
|
const filename = `${productName.toLowerCase().replace(/\s+/g, '-')}-mockup.jpg`
|
||||||
|
const res = await fetch('/api/images/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ shopifyProductId: shopifyId, imageBase64: resultB64, filename }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
if (!res.ok) throw new Error(data.error)
|
||||||
|
setUploadedUrl(data.url)
|
||||||
|
onUploaded?.(data.url)
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Erreur upload')
|
||||||
|
} finally {
|
||||||
|
setUploading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-gray-400 mb-2">Template de mise en ambiance</label>
|
||||||
|
{templates.length === 0 ? (
|
||||||
|
<p className="text-xs text-gray-500">Aucun template disponible — ajoutez-en dans la bibliothèque.</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{templates.map(t => (
|
||||||
|
<button
|
||||||
|
key={t.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedTemplate(t.id)}
|
||||||
|
className={`relative rounded-lg overflow-hidden border-2 transition-all ${selectedTemplate === t.id ? 'border-indigo-500' : 'border-slate-600 hover:border-slate-400'}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`/api/mockup/templates/image/${t.filename}`}
|
||||||
|
alt={t.name}
|
||||||
|
className="w-full h-24 object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 bg-black/60 px-2 py-1">
|
||||||
|
<p className="text-xs text-white truncate">{t.name}</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{dimensions ? (
|
||||||
|
<p className="text-xs text-green-400">Dimensions : {dimensions}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-amber-400">Dimensions non renseignées — le mockup peut ne pas être à l'échelle.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={generate}
|
||||||
|
disabled={!selectedTemplate || generating}
|
||||||
|
className="w-full py-2 px-4 rounded-lg bg-indigo-600 text-white text-sm font-medium disabled:opacity-40 hover:bg-indigo-500 transition-colors"
|
||||||
|
>
|
||||||
|
{generating ? 'Génération en cours…' : 'Générer le mockup'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{error && <p className="text-xs text-red-400">{error}</p>}
|
||||||
|
|
||||||
|
{resultB64 && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<img
|
||||||
|
src={`data:image/jpeg;base64,${resultB64}`}
|
||||||
|
alt="Mockup généré"
|
||||||
|
className="w-full rounded-lg border border-slate-600"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={instruction}
|
||||||
|
onChange={e => setInstruction(e.target.value)}
|
||||||
|
placeholder="Instruction supplémentaire (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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={generate}
|
||||||
|
disabled={generating}
|
||||||
|
className="px-3 py-1.5 rounded bg-slate-600 text-xs text-white hover:bg-slate-500 disabled:opacity-40"
|
||||||
|
>
|
||||||
|
{generating ? '…' : '↺ Regénérer'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{uploadedUrl ? (
|
||||||
|
<p className="text-xs text-green-400">✓ Uploadé sur Shopify</p>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={upload}
|
||||||
|
disabled={uploading || !shopifyId}
|
||||||
|
className="w-full py-2 px-4 rounded-lg bg-emerald-600 text-white text-sm font-medium disabled:opacity-40 hover:bg-emerald-500 transition-colors"
|
||||||
|
>
|
||||||
|
{uploading ? 'Upload…' : shopifyId ? '↑ Uploader sur Shopify' : 'Produit Shopify requis pour uploader'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user