feat: placement mur/sol pour templates carrelage avec prompt adapté
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
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 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>
|
||||
)}
|
||||
|
||||
@@ -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,27 +142,37 @@ 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="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{byFamily.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" />
|
||||
<div className="p-2 bg-slate-800">
|
||||
<p className="text-xs text-white font-medium truncate">{t.name}</p>
|
||||
{t.description && <p className="text-xs text-gray-400 truncate">{t.description}</p>}
|
||||
<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">
|
||||
{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" />
|
||||
<div className="p-2 bg-slate-800">
|
||||
<p className="text-xs text-white font-medium truncate">{t.name}</p>
|
||||
{t.description && <p className="text-xs text-gray-400 truncate">{t.description}</p>}
|
||||
</div>
|
||||
<button type="button" onClick={() => remove(t.id)}
|
||||
className="absolute top-1 right-1 bg-red-600/80 hover:bg-red-500 text-white text-xs rounded px-1.5 py-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button type="button" onClick={() => remove(t.id)}
|
||||
className="absolute top-1 right-1 bg-red-600/80 hover:bg-red-500 text-white text-xs rounded px-1.5 py-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user