From 9ad5cdb5fb6e358b88d5073584f3baa0ad0b27eb Mon Sep 17 00:00:00 2001 From: admtracksteel Date: Mon, 8 Jun 2026 00:10:28 +0000 Subject: [PATCH] =?UTF-8?q?Evolu=C3=A7=C3=A3o=20do=20Design=20Canvas=202.0?= =?UTF-8?q?:=20biblioteca=20de=20assets=20no=20Supabase=20e=20controle=20d?= =?UTF-8?q?e=20rota=C3=A7=C3=A3o/opacidade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/app.js | 250 +++++++++++++++++++++++++++++++++++++++++++++- public/index.html | 33 +++++- server.js | 100 +++++++++++++++++++ 3 files changed, 379 insertions(+), 4 deletions(-) diff --git a/public/app.js b/public/app.js index 767bf6a..87e9412 100644 --- a/public/app.js +++ b/public/app.js @@ -7570,10 +7570,19 @@ document.addEventListener('DOMContentLoaded', () => { const canvasFontSize = document.getElementById('canvasFontSize'); const canvasTextColor = document.getElementById('canvasTextColor'); const canvasBgColorInput = document.getElementById('canvasBgColor'); + const canvasRotation = document.getElementById('canvasRotation'); + const canvasOpacity = document.getElementById('canvasOpacity'); const btnCanvasZIndexUp = document.getElementById('btnCanvasZIndexUp'); const btnCanvasZIndexDown = document.getElementById('btnCanvasZIndexDown'); const btnCanvasDelete = document.getElementById('btnCanvasDelete'); + // Novos controles do Painel de Assets Lateral + const canvasAssetSearch = document.getElementById('canvasAssetSearch'); + const canvasAssetsContainer = document.getElementById('canvasAssetsContainer'); + const btnTabAssetPostits = document.getElementById('btnTabAssetPostits'); + const btnTabAssetStickers = document.getElementById('btnTabAssetStickers'); + const btnTabAssetFormas = document.getElementById('btnTabAssetFormas'); + let canvasModeActive = false; let selectedCanvasItem = null; let isDraggingCanvas = false; @@ -7581,6 +7590,7 @@ document.addEventListener('DOMContentLoaded', () => { let dragStartX, dragStartY; let dragStartLeft, dragStartTop; let dragStartWidth, dragStartHeight; + let activeAssetTab = 'postit'; function enterCanvasMode() { canvasModeActive = true; @@ -7590,6 +7600,15 @@ document.addEventListener('DOMContentLoaded', () => { btnCanvasModeToggle.style.color = 'white'; } if (canvasControlPanel) canvasControlPanel.style.display = 'flex'; + + // Alternar painéis laterais + const configPanel = document.getElementById('cartazLeftConfigPanel'); + const canvasPanel = document.getElementById('cartazLeftCanvasPanel'); + if (configPanel) configPanel.style.display = 'none'; + if (canvasPanel) canvasPanel.style.display = 'flex'; + + // Iniciar na aba correspondente de assets + selectAssetTab(activeAssetTab); cartazPrintableArea.style.position = 'relative'; const rectParent = cartazPrintableArea.getBoundingClientRect(); @@ -7639,7 +7658,7 @@ document.addEventListener('DOMContentLoaded', () => { }); cartazPreviewContent.style.display = 'none'; - showToast('Modo Design Canvas ativado! Arraste, redimensione e clique duas vezes para editar o texto.', 'info'); + showToast('Modo Design Canvas ativado! Adicione elementos e ajuste o mural livremente.', 'info'); } function exitCanvasMode() { @@ -7650,6 +7669,12 @@ document.addEventListener('DOMContentLoaded', () => { btnCanvasModeToggle.style.color = 'var(--text-secondary)'; } if (canvasControlPanel) canvasControlPanel.style.display = 'none'; + + // Alternar painéis de volta + const configPanel = document.getElementById('cartazLeftConfigPanel'); + const canvasPanel = document.getElementById('cartazLeftCanvasPanel'); + if (configPanel) configPanel.style.display = 'flex'; + if (canvasPanel) canvasPanel.style.display = 'none'; if (selectedCanvasItem) { selectedCanvasItem.classList.remove('active-item'); @@ -7660,8 +7685,47 @@ document.addEventListener('DOMContentLoaded', () => { items.forEach(item => { item.removeAttribute('contenteditable'); }); - - showToast('Layout do Canvas salvo com sucesso!', 'success'); + + // Limpar o HTML para persistência (remover seleções/handles) + const tempContainer = document.createElement('div'); + tempContainer.innerHTML = cartazPrintableArea.innerHTML; + tempContainer.querySelectorAll('.canvas-resize-handle').forEach(h => h.remove()); + tempContainer.querySelectorAll('.canvas-item').forEach(el => { + el.classList.remove('active-item'); + el.removeAttribute('contenteditable'); + if (el.style.border && el.style.border.includes('dashed')) { + el.style.border = 'none'; + } + }); + const cleanHtml = tempContainer.innerHTML; + + // Persistir alterações de canvas no banco + if (activeCartazData && activeCartazData.id) { + fetch(`/api/cartazes/${activeCartazData.id}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}` + }, + body: JSON.stringify({ + conteudo: cleanHtml, + corFundo: cartazBgColor.value, + corTexto: cartazTextColor.value + }) + }).then(res => { + if (res.ok) { + showToast('Layout do Canvas salvo no servidor!', 'success'); + loadCartazesHistory(); + } else { + showToast('Erro ao salvar layout no servidor.', 'error'); + } + }).catch(e => { + console.error(e); + showToast('Erro de conexão ao salvar canvas.', 'error'); + }); + } else { + showToast('Layout salvo temporariamente. Crie o cartaz para salvar definitivamente.', 'warning'); + } } function setupCanvasItemEvents(item) { @@ -7916,6 +7980,12 @@ document.addEventListener('DOMContentLoaded', () => { const bg = rgbToHex(window.getComputedStyle(selectedCanvasItem).backgroundColor); if (canvasBgColorInput) canvasBgColorInput.value = bg; + + const rot = selectedCanvasItem.getAttribute('data-rotation') || '0'; + if (canvasRotation) canvasRotation.value = rot; + + const op = window.getComputedStyle(selectedCanvasItem).opacity || '1'; + if (canvasOpacity) canvasOpacity.value = Math.round(parseFloat(op) * 100); } if (canvasFontFamily) { @@ -7938,6 +8008,23 @@ document.addEventListener('DOMContentLoaded', () => { if (selectedCanvasItem) selectedCanvasItem.style.backgroundColor = canvasBgColorInput.value; }); } + if (canvasRotation) { + canvasRotation.addEventListener('input', () => { + if (selectedCanvasItem) { + const val = canvasRotation.value; + selectedCanvasItem.style.transform = `rotate(${val}deg)`; + selectedCanvasItem.setAttribute('data-rotation', val); + } + }); + } + if (canvasOpacity) { + canvasOpacity.addEventListener('input', () => { + if (selectedCanvasItem) { + const val = canvasOpacity.value; + selectedCanvasItem.style.opacity = parseFloat(val) / 100; + } + }); + } if (btnCanvasZIndexUp) { btnCanvasZIndexUp.addEventListener('click', () => { if (selectedCanvasItem) { @@ -8039,4 +8126,161 @@ document.addEventListener('DOMContentLoaded', () => { } }); } + + function selectAssetTab(tab) { + activeAssetTab = tab; + [btnTabAssetPostits, btnTabAssetStickers, btnTabAssetFormas].forEach(btn => { + if (!btn) return; + const tabIdName = `btnTabAsset${tab.charAt(0).toUpperCase() + tab.slice(1)}s`; + if (btn.id === tabIdName) { + btn.classList.add('active'); + btn.style.background = 'rgba(59, 130, 246, 0.1)'; + btn.style.color = 'var(--text-primary)'; + btn.style.fontWeight = 'bold'; + } else { + btn.classList.remove('active'); + btn.style.background = 'transparent'; + btn.style.color = 'var(--text-secondary)'; + btn.style.fontWeight = 'normal'; + } + }); + loadCanvasAssets(tab, canvasAssetSearch ? canvasAssetSearch.value : ''); + } + + if (btnTabAssetPostits) btnTabAssetPostits.addEventListener('click', () => selectAssetTab('postit')); + if (btnTabAssetStickers) btnTabAssetStickers.addEventListener('click', () => selectAssetTab('sticker')); + if (btnTabAssetFormas) btnTabAssetFormas.addEventListener('click', () => selectAssetTab('forma')); + + if (canvasAssetSearch) { + canvasAssetSearch.addEventListener('input', (e) => { + loadCanvasAssets(activeAssetTab, e.target.value); + }); + } + + async function loadCanvasAssets(tipo, busca = '') { + if (!canvasAssetsContainer) return; + canvasAssetsContainer.innerHTML = '
Carregando...
'; + + try { + const res = await fetch(`/api/biblioteca-assets?tipo=${tipo}&busca=${encodeURIComponent(busca)}`, { + headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } + }); + const data = await res.json(); + canvasAssetsContainer.innerHTML = ''; + + if (!data || data.length === 0) { + canvasAssetsContainer.innerHTML = '
Nenhum item encontrado.
'; + return; + } + + data.forEach(asset => { + const item = document.createElement('div'); + item.style.cursor = 'pointer'; + item.style.padding = '8px'; + item.style.borderRadius = '6px'; + item.style.background = 'var(--bg-tertiary)'; + item.style.border = '1px solid var(--border-light)'; + item.style.display = 'flex'; + item.style.flexDirection = 'column'; + item.style.alignItems = 'center'; + item.style.justifyContent = 'center'; + item.style.gap = '6px'; + item.style.transition = 'all 0.2s'; + item.title = asset.nome; + + item.addEventListener('mouseenter', () => { + item.style.transform = 'scale(1.05)'; + item.style.borderColor = 'rgba(59, 130, 246, 0.5)'; + }); + item.addEventListener('mouseleave', () => { + item.style.transform = 'scale(1)'; + item.style.borderColor = 'var(--border-light)'; + }); + + let previewHtml = ''; + if (asset.tipo === 'postit') { + previewHtml = `
`; + } else if (asset.tipo === 'sticker') { + previewHtml = ``; + } else if (asset.tipo === 'forma') { + if (asset.url === 'rect') { + previewHtml = `
`; + } else if (asset.url === 'circle') { + previewHtml = `
`; + } else if (asset.url === 'ellipse') { + previewHtml = `
`; + } else if (asset.url === 'line') { + previewHtml = `
`; + } + } + + item.innerHTML = ` + ${previewHtml} + ${asset.nome} + `; + + item.addEventListener('click', () => addAssetToCanvas(asset)); + canvasAssetsContainer.appendChild(item); + }); + } catch(err) { + console.error(err); + canvasAssetsContainer.innerHTML = '
Erro ao carregar elementos.
'; + } + } + + function addAssetToCanvas(asset) { + if (!canvasModeActive) return; + + const item = document.createElement('div'); + item.className = 'canvas-item'; + item.style.position = 'absolute'; + item.style.left = '35%'; + item.style.top = '35%'; + item.style.width = '30%'; + item.style.height = '15%'; + item.style.zIndex = '10'; + + if (asset.tipo === 'postit') { + item.style.background = asset.url; + item.style.color = '#2d3748'; + item.style.boxShadow = '3px 5px 10px rgba(0,0,0,0.15)'; + item.style.borderRadius = '4px'; + item.style.transform = 'rotate(' + (Math.random() * 8 - 4) + 'deg)'; + item.style.padding = '12px'; + item.style.fontFamily = "'Caveat', cursive"; + item.style.fontSize = '22px'; + item.innerHTML = 'Anotação...'; + item.setAttribute('contenteditable', 'true'); + item.style.width = '35%'; + item.style.height = '20%'; + } else if (asset.tipo === 'sticker') { + item.style.background = 'transparent'; + item.innerHTML = ``; + item.style.width = '20%'; + item.style.height = '20%'; + } else if (asset.tipo === 'forma') { + item.style.background = 'transparent'; + item.style.border = '3px solid ' + (cartazTextColor.value || '#2d3748'); + if (asset.url === 'rect') { + item.style.borderRadius = '4px'; + } else if (asset.url === 'circle' || asset.url === 'ellipse') { + item.style.borderRadius = '50%'; + item.style.width = '20%'; + item.style.height = '20%'; + } else if (asset.url === 'line') { + item.style.border = 'none'; + item.style.borderTop = '4px solid ' + (cartazTextColor.value || '#2d3748'); + item.style.height = '10px'; + item.style.width = '40%'; + } + } + + const handle = document.createElement('div'); + handle.className = 'canvas-resize-handle'; + item.appendChild(handle); + + cartazPrintableArea.appendChild(item); + setupCanvasItemEvents(item); + item.click(); + } }); diff --git a/public/index.html b/public/index.html index d00acff..c3b6002 100644 --- a/public/index.html +++ b/public/index.html @@ -1778,7 +1778,7 @@
-
+

1. Definições do Cartaz

@@ -1879,6 +1879,27 @@
+ + +
@@ -1919,6 +1940,16 @@
+
+ + +
+ +
+ + +
+
diff --git a/server.js b/server.js index 466a47d..ed18e07 100644 --- a/server.js +++ b/server.js @@ -1607,6 +1607,62 @@ app.delete('/api/cartazes/:id', requireAuth, async (req, res) => { } }); +app.put('/api/cartazes/:id', requireAuth, async (req, res) => { + const { id } = req.params; + const usuarioId = req.user?.id || '00000000-0000-0000-0000-000000000000'; + const { titulo, tema, corFundo, corTexto, layout, conteudo, imagemUrl } = req.body; + try { + const dbRes = await dbPool.query( + `UPDATE escola.cartazes + SET titulo = COALESCE($1, titulo), + tema = COALESCE($2, tema), + cor_fundo = COALESCE($3, cor_fundo), + cor_texto = COALESCE($4, cor_texto), + layout = COALESCE($5, layout), + conteudo = COALESCE($6, conteudo), + imagem_url = COALESCE($7, imagem_url) + WHERE id = $8 AND usuario_id = $9 RETURNING *`, + [titulo, tema, corFundo, corTexto, layout, conteudo, imagemUrl, id, usuarioId] + ); + if (dbRes.rows.length === 0) { + return res.status(404).json({ error: 'Cartaz não encontrado ou não pertence ao usuário.' }); + } + res.json(dbRes.rows[0]); + } catch (err) { + console.error('Erro ao atualizar cartaz:', err); + res.status(500).json({ error: 'Erro ao atualizar cartaz.' }); + } +}); + +app.get('/api/biblioteca-assets', requireAuth, async (req, res) => { + const { busca, tipo } = req.query; + try { + let query = `SELECT id, tipo, nome, url, tags FROM escola.biblioteca_assets WHERE 1=1`; + const params = []; + let paramIdx = 1; + + if (tipo) { + query += ` AND tipo = $${paramIdx}`; + params.push(tipo); + paramIdx++; + } + + if (busca) { + query += ` AND (nome ILIKE $${paramIdx} OR $${paramIdx} = ANY(tags))`; + params.push(`%${busca}%`); + paramIdx++; + } + + query += ` ORDER BY tipo, nome ASC`; + + const dbRes = await dbPool.query(query, params); + res.json(dbRes.rows); + } catch (err) { + console.error('Erro ao buscar assets da biblioteca:', err); + res.status(500).json({ error: 'Erro ao carregar assets da biblioteca.' }); + } +}); + // ============================================================ // ROTAS DO MUSICANDO IDEIAS // ============================================================ @@ -2173,8 +2229,52 @@ async function initDatabase() { dados BYTEA NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); + + -- Biblioteca de Assets (Stickers, Ícones, Post-its, etc.) + CREATE TABLE IF NOT EXISTS escola.biblioteca_assets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tipo VARCHAR(50) NOT NULL, -- 'sticker', 'postit', 'icone', 'forma' + nome VARCHAR(100) NOT NULL, + url TEXT NOT NULL, + tags TEXT[], -- array de tags para busca + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); `); + const checkAssets = await dbPool.query("SELECT COUNT(*) FROM escola.biblioteca_assets;"); + if (parseInt(checkAssets.rows[0].count) === 0) { + console.log('[Database Init] Inserindo assets padrão na biblioteca (Design Canvas 2.0)...'); + await dbPool.query(` + INSERT INTO escola.biblioteca_assets (tipo, nome, url, tags) VALUES + ('postit', 'Post-it Amarelo', '#feff9c', ARRAY['post-it', 'amarelo', 'anotacao']), + ('postit', 'Post-it Azul', '#7afcff', ARRAY['post-it', 'azul', 'anotacao']), + ('postit', 'Post-it Rosa', '#ff7eb9', ARRAY['post-it', 'rosa', 'anotacao']), + ('postit', 'Post-it Verde', '#98ff98', ARRAY['post-it', 'verde', 'anotacao']), + ('postit', 'Post-it Laranja', '#ffb7b2', ARRAY['post-it', 'laranja', 'anotacao']), + + ('sticker', 'Escola', 'https://openmoji.org/data/color/svg/1F3EB.svg', ARRAY['escola', 'colegio', 'aula']), + ('sticker', 'Lápis', 'https://openmoji.org/data/color/svg/270F.svg', ARRAY['lapis', 'escrever', 'desenhar']), + ('sticker', 'Caderno', 'https://openmoji.org/data/color/svg/1F4DD.svg', ARRAY['caderno', 'anotacao', 'estudo']), + ('sticker', 'Paleta de Arte', 'https://openmoji.org/data/color/svg/1F3A8.svg', ARRAY['arte', 'paleta', 'pintura', 'desenho']), + ('sticker', 'Quebra-cabeça', 'https://openmoji.org/data/color/svg/1F9E9.svg', ARRAY['jogo', 'quebra-cabeca', 'brinquedo']), + ('sticker', 'Ursinho', 'https://openmoji.org/data/color/svg/1F9F8.svg', ARRAY['brinquedo', 'ursinho', 'fofo']), + ('sticker', 'Foguete', 'https://openmoji.org/data/color/svg/1F680.svg', ARRAY['foguete', 'espaco', 'voar']), + ('sticker', 'Notas Musicais', 'https://openmoji.org/data/color/svg/1F3B6.svg', ARRAY['musica', 'som', 'notas']), + ('sticker', 'Gatinho', 'https://openmoji.org/data/color/svg/1F431.svg', ARRAY['animal', 'gato', 'gatinho', 'fofo']), + ('sticker', 'Cachorrinho', 'https://openmoji.org/data/color/svg/1F436.svg', ARRAY['animal', 'cao', 'cachorro', 'fofo']), + ('sticker', 'Maçã', 'https://openmoji.org/data/color/svg/1F34E.svg', ARRAY['fruta', 'maca', 'comida']), + ('sticker', 'Balão', 'https://openmoji.org/data/color/svg/1F388.svg', ARRAY['festa', 'balao', 'bexiga']), + ('sticker', 'Estrela Brilhante', 'https://openmoji.org/data/color/svg/1F31F.svg', ARRAY['estrela', 'brilho', 'premio']), + ('sticker', 'Coração', 'https://openmoji.org/data/color/svg/1F496.svg', ARRAY['amor', 'coracao', 'carinho']), + ('sticker', 'Balão de Fala', 'https://openmoji.org/data/color/svg/1F4AC.svg', ARRAY['fala', 'dialogo', 'comunicacao']), + + ('forma', 'Retângulo', 'rect', ARRAY['forma', 'retangulo', 'quadrado', 'fundo']), + ('forma', 'Círculo', 'circle', ARRAY['forma', 'circulo', 'bola', 'redondo']), + ('forma', 'Elipse', 'ellipse', ARRAY['forma', 'elipse', 'oval']), + ('forma', 'Linha Horizontal', 'line', ARRAY['forma', 'linha', 'divisoria']); + `); + } + // Garantir que a coluna character_description_english exista na tabela projetos_comics await dbPool.query(`ALTER TABLE escola.projetos_comics ADD COLUMN IF NOT EXISTS character_description_english TEXT;`);