feat: add background image upload to banner builder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 10:46:45 +02:00
parent 461667c506
commit 3ec0b9fa3c
2 changed files with 77 additions and 5 deletions
+54 -3
View File
@@ -48,6 +48,9 @@ export function BannerBuilder({ onApply, onClose }: Props) {
const [price, setPrice] = useState('') const [price, setPrice] = useState('')
const [comparePrice, setComparePrice] = useState('') const [comparePrice, setComparePrice] = useState('')
const [selectedImageUrl, setSelectedImageUrl] = useState<string | null>(null) const [selectedImageUrl, setSelectedImageUrl] = useState<string | null>(null)
const [backgroundBlob, setBackgroundBlob] = useState<Blob | null>(null)
const [backgroundPreview, setBackgroundPreview] = useState<string | null>(null)
const bgFileRef = useRef<HTMLInputElement>(null)
// Generation // Generation
const [rendering, setRendering] = useState(false) const [rendering, setRendering] = useState(false)
@@ -112,6 +115,22 @@ export function BannerBuilder({ onApply, onClose }: Props) {
} }
} }
const handleBgFile = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
setBackgroundBlob(file)
const url = URL.createObjectURL(file)
if (backgroundPreview) URL.revokeObjectURL(backgroundPreview)
setBackgroundPreview(url)
e.target.value = ''
}
const removeBg = () => {
if (backgroundPreview) URL.revokeObjectURL(backgroundPreview)
setBackgroundBlob(null)
setBackgroundPreview(null)
}
const getConfig = useCallback((): BannerConfig => ({ const getConfig = useCallback((): BannerConfig => ({
scheme, scheme,
promoLabel, promoLabel,
@@ -119,7 +138,8 @@ export function BannerBuilder({ onApply, onClose }: Props) {
price, price,
comparePrice, comparePrice,
productImageUrl: selectedImageUrl, productImageUrl: selectedImageUrl,
}), [scheme, promoLabel, title, price, comparePrice, selectedImageUrl]) backgroundImageBlob: backgroundBlob,
}), [scheme, promoLabel, title, price, comparePrice, selectedImageUrl, backgroundBlob])
const generate = useCallback(async () => { const generate = useCallback(async () => {
setRendering(true) setRendering(true)
@@ -142,7 +162,7 @@ export function BannerBuilder({ onApply, onClose }: Props) {
if (!title && !product) return if (!title && !product) return
const t = setTimeout(() => { generate() }, 600) const t = setTimeout(() => { generate() }, 600)
return () => clearTimeout(t) return () => clearTimeout(t)
}, [scheme, promoLabel, title, price, comparePrice, selectedImageUrl]) // eslint-disable-line react-hooks/exhaustive-deps }, [scheme, promoLabel, title, price, comparePrice, selectedImageUrl, backgroundBlob]) // eslint-disable-line react-hooks/exhaustive-deps
const applyToSlide = async () => { const applyToSlide = async () => {
if (!previewRef.current) return if (!previewRef.current) return
@@ -248,9 +268,40 @@ export function BannerBuilder({ onApply, onClose }: Props) {
)} )}
</section> </section>
{/* Image de fond */}
<section>
<h3 className="text-xs font-semibold text-gray-300 uppercase tracking-wide mb-2">Image de fond</h3>
{backgroundPreview ? (
<div className="relative rounded overflow-hidden border border-slate-600">
<img src={backgroundPreview} alt="Fond" className="w-full h-24 object-cover" />
<button
type="button"
onClick={removeBg}
className="absolute top-1 right-1 bg-black/70 hover:bg-red-900 text-white text-xs px-1.5 py-0.5 rounded"
>
Retirer
</button>
</div>
) : (
<button
type="button"
onClick={() => bgFileRef.current?.click()}
className="w-full py-3 border-2 border-dashed border-slate-600 hover:border-indigo-500 rounded text-xs text-slate-400 hover:text-indigo-400 transition-colors"
>
+ Uploader une image de fond
</button>
)}
<input ref={bgFileRef} type="file" accept="image/*" className="hidden" onChange={handleBgFile} />
{backgroundPreview && (
<p className="text-xs text-slate-500 mt-1">Un overlay sombre est appliqué automatiquement pour la lisibilité du texte.</p>
)}
</section>
{/* Thème couleur */} {/* Thème couleur */}
<section> <section>
<h3 className="text-xs font-semibold text-gray-300 uppercase tracking-wide mb-2">Thème couleur</h3> <h3 className="text-xs font-semibold text-gray-300 uppercase tracking-wide mb-2">
{backgroundPreview ? 'Couleur du texte' : 'Thème couleur'}
</h3>
<div className="grid grid-cols-2 gap-1.5"> <div className="grid grid-cols-2 gap-1.5">
{schemeEntries.map(([key, s]) => ( {schemeEntries.map(([key, s]) => (
<button <button
+23 -2
View File
@@ -113,6 +113,7 @@ export interface BannerConfig {
price: string // ex: "79 €" price: string // ex: "79 €"
comparePrice: string // ex: "119 €" (barré) comparePrice: string // ex: "119 €" (barré)
productImageUrl: string | null productImageUrl: string | null
backgroundImageBlob?: Blob | null // image de fond uploadée (remplace la couleur unie)
} }
export async function renderBanner(config: BannerConfig): Promise<Blob> { export async function renderBanner(config: BannerConfig): Promise<Blob> {
@@ -123,8 +124,28 @@ export async function renderBanner(config: BannerConfig): Promise<Blob> {
const scheme = COLOR_SCHEMES[config.scheme] const scheme = COLOR_SCHEMES[config.scheme]
// ── Fond ───────────────────────────────────────────────────────────────── // ── Fond ─────────────────────────────────────────────────────────────────
ctx.fillStyle = scheme.bg if (config.backgroundImageBlob) {
ctx.fillRect(0, 0, W, H) try {
const bgImg = await createImageBitmap(config.backgroundImageBlob)
// Cover : remplit tout le canvas en centrant
const scale = Math.max(W / bgImg.width, H / bgImg.height)
const dw = bgImg.width * scale
const dh = bgImg.height * scale
const dx = (W - dw) / 2
const dy = (H - dh) / 2
ctx.drawImage(bgImg, dx, dy, dw, dh)
bgImg.close()
// Overlay semi-transparent pour lisibilité du texte
ctx.fillStyle = 'rgba(0,0,0,0.35)'
ctx.fillRect(0, 0, W, H)
} catch {
ctx.fillStyle = scheme.bg
ctx.fillRect(0, 0, W, H)
}
} else {
ctx.fillStyle = scheme.bg
ctx.fillRect(0, 0, W, H)
}
// Zone texte : 55% gauche, zone image : 45% droite // Zone texte : 55% gauche, zone image : 45% droite
const textZoneW = Math.round(W * 0.55) const textZoneW = Math.round(W * 0.55)