feat: placement mur/sol pour templates carrelage avec prompt adapté

This commit is contained in:
2026-06-24 14:52:57 +02:00
parent e85314eb77
commit cdde567558
5 changed files with 104 additions and 31 deletions
+8 -1
View File
@@ -6,10 +6,17 @@ import { getTemplateImagePath, listTemplates } from '@/lib/mockupTemplates'
import type { MockupGenerateRequest } from '@/types/mockup'
function buildPrompt(req: MockupGenerateRequest): string {
let carrelageContext = 'tiles covering the floor or wall of a modern room'
if (req.family === 'Carrelage' && req.placement === 'mur') {
carrelageContext = 'wall tiles (faïence) covering the wall of a modern bathroom or kitchen — applied only to the wall, not the floor'
} else if (req.family === 'Carrelage' && req.placement === 'sol') {
carrelageContext = 'floor tiles covering the floor of a modern room — applied only to the floor, not the walls'
}
const familyContext: Record<string, string> = {
'Menuiserie': 'door installed in a residential entrance or hallway, visible wall and floor context',
'Baies vitrées': 'bay window or sliding door installed in a living room with garden view',
'Carrelage': 'tiles covering the floor or wall of a modern room',
'Carrelage': carrelageContext,
}
const context = familyContext[req.family] ?? 'product installed in an appropriate room setting'
const dimClause = req.dimensions
+2 -1
View File
@@ -17,6 +17,7 @@ export async function POST(req: NextRequest) {
const form = await req.formData()
const name = form.get('name') as string
const family = form.get('family') as string
const placement = (form.get('placement') as string) || undefined
const description = (form.get('description') as string) ?? ''
const imageFile = form.get('image') as File
@@ -26,6 +27,6 @@ export async function POST(req: NextRequest) {
const buffer = Buffer.from(await imageFile.arrayBuffer())
const ext = imageFile.name.split('.').pop() ?? 'jpg'
const template = await saveTemplate({ name, family, description, filename: '' }, buffer, ext)
const template = await saveTemplate({ name, family, placement, description, filename: '' }, buffer, ext)
return Response.json(template)
}
+26 -5
View File
@@ -35,6 +35,7 @@ export function MockupModal({ product, onClose }: Props) {
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null)
const [family, setFamily] = useState('')
const [placement, setPlacement] = useState<string>('')
const [selectedMeta, setSelectedMeta] = useState<string[]>([])
const [cutoutB64, setCutoutB64] = useState<string | null>(null)
const [selectedImageUrl, setSelectedImageUrl] = useState<string | null>(null)
@@ -67,9 +68,13 @@ export function MockupModal({ product, onClose }: Props) {
}).finally(() => setLoadingMeta(false))
}, [product.shopifyId])
const filteredTemplates = templates.filter(t =>
!family || t.family === family || t.family === 'Tous'
)
const isCarrelage = family.toLowerCase().includes('carrelage')
const filteredTemplates = templates.filter(t => {
if (t.family !== family && t.family !== 'Tous') return false
if (isCarrelage && placement && t.placement && t.placement !== placement) return false
return true
})
const dimensionStr = selectedMeta.length > 0
? selectedMeta.map(key => {
@@ -137,6 +142,7 @@ export function MockupModal({ product, onClose }: Props) {
productImageBase64: cutoutB64,
productName: product.title,
family: family || 'Tous',
placement: placement || undefined,
dimensions: dimensionStr,
instruction: instruction || undefined,
}),
@@ -218,14 +224,29 @@ export function MockupModal({ product, onClose }: Props) {
)}
</section>
{/* 2. Famille */}
{/* 2. Famille + placement */}
<section>
<h3 className="text-xs font-semibold text-gray-300 uppercase tracking-wide mb-2">2. Famille</h3>
{families.length > 0 ? (
<select value={family} onChange={e => setFamily(e.target.value)}
<div className="space-y-2">
<select value={family} onChange={e => { setFamily(e.target.value); setPlacement('') }}
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">
{families.map(f => <option key={f}>{f}</option>)}
</select>
{isCarrelage && (
<div>
<p className="text-xs text-gray-400 mb-1.5">Placement</p>
<div className="flex gap-2">
{[{ val: 'sol', label: 'Sol' }, { val: 'mur', label: 'Mur (faïence)' }].map(({ val, label }) => (
<button key={val} type="button" onClick={() => setPlacement(p => p === val ? '' : val)}
className={`px-3 py-1.5 rounded text-xs font-medium transition-colors ${placement === val ? 'bg-indigo-600 text-white' : 'bg-slate-700 text-gray-300 hover:bg-slate-600'}`}>
{label}
</button>
))}
</div>
</div>
)}
</div>
) : (
<p className="text-xs text-gray-500">{loadingMeta ? 'Chargement…' : 'Aucune famille disponible'}</p>
)}
+52 -10
View File
@@ -2,6 +2,8 @@
import { useState, useEffect, useRef } from 'react'
import type { MockupTemplate } from '@/types/mockup'
const CARRELAGE_PLACEMENTS = ['sol', 'mur']
export function TemplateLibrary() {
const [templates, setTemplates] = useState<MockupTemplate[]>([])
const [families, setFamilies] = useState<string[]>(['Tous'])
@@ -10,6 +12,7 @@ export function TemplateLibrary() {
const [error, setError] = useState<string | null>(null)
const [name, setName] = useState('')
const [family, setFamily] = useState('Tous')
const [placement, setPlacement] = useState<string>('')
const [description, setDescription] = useState('')
const fileRef = useRef<HTMLInputElement>(null)
@@ -31,10 +34,13 @@ export function TemplateLibrary() {
useEffect(load, [])
const isCarrelage = family.toLowerCase().includes('carrelage')
const upload = async (e: React.FormEvent) => {
e.preventDefault()
const file = fileRef.current?.files?.[0]
if (!file || !name || !family) return
if (isCarrelage && !placement) return
setUploading(true)
setError(null)
try {
@@ -42,10 +48,11 @@ export function TemplateLibrary() {
form.append('name', name)
form.append('family', family)
form.append('description', description)
if (isCarrelage && placement) form.append('placement', placement)
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('')
setName(''); setDescription(''); setPlacement('')
if (fileRef.current) fileRef.current.value = ''
load()
} catch (e) {
@@ -63,6 +70,16 @@ export function TemplateLibrary() {
const allUsedFamilies = Array.from(new Set(templates.map(t => t.family)))
const displayFamilies = families.filter(f => allUsedFamilies.includes(f) || f === 'Tous')
const groupedTemplates = (familyName: string) => {
const byFamily = templates.filter(t => t.family === familyName)
const isCarr = familyName.toLowerCase().includes('carrelage')
if (!isCarr) return [{ label: null, items: byFamily }]
return CARRELAGE_PLACEMENTS.map(p => ({
label: p === 'mur' ? 'Mur (faïence)' : 'Sol',
items: byFamily.filter(t => t.placement === p),
})).filter(g => g.items.length > 0)
}
return (
<div className="space-y-6">
{/* Upload form */}
@@ -73,21 +90,36 @@ export function TemplateLibrary() {
<div>
<label className="block text-xs text-gray-400 mb-1">Nom</label>
<input value={name} onChange={e => setName(e.target.value)}
placeholder="ex: Entrée moderne"
placeholder="ex: Salle de bain 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" />
</div>
<div>
<label className="block text-xs text-gray-400 mb-1">Famille</label>
<select value={family} onChange={e => setFamily(e.target.value)}
<select value={family} onChange={e => { setFamily(e.target.value); setPlacement('') }}
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">
{families.map(f => <option key={f}>{f}</option>)}
</select>
</div>
</div>
{isCarrelage && (
<div>
<label className="block text-xs text-gray-400 mb-1">Placement <span className="text-red-400">*</span></label>
<div className="flex gap-2">
{CARRELAGE_PLACEMENTS.map(p => (
<button key={p} type="button" onClick={() => setPlacement(p)}
className={`px-4 py-1.5 rounded text-sm font-medium transition-colors ${placement === p ? 'bg-indigo-600 text-white' : 'bg-slate-700 text-gray-300 hover:bg-slate-600'}`}>
{p === 'mur' ? 'Mur (faïence)' : 'Sol'}
</button>
))}
</div>
</div>
)}
<div>
<label className="block text-xs text-gray-400 mb-1">Description (optionnel)</label>
<input value={description} onChange={e => setDescription(e.target.value)}
placeholder="ex: Pièce lumineuse avec parquet"
placeholder="ex: Salle de bain lumineuse"
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" />
</div>
<div>
@@ -95,7 +127,7 @@ export function TemplateLibrary() {
<input ref={fileRef} type="file" accept="image/*" className="text-sm text-gray-300" />
</div>
{error && <p className="text-xs text-red-400">{error}</p>}
<button type="submit" disabled={uploading || !name || !family}
<button type="submit" disabled={uploading || !name || !family || (isCarrelage && !placement)}
className="px-4 py-2 bg-indigo-600 text-white text-sm rounded-lg disabled:opacity-40 hover:bg-indigo-500">
{uploading ? 'Ajout…' : '+ Ajouter'}
</button>
@@ -110,16 +142,23 @@ export function TemplateLibrary() {
) : (
<div className="space-y-6">
{displayFamilies.map(f => {
const byFamily = templates.filter(t => t.family === f)
if (byFamily.length === 0) return null
const groups = groupedTemplates(f)
const total = groups.reduce((n, g) => n + g.items.length, 0)
if (total === 0) return null
return (
<div key={f}>
<h3 className="text-sm font-medium text-gray-300 mb-2 flex items-center gap-2">
<h3 className="text-sm font-medium text-gray-300 mb-3 flex items-center gap-2">
{f}
<span className="text-xs text-gray-500">{byFamily.length} template{byFamily.length > 1 ? 's' : ''}</span>
<span className="text-xs text-gray-500">{total} template{total > 1 ? 's' : ''}</span>
</h3>
<div className="space-y-4">
{groups.map((group, gi) => (
<div key={gi}>
{group.label && (
<p className="text-xs text-gray-400 font-medium mb-2 uppercase tracking-wide">{group.label}</p>
)}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{byFamily.map(t => (
{group.items.map(t => (
<div key={t.id} className="relative rounded-lg overflow-hidden border border-slate-600 group">
<img src={`/api/mockup/templates/image/${t.filename}`} alt={t.name}
className="w-full h-32 object-cover" />
@@ -135,6 +174,9 @@ export function TemplateLibrary() {
))}
</div>
</div>
))}
</div>
</div>
)
})}
</div>
+2
View File
@@ -2,6 +2,7 @@ export interface MockupTemplate {
id: string
name: string
family: string
placement?: string
filename: string
description: string
}
@@ -12,6 +13,7 @@ export interface MockupGenerateRequest {
productName: string
dimensions?: string
family: string
placement?: string
instruction?: string
}