import { api } from '../api.js'; import { showToast } from '../components/toast.js'; const BACKGROUNDS = [ { name: 'Black', value: '#000000' }, { name: 'Dark Blue', value: '#0f172a' }, { name: 'Dark Gradient', value: 'linear-gradient(135deg, #0c0c0c 0%, #1a1a2e 50%, #16213e 100%)' }, { name: 'Blue Gradient', value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }, { name: 'Sunset', value: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)' }, { name: 'Ocean', value: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)' }, { name: 'Forest', value: 'linear-gradient(135deg, #134e5e 0%, #71b280 100%)' }, { name: 'Dark Red', value: 'linear-gradient(135deg, #200122 0%, #6f0000 100%)' }, { name: 'White', value: '#FFFFFF' }, ]; const FONTS = ['Arial', 'Helvetica', 'Georgia', 'Impact', 'Verdana', 'Trebuchet MS', 'Courier New', 'Times New Roman']; let elements = []; let selectedIdx = -1; let bgValue = '#000000'; let bgImageDataUrl = null; let dragging = null; let dragStart = null; export function render(container) { elements = []; selectedIdx = -1; bgValue = '#000000'; bgImageDataUrl = null; container.innerHTML = `

Click elements to select. Drag to reposition. Live preview updates in real-time.

Add Element

Background

${BACKGROUNDS.map(b => `
`).join('')}

Layers

`; // Background handlers document.querySelectorAll('[data-bg]').forEach(el => { el.onclick = () => { bgValue = el.dataset.bg; bgImageDataUrl = null; redraw(); }; }); document.getElementById('bgColor').oninput = (e) => { bgValue = e.target.value; bgImageDataUrl = null; redraw(); }; document.getElementById('bgImageBtn').onclick = () => document.getElementById('bgImageInput').click(); document.getElementById('bgImageInput').onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { bgImageDataUrl = ev.target.result; redraw(); }; reader.readAsDataURL(file); }; // Add element handlers document.getElementById('addText').onclick = () => addElement({ type: 'text', x: 10, y: 60, text: 'Your text here', fontSize: 24, fontFamily: 'Arial', color: '#FFFFFF', bold: false, shadow: false }); document.getElementById('addHeading').onclick = () => addElement({ type: 'text', x: 5, y: 5, text: 'HEADING', fontSize: 64, fontFamily: 'Impact', color: '#FFFFFF', bold: true, shadow: true }); document.getElementById('addImage').onclick = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = () => { const reader = new FileReader(); reader.onload = (ev) => addElement({ type: 'image', x: 10, y: 10, width: 30, height: 30, src: ev.target.result }); reader.readAsDataURL(input.files[0]); }; input.click(); }; document.getElementById('addVideo').onclick = () => { const url = prompt('Video URL (MP4):'); if (url) addElement({ type: 'video', x: 5, y: 5, width: 50, height: 50, src: url, muted: true, loop: true }); }; document.getElementById('addClock').onclick = () => addElement({ type: 'clock', x: 60, y: 5, fontSize: 48, fontFamily: 'Arial', color: '#FFFFFF', format: '12h', showSeconds: true, shadow: true }); document.getElementById('addDate').onclick = () => addElement({ type: 'date', x: 60, y: 20, fontSize: 24, fontFamily: 'Arial', color: '#FFFFFF', shadow: false }); document.getElementById('addWeather').onclick = () => { const location = prompt('City, State:', 'Milwaukee, WI'); if (location) addElement({ type: 'weather', x: 5, y: 70, fontSize: 36, color: '#FFFFFF', location, units: 'imperial' }); }; document.getElementById('addTicker').onclick = () => { const url = prompt('RSS Feed URL:', 'https://feeds.bbci.co.uk/news/rss.xml'); if (url) addElement({ type: 'ticker', x: 0, y: 90, width: 100, height: 10, feedUrl: url, speed: 30, fontSize: 20, color: '#FFFFFF', bgColor: 'rgba(0,0,0,0.7)' }); }; document.getElementById('addShape').onclick = () => addElement({ type: 'shape', x: 20, y: 20, width: 30, height: 20, color: '#3b82f6', opacity: 0.7, radius: 8, shape: 'rect' }); document.getElementById('addQR').onclick = () => { const data = prompt('QR Code URL:', 'https://example.com'); if (data) addElement({ type: 'qr', x: 80, y: 70, size: 15, data, fgColor: '#FFFFFF', bgColor: '#000000' }); }; document.getElementById('addCountdown').onclick = () => { const target = prompt('Target date (YYYY-MM-DD):', '2026-04-01'); if (target) addElement({ type: 'countdown', x: 20, y: 40, fontSize: 48, color: '#FFFFFF', targetDate: target, label: 'Coming Soon' }); }; document.getElementById('addWebpage').onclick = () => { const url = prompt('Webpage URL:'); if (url) addElement({ type: 'webpage', x: 5, y: 5, width: 40, height: 40, url }); }; document.getElementById('deleteEl').onclick = () => { if (selectedIdx >= 0) { elements.splice(selectedIdx, 1); selectedIdx = -1; redraw(); } }; // Publish as dynamic HTML content document.getElementById('publishBtn').onclick = async () => { try { const html = generateHTML(); const blob = new Blob([html], { type: 'text/html' }); const file = new File([blob], `design-${Date.now()}.html`, { type: 'text/html' }); // Upload as a widget instead - create a text widget with the HTML const res = await fetch('/api/widgets', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}` }, body: JSON.stringify({ widget_type: 'text', name: `Design ${new Date().toLocaleDateString()}`, config: { html: generateInnerHTML(), css: '', background: bgValue } }) }); if (res.ok) showToast('Published as widget! Assign it to a layout zone.', 'success'); else showToast('Publish failed', 'error'); } catch (err) { showToast(err.message, 'error'); } }; // Export PNG screenshot document.getElementById('exportPngBtn').onclick = async () => { try { const preview = document.getElementById('designPreview'); // Use a canvas to capture const canvas = document.createElement('canvas'); canvas.width = 1920; canvas.height = 1080; const ctx = canvas.getContext('2d'); // Draw background if (bgImageDataUrl) { const img = new Image(); img.src = bgImageDataUrl; await new Promise(r => { img.onload = r; }); ctx.drawImage(img, 0, 0, 1920, 1080); } else if (bgValue.startsWith('linear')) { const colors = bgValue.match(/#[a-f0-9]{6}/gi) || ['#000']; const grad = ctx.createLinearGradient(0, 0, 1920, 1080); colors.forEach((c, i) => grad.addColorStop(i / Math.max(1, colors.length - 1), c)); ctx.fillStyle = grad; ctx.fillRect(0, 0, 1920, 1080); } else { ctx.fillStyle = bgValue; ctx.fillRect(0, 0, 1920, 1080); } // Draw text elements for (const el of elements) { if (el.type === 'text' || el.type === 'clock' || el.type === 'date' || el.type === 'countdown') { ctx.save(); ctx.font = `${el.bold ? 'bold ' : ''}${(el.fontSize / 100) * 1080}px ${el.fontFamily || 'Arial'}`; ctx.fillStyle = el.color || '#FFF'; if (el.shadow) { ctx.shadowColor = 'rgba(0,0,0,0.5)'; ctx.shadowBlur = 8; ctx.shadowOffsetX = 2; ctx.shadowOffsetY = 2; } let text = el.text || el.label || ''; if (el.type === 'clock') text = new Date().toLocaleTimeString(); if (el.type === 'date') text = new Date().toLocaleDateString(); ctx.fillText(text, (el.x / 100) * 1920, (el.y / 100) * 1080 + (el.fontSize / 100) * 1080); ctx.restore(); } else if (el.type === 'shape') { ctx.save(); ctx.globalAlpha = el.opacity || 1; ctx.fillStyle = el.color; ctx.fillRect((el.x / 100) * 1920, (el.y / 100) * 1080, (el.width / 100) * 1920, (el.height / 100) * 1080); ctx.restore(); } } const link = document.createElement('a'); link.download = 'signage-design.png'; link.href = canvas.toDataURL('image/png'); link.click(); } catch (err) { showToast('Export failed: ' + err.message, 'error'); } }; // Load saved design document.getElementById('loadDesignBtn').onclick = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = () => { const reader = new FileReader(); reader.onload = (ev) => { try { const data = JSON.parse(ev.target.result); elements = data.elements || []; bgValue = data.bgValue || '#000'; bgImageDataUrl = data.bgImageDataUrl || null; redraw(); showToast('Design loaded', 'success'); } catch { showToast('Invalid design file', 'error'); } }; reader.readAsText(input.files[0]); }; input.click(); }; // Mouse interaction on preview const preview = document.getElementById('designPreview'); preview.onmousedown = (e) => { const rect = preview.getBoundingClientRect(); const px = ((e.clientX - rect.left) / rect.width) * 100; const py = ((e.clientY - rect.top) / rect.height) * 100; selectedIdx = -1; for (let i = elements.length - 1; i >= 0; i--) { const el = elements[i]; const b = getBounds(el); if (px >= b.x && px <= b.x + b.w && py >= b.y && py <= b.y + b.h) { selectedIdx = i; dragging = el; dragStart = { px, py, ox: el.x, oy: el.y }; break; } } redraw(); }; preview.onmousemove = (e) => { if (!dragging || !dragStart) return; const rect = preview.getBoundingClientRect(); const px = ((e.clientX - rect.left) / rect.width) * 100; const py = ((e.clientY - rect.top) / rect.height) * 100; dragging.x = Math.max(0, Math.min(95, dragStart.ox + (px - dragStart.px))); dragging.y = Math.max(0, Math.min(95, dragStart.oy + (py - dragStart.py))); redraw(); }; preview.onmouseup = () => { dragging = null; dragStart = null; }; redraw(); } function addElement(el) { elements.push(el); selectedIdx = elements.length - 1; redraw(); } function getBounds(el) { const w = el.width || el.size || (el.fontSize ? el.fontSize * 0.6 * (el.text?.length || 8) / 100 * 100 : 20); const h = el.height || el.size || (el.fontSize ? el.fontSize * 1.2 / 100 * 100 : 10); return { x: el.x, y: el.y, w: Math.min(w, 100), h: Math.min(h, 100) }; } function redraw() { const preview = document.getElementById('designPreview'); if (!preview) return; let html = ''; // Background if (bgImageDataUrl) { preview.style.background = `url(${bgImageDataUrl}) center/cover`; } else { preview.style.background = bgValue; } // Elements elements.forEach((el, i) => { const selected = i === selectedIdx; const border = selected ? 'outline:2px solid #3b82f6;outline-offset:2px;' : ''; const cursor = 'cursor:move;'; switch (el.type) { case 'text': html += `
${el.text}
`; break; case 'clock': html += `
`; break; case 'date': html += `
`; break; case 'image': html += ``; break; case 'video': html += ``; break; case 'shape': html += `
`; break; case 'weather': html += `
⛅ Loading...
`; break; case 'ticker': html += `
Loading news...
`; break; case 'qr': html += `
QR CODE
${el.data?.slice(0, 25)}
`; break; case 'countdown': html += `
${el.label || ''}
`; break; case 'webpage': html += ``; break; } }); // Add ticker animation CSS html += ``; preview.innerHTML = html; // Update dynamic elements updateDynamic(); // Update properties panel updateProps(); updateLayers(); } function updateDynamic() { elements.forEach((el, i) => { if (el.type === 'clock') { const clockEl = document.getElementById(`clock_${i}`); if (clockEl) { const update = () => { const opts = { hour: '2-digit', minute: '2-digit' }; if (el.showSeconds) opts.second = '2-digit'; opts.hour12 = el.format !== '24h'; clockEl.textContent = new Date().toLocaleTimeString('en-US', opts); }; update(); // Only set interval if element still exists const iv = setInterval(() => { if (document.getElementById(`clock_${i}`)) update(); else clearInterval(iv); }, 1000); } } if (el.type === 'date') { const dateEl = document.getElementById(`date_${i}`); if (dateEl) dateEl.textContent = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); } if (el.type === 'countdown') { const cdEl = document.getElementById(`countdown_${i}`); if (cdEl && el.targetDate) { const update = () => { const diff = new Date(el.targetDate) - new Date(); if (diff <= 0) { cdEl.textContent = 'NOW!'; return; } const days = Math.floor(diff / 86400000); const hours = Math.floor((diff % 86400000) / 3600000); const mins = Math.floor((diff % 3600000) / 60000); cdEl.textContent = `${days}d ${hours}h ${mins}m`; }; update(); const iv = setInterval(() => { if (document.getElementById(`countdown_${i}`)) update(); else clearInterval(iv); }, 60000); } } if (el.type === 'weather') { const wEl = document.getElementById(`weather_${i}`); if (wEl && el.location) { fetch(`https://wttr.in/${encodeURIComponent(el.location)}?format=j1`).then(r => r.json()).then(d => { const cur = d.current_condition?.[0]; if (cur) { const temp = el.units === 'metric' ? cur.temp_C + '°C' : cur.temp_F + '°F'; wEl.textContent = `${temp} ${cur.weatherDesc?.[0]?.value || ''}`; } }).catch(() => { wEl.textContent = '⛅ ' + el.location; }); } } if (el.type === 'ticker') { const tEl = document.getElementById(`ticker_${i}`); if (tEl && el.feedUrl) { fetch(`https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent(el.feedUrl)}`).then(r => r.json()).then(d => { tEl.textContent = (d.items || []).map(item => item.title).join(' • ') || 'No items'; }).catch(() => { tEl.textContent = 'Feed unavailable'; }); } } }); } function updateProps() { const panel = document.getElementById('propPanel'); const fields = document.getElementById('propFields'); if (selectedIdx < 0 || !elements[selectedIdx]) { panel.style.display = 'none'; return; } panel.style.display = 'block'; const el = elements[selectedIdx]; let html = ''; // Common position html += `
`; if (el.type === 'text') { html += `
${el.fontSize}px
`; } else if (el.type === 'clock') { html += `
`; } else if (el.type === 'image' || el.type === 'video' || el.type === 'webpage') { html += `
`; if (el.type === 'video') html += ` `; } else if (el.type === 'shape') { html += `
`; } else if (el.type === 'weather') { html += `
`; } else if (el.type === 'ticker') { html += `
`; } else if (el.type === 'countdown') { html += `
`; } // Save design button html += ``; fields.innerHTML = html; fields.querySelectorAll('[data-prop]').forEach(input => { const handler = () => { const prop = input.dataset.prop; if (input.type === 'checkbox') el[prop] = input.checked; else if (input.type === 'number' || input.type === 'range') el[prop] = parseFloat(input.value); else el[prop] = input.value; redraw(); }; input.oninput = handler; input.onchange = handler; }); } function updateLayers() { const list = document.getElementById('layerList'); if (!list) return; const typeIcons = { text: '💬', clock: '🕓', date: '📅', image: '📷', video: '🎬', shape: '■', weather: '⛅', ticker: '📰', qr: '▩', countdown: '⏱', webpage: '🌐' }; list.innerHTML = elements.map((el, i) => `
${typeIcons[el.type] || '?'} ${el.text || el.type}
`).join('') || '

No elements yet

'; list.querySelectorAll('[data-layer]').forEach(el => { el.onclick = () => { selectedIdx = parseInt(el.dataset.layer); redraw(); }; }); } function generateInnerHTML() { let html = ''; elements.forEach((el, i) => { // Use vw units for font sizes (same as designer preview) so output scales to any viewport const fs = el.fontSize / 10; const fsLabel = el.fontSize / 15; switch (el.type) { case 'text': html += `
${el.text}
`; break; case 'clock': html += `
`; break; case 'date': html += `
`; break; case 'image': html += ``; break; case 'video': html += ``; break; case 'shape': html += `
`; break; case 'weather': html += `
Loading...
`; break; case 'ticker': html += `
Loading...
`; break; case 'countdown': html += `
${el.label}
`; break; case 'webpage': html += ``; break; } }); return html; } function generateHTML() { return `${generateInnerHTML()}`; } export function cleanup() {}