import React, { useEffect, useRef, useState } from 'react' import { createRoot } from 'react-dom/client' import { ArrowLeft, ArrowRight, Check, ChevronRight, ClipboardList, Edit3, Heart, LayoutDashboard, Menu, Package, Palette, Plus, Search, Settings, ShoppingBag, Sparkles, Trash2, X } from 'lucide-react' import './styles.css' import { api, API_URL, AUTH_EXPIRED_EVENT } from './api' const initialCategories = [ { id: 'canecas', name: 'Canecas', emoji: '☕', color: '#ffe8d8' }, { id: 'garrafas', name: 'Garrafas', emoji: '🧴', color: '#e4efff' }, { id: 'imas', name: 'Ímãs de geladeira', emoji: '🧲', color: '#e4f6e9' }, { id: 'kits', name: 'Kits presente', emoji: '🎁', color: '#f2e8ff' } ] const initialProducts = [ { id: 'caneca-classica', category: 'canecas', name: 'Caneca cerâmica clássica', description: 'Caneca branca de cerâmica, impressão frente e verso.', price: 39.9, oldPrice: 49.9, image: 'https://images.unsplash.com/photo-1514228742587-6b1558fcca3d?auto=format&fit=crop&w=900&q=85', badge: 'Mais vendida' }, { id: 'caneca-colorida', category: 'canecas', name: 'Caneca com interior colorido', description: 'Escolha sua cor favorita e crie algo único.', price: 44.9, image: 'https://images.unsplash.com/photo-1577937927133-66ef06acdf18?auto=format&fit=crop&w=900&q=85', badge: 'Novidade' }, { id: 'garrafa-termica', category: 'garrafas', name: 'Garrafa térmica 500ml', description: 'Aço inoxidável, mantém a bebida na temperatura ideal.', price: 79.9, oldPrice: 99.9, image: 'https://images.unsplash.com/photo-1602143407151-7111542de6e8?auto=format&fit=crop&w=900&q=85', badge: 'Oferta' }, { id: 'ima-polaroid', category: 'imas', name: 'Ímã estilo polaroid', description: 'Transforme seus melhores momentos em lembranças.', price: 12.9, image: 'https://images.unsplash.com/photo-1500534623283-312aade485b7?auto=format&fit=crop&w=900&q=85', badge: '' }, { id: 'kit-afeto', category: 'kits', name: 'Kit afeto personalizado', description: 'Caneca, cartão e embalagem especial para presentear.', price: 89.9, image: 'https://images.unsplash.com/photo-1513883049090-d0b7439799bf?auto=format&fit=crop&w=900&q=85', badge: 'Presenteie' } ] const initialArtworks = [ { id: 'amor', name: 'Amor em cada detalhe', category: 'Românticas', color: '#f9d4db', accent: '#e86d83', texts: ['Meu amor', 'é você'] }, { id: 'minimal', name: 'Frase minimalista', category: 'Frases', color: '#e7e2d7', accent: '#2c2c2c', texts: ['Faça', 'acontecer'] }, { id: 'sun', name: 'Good vibes', category: 'Alegres', color: '#ffe5a6', accent: '#ef8b37', texts: ['Good', 'vibes'] }, { id: 'family', name: 'Família', category: 'Afeto', color: '#d9eddf', accent: '#4f8f69', texts: ['Lar é onde', 'o amor está'] } ] const TEXT_FIELD_IDS = ['texto_1', 'texto_2', 'texto_3', 'texto_4'] const LEGACY_TEXT_FIELD_IDS = ['texto-superior', 'frase-principal', 'frase-secundaria', 'nome-personalizavel'] const FONT_OPTIONS = ['Georgia', 'Licorice', 'Indie Flower', 'Playfair Display', 'DM Sans'] const FONT_SIZES = [24, 32, 40, 48, 56, 64, 72, 84, 96, 108] function textFieldIndex(id, fields) { const configuredIndex = fields.findIndex(field => field.id === id) if (configuredIndex >= 0) return configuredIndex return TEXT_FIELD_IDS.indexOf(id) >= 0 ? TEXT_FIELD_IDS.indexOf(id) : LEGACY_TEXT_FIELD_IDS.indexOf(id) } function load(key, fallback) { try { return JSON.parse(localStorage.getItem(key)) || fallback } catch { return fallback } } function normalizeProduct(product) { if (!product) return null const image = product.image || product.image_url return { ...product, category: product.category || product.category_slug || product.category_name, price: Number(product.price), oldPrice: product.oldPrice === undefined ? (product.old_price === null ? null : Number(product.old_price)) : Number(product.oldPrice), active: product.active === 0 || product.active === false ? false : true, image: image?.startsWith('/uploads/') ? `${API_URL.replace(/\/api$/, '')}${image}` : image, art_width_cm: Number(product.art_width_cm) || null, art_height_cm: Number(product.art_height_cm) || null ,category_ids: product.category_ids || (product.category_id ? [product.category_id] : []) } } function normalizeArtwork(artwork) { const fields = artwork.text_fields const texts = Array.isArray(fields) ? fields.map(field => field.default || '').slice(0, 4) : [] const fileUrl = artwork.file_url return { ...artwork, textFields: Array.isArray(fields) ? fields : [], file_url: fileUrl?.startsWith('/uploads/') ? `${API_URL.replace(/\/api$/, '')}${fileUrl}` : fileUrl, color: artwork.color || '#e8e2da', accent: artwork.accent || '#423b36', texts: texts.length ? texts : ['', '', '', ''], width_cm: Number(artwork.width_cm) || null, height_cm: Number(artwork.height_cm) || null } } function App() { const [page, setPage] = useState('home') const [products, setProducts] = useState(() => load('fp-products', initialProducts)) const [categories, setCategories] = useState(() => load('fp-categories', initialCategories)) const [artworks, setArtworks] = useState(() => load('fp-artworks', initialArtworks)) const [orders, setOrders] = useState(() => load('fp-orders', [])) const [cart, setCart] = useState(() => load('fp-cart', [])) const [selectedProduct, setSelectedProduct] = useState(null) const [selectedArtwork, setSelectedArtwork] = useState(null) const [search, setSearch] = useState('') const [adminOpen, setAdminOpen] = useState(false) useEffect(() => localStorage.setItem('fp-orders', JSON.stringify(orders)), [orders]) useEffect(() => localStorage.setItem('fp-cart', JSON.stringify(cart)), [cart]) useEffect(() => { Promise.all([api.getProducts(), api.getCategories(), api.getArtworks()]) .then(([remoteProducts, remoteCategories, remoteArtworks]) => { if (remoteProducts.length) setProducts(remoteProducts.map(normalizeProduct)) if (remoteCategories.length) setCategories(remoteCategories) if (remoteArtworks.length) setArtworks(remoteArtworks.map(normalizeArtwork)) }) .catch(error => console.warn('API indisponível; usando catálogo local:', error.message)) }, []) const navigate = (target) => { setPage(target); setAdminOpen(false); window.scrollTo(0, 0) } const openProduct = (product) => { const compatible = product.art_width_cm && product.art_height_cm ? artworks.filter(artwork => artwork.width_cm === Number(product.art_width_cm) && artwork.height_cm === Number(product.art_height_cm)) : artworks setSelectedProduct(product); setSelectedArtwork(compatible[0] || null); navigate('product') } const addToCart = (item) => setCart(current => [...current, { ...item, cartId: Date.now() }]) const removeFromCart = (id) => setCart(current => current.filter(item => item.cartId !== id)) const submitOrder = async (customer) => { try { const orderItems = cart.map(item => { const numericId = Number(item.productId) const catalogProduct = Number.isInteger(numericId) && numericId > 0 ? products.find(product => Number(product.id) === numericId) : products.find(product => product.name === item.name && Number.isInteger(Number(product.id))) if (!catalogProduct) throw new Error(`O produto "${item.name}" não está disponível no catálogo atual. Remova-o e adicione novamente.`) return { product_id: Number(catalogProduct.id), artwork_name: item.artwork, custom_text: item.customText, artwork_svg: item.artworkSvg || null, quantity: item.quantity } }) const order = await api.createOrder({ customer_name: customer.name, customer_email: customer.email, customer_phone: customer.phone, delivery_address: customer.address, items: orderItems }) setOrders(current => [{ id: order.order_code, date: new Date().toLocaleDateString('pt-BR'), status: 'Recebido', customer, items: cart, total: order.total }, ...current]) setCart([]); navigate('success') } catch (error) { window.alert(`Não foi possível enviar o pedido: ${error.message}`) } } const filteredProducts = products.filter(p => `${p.name} ${p.description}`.toLowerCase().includes(search.toLowerCase())) return
setAdminOpen(true)} /> {adminOpen ? setAdminOpen(false)} /> : <> {page === 'home' && } {page === 'categories' && } {page === 'product' && selectedProduct && } {page === 'create' && selectedProduct && } {page === 'cart' && } {page === 'success' && } }
} function Header({ page, navigate, cartCount, search, setSearch, onAdmin }) { return
} function Home({ products, categories, navigate, openProduct }) { const heroSlides = [ { id: 'bridix-1', eyebrow: 'Bridix personalizados', title: 'Peças únicas', accent: 'com identidade.', text: 'Crie presentes, lembranças e detalhes exclusivos que refletem sua personalidade em cada produto.', cta: 'Explorar coleção', badge: 'Bridix', accentIcon: '☕', cardText: 'Seu presente,\ndo seu jeito.', custom: 'feito para você ♡' }, { id: 'bridix-2', eyebrow: 'Presentes com sentimento', title: 'Tudo com alma', accent: 'e história.', text: 'Personalize canecas, garrafas e itens especiais para comemorar qualquer momento com charme e exclusividade.', cta: 'Ver produtos', badge: 'Mais vendidos', accentIcon: '🎁', cardText: 'Presente com\nmemória.', custom: 'feito para celebrar ♡' }, { id: 'bridix-3', eyebrow: 'Crie do seu jeito', title: 'Seu design', accent: 'na sua cara.', text: 'Escolha a arte, ajuste os textos e transforme cada item em uma peça que surpreende e emociona.', cta: 'Personalizar agora', badge: 'Custom', accentIcon: '✨', cardText: 'Seu estilo\nem cada detalhe.', custom: 'feito para você ♡' } ] const [currentSlide, setCurrentSlide] = useState(0) const [dragOffset, setDragOffset] = useState(0) const [dragging, setDragging] = useState(false) const dragStartX = useRef(0) const goToSlide = (nextIndex) => { setCurrentSlide((nextIndex + heroSlides.length) % heroSlides.length) } const handlePointerDown = (event) => { dragStartX.current = event.clientX setDragging(true) } const handlePointerMove = (event) => { if (!dragging) return setDragOffset(event.clientX - dragStartX.current) } const handlePointerEnd = () => { if (!dragging) return if (dragOffset < -60) { goToSlide(currentSlide + 1) } else if (dragOffset > 60) { goToSlide(currentSlide - 1) } setDragOffset(0) setDragging(false) } return
{heroSlides.map(slide =>
{slide.eyebrow}

{slide.title}
{slide.accent}

{slide.text}

BRI
+2.500 pedidos personalizados
entregues com estilo
{slide.badge}
{slide.accentIcon}{slide.cardText}{slide.custom}
) }
{heroSlides.map((slide, index) =>
navigate('categories')} />
{categories.map(category => )}
navigate('categories')} />
{products.slice(0, 4).map(product => )}
Do seu jeito

Tem coisa melhor que
presente com história?

Escolha uma arte, escreva sua mensagem e deixe a gente cuidar do resto.

As melhores coisas
são feitas à mão
e com o coração.

} function SectionTitle({ eyebrow, title, action, onAction }) { return
{eyebrow}

{title}

{action && }
} function ProductCard({ product, openProduct }) { return
openProduct(product)}>
{product.badge && {product.badge}}{product.name}
{product.category}

{product.name}

{product.description}

{product.oldPrice && R$ {product.oldPrice.toFixed(2).replace('.', ',')}}R$ {product.price.toFixed(2).replace('.', ',')}
} function Catalog({ products, categories, navigate, openProduct }) { return
Nosso catálogo

Produtos que contam
uma história.

Escolha um produto, uma arte e coloque sua personalidade em cada detalhe.

{categories.map(c => )}
{products.map(p => )}
{!products.length &&
Nenhum produto encontrado. Tente outra busca.
}
} function Product({ product, artworks, selectedArtwork, setSelectedArtwork, navigate, addToCart }) { const [quantity, setQuantity] = useState(1) const compatibleArtworks = product.art_width_cm && product.art_height_cm ? artworks.filter(art => Number(art.width_cm) === Number(product.art_width_cm) && Number(art.height_cm) === Number(product.art_height_cm)) : artworks return
{product.name}feito pra você ♡
{product.category}

{product.name}

{product.description}

R$ {product.price.toFixed(2).replace('.', ',')} à vista

01
Escolha uma arte{product.art_width_cm ? `Medida compatível: ${product.art_width_cm} × ${product.art_height_cm} cm` : 'Você poderá editar os textos no próximo passo'}
{compatibleArtworks.map(art => )}{!compatibleArtworks.length && Nenhuma arte cadastrada para este tamanho.}
02
QuantidadePersonalize quantas unidades quiser
{quantity}
} function artworkFields(artwork) { return artwork.textFields?.length ? artwork.textFields : [ { id: TEXT_FIELD_IDS[0], default: artwork.texts[0] }, { id: TEXT_FIELD_IDS[1], default: artwork.texts[1] }, { id: TEXT_FIELD_IDS[2], default: artwork.texts[2] }, { id: TEXT_FIELD_IDS[3], default: artwork.texts[3] } ] } function renderArtworkSource(source, artwork, values, styles = [], positions = []) { const fields = artworkFields(artwork) const fieldValues = fields.map((field, index) => values[index] === undefined ? field.default || '' : values[index]) return source.replace(/(]*>)([\s\S]*?)(<\/text>)/gi, (full, opening, content, closing) => { const id = opening.match(/\bid=["']([^"']+)["']/i)?.[1] const index = textFieldIndex(id, fields) if (index < 0 || index >= fieldValues.length) return full const style = styles[index] || {} const position = positions[index] let nextOpening = opening.replace(/\sdata-artwork-field=["'][^"']*["']/i, '').replace(/>\s*$/, '') const setAttribute = (name, value) => { const escaped = escapeXml(value) const expression = new RegExp(`\\s${name}=(?:"[^"]*"|'[^']*')`, 'i') nextOpening = expression.test(nextOpening) ? nextOpening.replace(expression, ` ${name}="${escaped}"`) : `${nextOpening} ${name}="${escaped}"` } setAttribute('data-artwork-field', index) if (style.font) setAttribute('font-family', style.font) if (style.size) setAttribute('font-size', Number(style.size)) if (style.color) setAttribute('fill', style.color) if (position?.x !== undefined) setAttribute('x', Number(position.x)) if (position?.y !== undefined) setAttribute('y', Number(position.y)) nextOpening += '>' const value = index === 3 ? content.replace(/Ana/g, escapeXml(fieldValues[index])) : escapeXml(fieldValues[index]) return `${nextOpening}${value}${closing}` }) } function SvgArtwork({ artwork, values, styles = [], positions = [], onPositionsChange }) { const [markup, setMarkup] = useState('') const containerRef = useRef(null) useEffect(() => { let active = true fetch(artwork.file_url).then(response => response.text()).then(source => { if (active) setMarkup(renderArtworkSource(source, artwork, values, styles, positions)) }).catch(() => { if (active) setMarkup('') }) return () => { active = false } }, [artwork, values, styles, positions]) useEffect(() => { if (!markup || !onPositionsChange) return undefined const svg = containerRef.current?.querySelector('svg') if (!svg) return undefined const viewBox = (svg.getAttribute('viewBox') || '0 0 1000 1000').split(/\s+/).map(Number) const onPointerDown = event => { const text = event.target.closest('[data-artwork-field]') if (!text) return event.preventDefault() const index = Number(text.getAttribute('data-artwork-field')) const rect = svg.getBoundingClientRect() const toSvgPoint = pointerEvent => ({ x: viewBox[0] + ((pointerEvent.clientX - rect.left) / rect.width) * viewBox[2], y: viewBox[1] + ((pointerEvent.clientY - rect.top) / rect.height) * viewBox[3] }) const pointerStart = toSvgPoint(event) const bounds = text.getBBox() const textX = Number(text.getAttribute('x')) || bounds.x + bounds.width / 2 const textY = Number(text.getAttribute('y')) || bounds.y + bounds.height const offsetX = textX - pointerStart.x const offsetY = textY - pointerStart.y const move = moveEvent => { const point = toSvgPoint(moveEvent) const x = Math.max(viewBox[0], Math.min(viewBox[0] + viewBox[2], point.x + offsetX)) const y = Math.max(viewBox[1], Math.min(viewBox[1] + viewBox[3], point.y + offsetY)) onPositionsChange(index, { x: Math.round(x), y: Math.round(y) }) } const stop = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', stop) } window.addEventListener('pointermove', move) window.addEventListener('pointerup', stop) } svg.addEventListener('pointerdown', onPointerDown) return () => svg.removeEventListener('pointerdown', onPointerDown) }, [markup, onPositionsChange]) return markup ?
:
Carregando arte...
} function escapeXml(value) { return String(value).replace(/[<>&'"]/g, character => ({ '<': '<', '>': '>', '&': '&', "'": ''', '"': '"' }[character])) } async function getArtworkSvg(artwork, values, styles, positions) { if (!artwork.file_url) return null const source = await fetch(artwork.file_url).then(response => response.text()) return renderArtworkSource(source, artwork, values, styles, positions) } function ArtPreview({ artwork, large = false, text0, text1, text2, text3, styles, positions, onPositionsChange }) { return
{artwork.file_url ? :
{text1 || artwork.texts[0]}
{text2 || artwork.texts[1]}
feito pra você
}
} function Creator({ product, artwork, addToCart, navigate }) { const nameDefault = (artwork.texts[2] || '').replace(/^.*?,\s*/, '').replace(/\s*♡.*$/, '').trim() || 'Ana' const [texts, setTexts] = useState([artwork.texts[0] || '', artwork.texts[1] || '', artwork.texts[2] || '', nameDefault]) const [styles, setStyles] = useState(artwork.textFields?.map(field => ({ font: 'Georgia', size: 40, color: '#71384d', ...field.style })) || TEXT_FIELD_IDS.map(() => ({ font: 'Georgia', size: 40, color: '#71384d' }))) const [positions, setPositions] = useState([]) const [quantity, setQuantity] = useState(1) const updateText = (index, value) => setTexts(current => current.map((text, textIndex) => textIndex === index ? value : text)) const updateStyle = (index, key, value) => setStyles(current => current.map((style, styleIndex) => styleIndex === index ? { ...style, [key]: value } : style)) const updatePosition = (index, position) => setPositions(current => { const next = [...current]; next[index] = position; return next }) const saveCreation = async () => { const artworkSvg = await getArtworkSvg(artwork, texts, styles, positions); addToCart({ productId: product.id, name: product.name, price: product.price, image: product.image, artwork: artwork.name, artworkSvg, customText: texts.join(' / '), quantity }); navigate('cart') } return
Área de criação

Deixe com a sua cara.

Edite os textos e arraste cada parte na pré-visualização.

123
{texts.map((text, index) =>
{styles[index]?.color || '#71384d'}
)}
Uma dica carinhosa
Arraste os textos na arte para posicioná-los como preferir.
Seu produto{product.name}R$ {product.price.toFixed(2).replace('.', ',')}
{quantity}
Pré-visualização · arraste os textos
As cores podem variar na impressão.
} function Checkout({ cart, removeFromCart, submitOrder, navigate }) { const [form, setForm] = useState({ name: '', email: '', phone: '', address: '' }); const total = cart.reduce((s, i) => s + i.price * i.quantity, 0); return
Quase lá

Finalize seu pedido.

{!cart.length ?

Seu carrinho está vazio

Encontre algo especial para personalizar.

:

Seus produtos {cart.length} itens

{cart.map(item =>
{item.name}{item.artwork || 'Sem arte'} {item.customText && `· ${item.customText}`}Qtd. {item.quantity}
R$ {(item.price * item.quantity).toFixed(2).replace('.', ',')}
)}

Seus dados

}
} function Success({ navigate }) { return
Pedido recebido

Que alegria, obrigado!

Recebemos seu pedido e já vamos começar a preparar tudo com muito carinho. Em breve entraremos em contato para confirmar os detalhes.

} function Admin({ products, setProducts, categories, setCategories, artworks, setArtworks, orders, close }) { const [tab, setTab] = useState('overview'); const [notice, setNotice] = useState('') const [showArtworkForm, setShowArtworkForm] = useState(false) const [token, setToken] = useState(() => localStorage.getItem('fp-admin-token')) const [remoteOrders, setRemoteOrders] = useState(orders) const notify = (message) => { setNotice(message); setTimeout(() => setNotice(''), 2500) } useEffect(() => { const handleAuthExpired = () => setToken(null) window.addEventListener(AUTH_EXPIRED_EVENT, handleAuthExpired) return () => window.removeEventListener(AUTH_EXPIRED_EVENT, handleAuthExpired) }, []) useEffect(() => { if (!token) return api.getOrders(token).then(setRemoteOrders).catch(error => notify(error.message)) }, [token]) if (!token) return { localStorage.setItem('fp-admin-token', value); setToken(value) }} close={close} /> const addProduct = async () => { try { const data = new FormData() data.append('category_id', categories[0]?.id || '') data.append('name', 'Novo produto') data.append('description', 'Descrição do novo produto.') data.append('price', '39.90') data.append('badge', 'Novo') const product = await api.createProduct(data, token) setProducts([...products, normalizeProduct(product)]); notify('Produto adicionado') } catch (error) { notify(error.message) } } const addCategory = async () => { try { const category = await api.createCategory({ name: 'Nova categoria', emoji: '✨', color: '#eee8ff' }, token) setCategories([...categories, category]); notify('Categoria adicionada') } catch (error) { notify(error.message) } } const addArtwork = async () => { setTab('artworks') setShowArtworkForm(true) } return
Gerenciador

{tab === 'overview' ? 'Olá, vamos criar?' : tab === 'orders' ? 'Pedidos feitos' : tab === 'products' ? 'Produtos' : tab === 'artworks' ? 'Artes prontas' : 'Categorias'}

{notice &&
{notice}
}{tab === 'overview' && }{tab === 'products' && }{tab === 'artworks' && setShowArtworkForm(false)} addArtwork={addArtwork} token={token} />}{tab === 'categories' && }{tab === 'orders' && }
} function AdminLogin({ onLogin, close }) { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState('') const submit = async event => { event.preventDefault(); try { const result = await api.login(email, password); if (result.user.role !== 'admin') throw new Error('Este usuário não é administrador'); onLogin(result.token) } catch (loginError) { setError(loginError.message) } } return
Área restrita

Entrar no gerenciador

Use o e-mail e a senha do administrador cadastrados no banco.

{error && {error}}
} function Overview({ products, artworks, categories, orders, setTab }) { return
Produtos ativos{products.length}
Artes prontas{artworks.length}
Categorias{categories.length}
Pedidos recebidos{orders.length}
Tudo em um só lugar

Seu ateliê, mais organizado.

Cadastre produtos, prepare artes para seus clientes e acompanhe cada pedido recebido.

} function AdminProducts({ products, setProducts, categories, addProduct, token }) { const [editing, setEditing] = useState(null) const remove = async id => { try { await api.deleteProduct(id, token); setProducts(products.filter(item => item.id !== id)) } catch (error) { window.alert(error.message) } } const save = async product => { try { const data = new FormData() data.append('category_id', product.category_id || product.categoryId || '') data.append('category_ids', JSON.stringify(product.category_ids || [product.category_id || product.categoryId])) data.append('name', product.name) data.append('description', product.description || '') data.append('price', String(product.price)) data.append('old_price', product.oldPrice ? String(product.oldPrice) : '') data.append('badge', product.badge || '') data.append('active', product.active === false ? 'false' : 'true') data.append('art_width_cm', product.art_width_cm || '') data.append('art_height_cm', product.art_height_cm || '') if (product.imageFile) data.append('image', product.imageFile) const isDatabaseProduct = Number.isInteger(Number(product.id)) && Number(product.id) > 0 const updated = isDatabaseProduct ? await api.updateProduct(product.id, data, token) : await api.createProduct(data, token) const normalized = normalizeProduct(updated || { ...product, image: product.imagePreview || product.image }) const refreshed = await api.getProducts() setProducts(refreshed.map(normalizeProduct)) setEditing(null) } catch (error) { window.alert(error.message) } } const changeImage = (event) => { const file = event.target.files[0] if (!file) return const image = new Image() image.onload = () => { if (image.width !== 1000 || image.height !== 1000) window.alert('A imagem precisa ter exatamente 1000 x 1000 px.') else setEditing(current => ({ ...current, imageFile: file, imagePreview: URL.createObjectURL(file) })) } image.onerror = () => window.alert('Não foi possível ler a imagem selecionada.') image.src = URL.createObjectURL(file) } return
{products.map(p =>
setEditing({ ...p, category_id: p.category_id || categories.find(category => category.slug === p.category)?.id, category_ids: p.category_ids?.length ? p.category_ids : [p.category_id] })}>{p.name}
{p.name}{p.category} · R$ {p.price.toFixed(2).replace('.', ',')}
)}
{editing &&
event.stopPropagation()}>
Editando produto

{editing.name}

Imagem do produtoA imagem deve ter exatamente 1000 × 1000 px.