From 10b9ba4e61950813a16e2dd6b765d0d39eba7214 Mon Sep 17 00:00:00 2001 From: Alvin-Zilverstand <524715@vistacollege.nl> Date: Mon, 9 Feb 2026 10:59:59 +0100 Subject: [PATCH] yes --- admin/index.html | 122 ++++++- admin/js/api.js | 14 + admin/js/ui.js | 265 +++++++++++++-- admin/styles.css | 505 ++++++++++++++++++---------- backend/database/DatabaseManager.js | 99 +++++- backend/server.js | 81 ++++- backend/services/ContentManager.js | 17 +- client/index.html | 1 + client/js/display.js | 41 ++- client/styles.css | 31 ++ docker-compose.yml | 63 ++++ package.json | 2 +- 12 files changed, 991 insertions(+), 250 deletions(-) create mode 100644 docker-compose.yml diff --git a/admin/index.html b/admin/index.html index 53f94ba..17a1331 100644 --- a/admin/index.html +++ b/admin/index.html @@ -4,6 +4,7 @@
${item.textContent ? item.textContent.substring(0, 50) + '...' : 'Tekst content'}
${zone.description}
@@ -430,6 +576,32 @@ class UIManager { `).join(''); } + selectIcon(iconName) { + // Update hidden input + document.getElementById('zoneIcon').value = iconName; + + // Update visual selection + document.querySelectorAll('.icon-option').forEach(option => { + option.classList.remove('selected'); + if (option.dataset.icon === iconName) { + option.classList.add('selected'); + } + }); + } + + openZoneModal() { + const modal = document.getElementById('zoneModal'); + if (!modal) { + console.error('Zone modal not found'); + return; + } + modal.classList.add('active'); + + // Reset icon selection to default + this.selectIcon('fa-map-marker-alt'); + } + } + // Analytics async loadAnalytics() { try { @@ -564,4 +736,23 @@ class UIManager { } // Create global UI instance -window.ui = new UIManager(); \ No newline at end of file +window.ui = new UIManager(); + +// Global helper functions for onclick handlers +window.closeModal = function() { + if (window.ui) { + window.ui.closeModals(); + } +}; + +window.closeScheduleModal = function() { + if (window.ui) { + window.ui.closeModals(); + } +}; + +window.closeZoneModal = function() { + if (window.ui) { + window.ui.closeModals(); + } +}; \ No newline at end of file diff --git a/admin/styles.css b/admin/styles.css index 3996631..a7ae057 100644 --- a/admin/styles.css +++ b/admin/styles.css @@ -98,9 +98,8 @@ body { } @keyframes pulse { - 0% { opacity: 1; } + 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } - 100% { opacity: 1; } } /* Navigation Tabs */ @@ -108,33 +107,32 @@ body { display: flex; background: var(--light-color); border-bottom: 1px solid var(--border-color); - overflow-x: auto; + padding: 0 2rem; } .nav-tab { - padding: 1rem 1.5rem; background: none; border: none; + padding: 1rem 1.5rem; cursor: pointer; font-size: 1rem; color: var(--text-secondary); + border-bottom: 3px solid transparent; transition: var(--transition); display: flex; align-items: center; gap: 0.5rem; - white-space: nowrap; } .nav-tab:hover { - background: var(--secondary-color); color: var(--primary-color); + background: var(--secondary-color); } .nav-tab.active { - background: white; color: var(--primary-color); - border-bottom: 3px solid var(--primary-color); - font-weight: 500; + border-bottom-color: var(--primary-color); + background: white; } /* Main Content */ @@ -150,34 +148,33 @@ body { display: block; } +/* Section Header */ .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; padding-bottom: 1rem; - border-bottom: 2px solid var(--border-color); + border-bottom: 1px solid var(--border-color); } .section-header h2 { color: var(--dark-color); - font-size: 1.8rem; - font-weight: 500; + font-weight: 400; } /* Buttons */ .btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; padding: 0.75rem 1.5rem; border: none; border-radius: var(--border-radius); cursor: pointer; font-size: 1rem; transition: var(--transition); - display: inline-flex; - align-items: center; - gap: 0.5rem; text-decoration: none; - font-weight: 500; } .btn-primary { @@ -191,14 +188,13 @@ body { } .btn-secondary { - background: var(--secondary-color); - color: var(--primary-color); - border: 1px solid var(--primary-color); + background: var(--light-color); + color: var(--text-primary); + border: 1px solid var(--border-color); } .btn-secondary:hover { - background: var(--primary-color); - color: white; + background: var(--border-color); } .btn-danger { @@ -212,39 +208,7 @@ body { .btn-small { padding: 0.5rem 1rem; - font-size: 0.9rem; -} - -/* Form Controls */ -.form-group { - margin-bottom: 1.5rem; -} - -.form-label { - display: block; - margin-bottom: 0.5rem; - font-weight: 500; - color: var(--text-primary); -} - -.form-control { - width: 100%; - padding: 0.75rem; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - font-size: 1rem; - transition: var(--transition); -} - -.form-control:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1); -} - -.form-select { - composes: form-control; - cursor: pointer; + font-size: 0.875rem; } /* Filter Controls */ @@ -252,11 +216,29 @@ body { display: flex; gap: 1rem; margin-bottom: 2rem; - flex-wrap: wrap; + align-items: center; } -.filter-controls .form-select { - min-width: 200px; +.form-select, +.form-control { + padding: 0.5rem 1rem; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + font-size: 1rem; + min-width: 150px; +} + +.form-control:focus, +.form-select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2); +} + +textarea.form-control { + min-height: 120px; + resize: vertical; + font-family: inherit; } /* Content Grid */ @@ -271,8 +253,8 @@ body { border-radius: var(--border-radius); box-shadow: var(--shadow); overflow: hidden; - transition: var(--transition); border: 1px solid var(--border-color); + transition: var(--transition); } .content-item:hover { @@ -286,7 +268,6 @@ body { display: flex; align-items: center; justify-content: center; - position: relative; overflow: hidden; } @@ -296,24 +277,63 @@ body { object-fit: cover; } -.content-preview.video { - background: linear-gradient(45deg, #667eea 0%, #764ba2 100%); +.content-preview i { + color: var(--text-secondary); + opacity: 0.6; } -.content-preview.video::before { - content: '▶'; - font-size: 3rem; +.content-preview.video { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.content-preview.video i { color: white; - opacity: 0.8; +} + +.content-preview.livestream { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); +} + +.content-preview.livestream i { + color: white; +} + +.content-preview.text { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); +} + +.content-preview.text i { + color: white; +} + +.text-preview { + text-align: center; + color: white; + padding: 1rem; +} + +.text-preview i { + margin-bottom: 0.5rem; +} + +.text-preview p { + font-size: 0.875rem; + opacity: 0.9; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; } .content-info { - padding: 1.5rem; + padding: 1rem; } .content-title { font-size: 1.1rem; - font-weight: 600; + font-weight: 500; margin-bottom: 0.5rem; color: var(--dark-color); } @@ -321,10 +341,16 @@ body { .content-meta { display: flex; flex-direction: column; - gap: 0.5rem; - margin-bottom: 1rem; - font-size: 0.9rem; + gap: 0.25rem; + font-size: 0.875rem; color: var(--text-secondary); + margin-bottom: 1rem; +} + +.content-meta span { + display: flex; + align-items: center; + gap: 0.25rem; } .content-actions { @@ -332,6 +358,18 @@ body { gap: 0.5rem; } +/* Empty State */ +.empty-state { + text-align: center; + padding: 3rem; + color: var(--text-secondary); +} + +.empty-state i { + margin-bottom: 1rem; + color: var(--border-color); +} + /* Modal Styles */ .modal { display: none; @@ -342,19 +380,18 @@ body { height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1000; - backdrop-filter: blur(5px); + align-items: center; + justify-content: center; } .modal.active { display: flex; - align-items: center; - justify-content: center; } .modal-content { background: white; border-radius: var(--border-radius); - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); max-width: 500px; width: 90%; max-height: 90vh; @@ -391,100 +428,79 @@ body { padding: 1.5rem; } +.form-group { + margin-bottom: 1.5rem; +} + +.form-group label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: var(--dark-color); +} + +.file-info { + margin-top: 0.5rem; + padding: 0.75rem; + background: var(--light-color); + border-radius: var(--border-radius); + font-size: 0.875rem; +} + .modal-actions { display: flex; gap: 1rem; justify-content: flex-end; margin-top: 2rem; - padding-top: 1rem; - border-top: 1px solid var(--border-color); } -/* Schedule Timeline */ -.schedule-timeline { +/* Schedule Styles */ +.schedule-container { + display: grid; + grid-template-columns: 300px 1fr; + gap: 2rem; +} + +.zone-selector { background: var(--light-color); + padding: 1.5rem; + border-radius: var(--border-radius); + height: fit-content; +} + +.schedule-timeline { + background: white; + border: 1px solid var(--border-color); border-radius: var(--border-radius); padding: 1.5rem; - margin-top: 1rem; + min-height: 400px; } .schedule-item { display: flex; align-items: center; + gap: 1rem; padding: 1rem; - background: white; + border-left: 4px solid var(--primary-color); + background: var(--light-color); border-radius: var(--border-radius); margin-bottom: 1rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - border-left: 4px solid var(--primary-color); } .schedule-time { - font-weight: 600; + font-weight: 500; color: var(--primary-color); - min-width: 150px; -} - -.schedule-content { - flex: 1; - margin-left: 1rem; + min-width: 120px; } .schedule-content h4 { - margin-bottom: 0.25rem; - color: var(--dark-color); + margin: 0 0 0.25rem 0; } .schedule-content p { + margin: 0; + font-size: 0.875rem; color: var(--text-secondary); - font-size: 0.9rem; -} - -/* Analytics */ -.analytics-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 1.5rem; -} - -.analytics-card { - background: white; - border-radius: var(--border-radius); - padding: 1.5rem; - box-shadow: var(--shadow); - border: 1px solid var(--border-color); -} - -.analytics-card h3 { - margin-bottom: 1rem; - color: var(--dark-color); - font-size: 1.2rem; -} - -.stats-container { - display: flex; - flex-direction: column; - gap: 1rem; -} - -.stat-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem; - background: var(--light-color); - border-radius: var(--border-radius); -} - -.stat-label { - font-weight: 500; - color: var(--text-secondary); -} - -.stat-value { - font-weight: 600; - color: var(--primary-color); - font-size: 1.1rem; } /* Zones Grid */ @@ -497,10 +513,10 @@ body { .zone-card { background: white; border-radius: var(--border-radius); - padding: 1.5rem; box-shadow: var(--shadow); - border: 1px solid var(--border-color); + padding: 2rem; text-align: center; + border: 1px solid var(--border-color); transition: var(--transition); } @@ -510,57 +526,114 @@ body { } .zone-icon { - font-size: 3rem; - color: var(--primary-color); margin-bottom: 1rem; } +.zone-icon i { + color: var(--primary-color); +} + .zone-name { - font-size: 1.2rem; - font-weight: 600; - color: var(--dark-color); + font-size: 1.25rem; + font-weight: 500; margin-bottom: 0.5rem; + color: var(--dark-color); } .zone-description { color: var(--text-secondary); - font-size: 0.9rem; + font-size: 0.875rem; +} + +/* Analytics */ +.analytics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 1.5rem; +} + +.analytics-card { + background: white; + border-radius: var(--border-radius); + box-shadow: var(--shadow); + padding: 1.5rem; + border: 1px solid var(--border-color); +} + +.analytics-card h3 { + margin-bottom: 1rem; + color: var(--dark-color); + font-weight: 500; +} + +.stats-container { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.stat-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem; + background: var(--light-color); + border-radius: var(--border-radius); +} + +.stat-label { + color: var(--text-secondary); +} + +.stat-value { + font-weight: 500; + color: var(--dark-color); } /* Toast Notifications */ .toast-container { position: fixed; - top: 20px; - right: 20px; - z-index: 1100; + top: 2rem; + right: 2rem; + z-index: 2000; display: flex; flex-direction: column; - gap: 10px; + gap: 0.5rem; } .toast { - background: white; - border-radius: var(--border-radius); + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; padding: 1rem 1.5rem; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - border-left: 4px solid var(--success-color); - min-width: 300px; - animation: slideInRight 0.3s ease; + border-radius: var(--border-radius); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); + animation: slideIn 0.3s ease; + max-width: 400px; +} + +.toast.success { + background: var(--success-color); + color: white; } .toast.error { - border-left-color: var(--danger-color); -} - -.toast.warning { - border-left-color: var(--warning-color); + background: var(--danger-color); + color: white; } .toast.info { - border-left-color: var(--info-color); + background: var(--primary-color); + color: white; } -@keyframes slideInRight { +.toast.warning { + background: var(--warning-color); + color: var(--dark-color); +} + +@keyframes slideIn { from { transform: translateX(100%); opacity: 0; @@ -571,6 +644,46 @@ body { } } +.toast-close { + background: none; + border: none; + color: inherit; + cursor: pointer; + font-size: 1.25rem; + opacity: 0.7; + transition: var(--transition); +} + +.toast-close:hover { + opacity: 1; +} + +/* Loading Overlay */ +.loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 3000; +} + +.loading-content { + background: white; + padding: 2rem; + border-radius: var(--border-radius); + text-align: center; +} + +.loading-content p { + margin-top: 1rem; + color: var(--text-secondary); +} + /* Responsive Design */ @media (max-width: 768px) { .container { @@ -580,30 +693,24 @@ body { .header-content { flex-direction: column; gap: 1rem; - text-align: center; } .nav-tabs { flex-wrap: wrap; - } - - .nav-tab { - flex: 1; - min-width: 150px; + padding: 0 1rem; } .main-content { padding: 1rem; } - .section-header { - flex-direction: column; - gap: 1rem; - text-align: center; - } - .filter-controls { flex-direction: column; + align-items: stretch; + } + + .schedule-container { + grid-template-columns: 1fr; } .content-grid { @@ -615,8 +722,8 @@ body { } .modal-content { - width: 95%; margin: 1rem; + max-width: none; } } @@ -663,4 +770,46 @@ body { .analytics-card { border-top: 3px solid var(--accent-color); -} \ No newline at end of file +} + +/* Icon Selector Styles */ +.icon-selector { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0.5rem; + margin-top: 0.5rem; +} + +.icon-option { + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + border: 2px solid var(--border-color); + border-radius: var(--border-radius); + cursor: pointer; + transition: var(--transition); + background: white; +} + +.icon-option:hover { + border-color: var(--primary-color); + background: var(--secondary-color); + transform: scale(1.05); +} + +.icon-option.selected { + border-color: var(--primary-color); + background: var(--primary-color); + color: white; +} + +.icon-option i { + font-size: 1.5rem; +} + +@media (max-width: 768px) { + .icon-selector { + grid-template-columns: repeat(4, 1fr); + } +} diff --git a/backend/database/DatabaseManager.js b/backend/database/DatabaseManager.js index 1fd67db..b089b02 100644 --- a/backend/database/DatabaseManager.js +++ b/backend/database/DatabaseManager.js @@ -61,6 +61,7 @@ class DatabaseManager { id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, + icon TEXT DEFAULT 'fa-map-marker-alt', displayOrder INTEGER DEFAULT 0, isActive INTEGER DEFAULT 1 ) @@ -82,23 +83,23 @@ class DatabaseManager { this.db.run(zonesTable); this.db.run(logsTable); - // Insert default zones + // Insert default zones with icons const defaultZones = [ - { id: 'reception', name: 'Receptie', description: 'Hoofdingang en receptie', displayOrder: 1 }, - { id: 'restaurant', name: 'Restaurant', description: 'Eetgelegenheid', displayOrder: 2 }, - { id: 'skislope', name: 'Skibaan', description: 'Hoofdskibaan', displayOrder: 3 }, - { id: 'lockers', name: 'Kluisjes', description: 'Kleedkamers en kluisjes', displayOrder: 4 }, - { id: 'shop', name: 'Winkel', description: 'Ski-uitrusting winkel', displayOrder: 5 }, - { id: 'all', name: 'Alle zones', description: 'Toon op alle schermen', displayOrder: 0 } + { id: 'reception', name: 'Receptie', description: 'Hoofdingang en receptie', icon: 'fa-door-open', displayOrder: 1 }, + { id: 'restaurant', name: 'Restaurant', description: 'Eetgelegenheid', icon: 'fa-utensils', displayOrder: 2 }, + { id: 'skislope', name: 'Skibaan', description: 'Hoofdskibaan', icon: 'fa-skiing', displayOrder: 3 }, + { id: 'lockers', name: 'Kluisjes', description: 'Kleedkamers en kluisjes', icon: 'fa-locker', displayOrder: 4 }, + { id: 'shop', name: 'Winkel', description: 'Ski-uitrusting winkel', icon: 'fa-shopping-bag', displayOrder: 5 }, + { id: 'all', name: 'Alle zones', description: 'Toon op alle schermen', icon: 'fa-globe', displayOrder: 0 } ]; const stmt = this.db.prepare(` - INSERT OR IGNORE INTO zones (id, name, description, displayOrder) - VALUES (?, ?, ?, ?) + INSERT OR IGNORE INTO zones (id, name, description, icon, displayOrder) + VALUES (?, ?, ?, ?, ?) `); defaultZones.forEach(zone => { - stmt.run(zone.id, zone.name, zone.description, zone.displayOrder); + stmt.run(zone.id, zone.name, zone.description, zone.icon, zone.displayOrder); }); stmt.finalize(); @@ -111,22 +112,57 @@ class DatabaseManager { async addContent(contentData) { return new Promise((resolve, reject) => { const stmt = this.db.prepare(` - INSERT INTO content (id, type, title, filename, originalName, mimeType, size, path, url, zone, duration, createdAt) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO content (id, type, title, filename, originalName, mimeType, size, path, url, zone, duration, textContent, createdAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run( contentData.id, contentData.type, contentData.title, - contentData.filename, - contentData.originalName, - contentData.mimeType, - contentData.size, - contentData.path, - contentData.url, + contentData.filename || '', + contentData.originalName || '', + contentData.mimeType || '', + contentData.size || 0, + contentData.path || '', + contentData.url || '', contentData.zone, contentData.duration, + contentData.textContent || null, + contentData.createdAt, + function(err) { + if (err) { + reject(err); + } else { + resolve(contentData); + } + } + ); + + stmt.finalize(); + }); + } + + async addTextContent(contentData) { + return new Promise((resolve, reject) => { + const stmt = this.db.prepare(` + INSERT INTO content (id, type, title, filename, originalName, mimeType, size, path, url, zone, duration, textContent, createdAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + stmt.run( + contentData.id, + 'text', + contentData.title, + '', + '', + 'text/plain', + 0, + '', + '', + contentData.zone, + contentData.duration, + contentData.textContent, contentData.createdAt, function(err) { if (err) { @@ -257,6 +293,33 @@ class DatabaseManager { }); } + async addZone(zoneData) { + return new Promise((resolve, reject) => { + const stmt = this.db.prepare(` + INSERT INTO zones (id, name, description, icon, displayOrder, isActive) + VALUES (?, ?, ?, ?, ?, ?) + `); + + stmt.run( + zoneData.id, + zoneData.name, + zoneData.description || '', + zoneData.icon || 'fa-map-marker-alt', + zoneData.displayOrder || 0, + 1, + function(err) { + if (err) { + reject(err); + } else { + resolve(zoneData); + } + } + ); + + stmt.finalize(); + }); + } + // Logging async addLog(type, message, data = null) { return new Promise((resolve, reject) => { diff --git a/backend/server.js b/backend/server.js index 9f53d5e..abc9174 100644 --- a/backend/server.js +++ b/backend/server.js @@ -95,6 +95,39 @@ app.post('/api/content/upload', upload.single('content'), async (req, res) => { } }); +// Text Content Management +app.post('/api/content/text', async (req, res) => { + try { + const { title, textContent, zone, duration } = req.body; + + if (!title || !textContent) { + return res.status(400).json({ error: 'Title and text content are required' }); + } + + const contentData = { + id: uuidv4(), + title: title, + textContent: textContent, + zone: zone || 'all', + duration: parseInt(duration) || 15, + createdAt: new Date().toISOString() + }; + + const content = await contentManager.addTextContent(contentData); + + // Emit real-time update + io.emit('contentUpdated', { + type: 'content_added', + content: content + }); + + res.json({ success: true, content }); + } catch (error) { + console.error('Text content creation error:', error); + res.status(500).json({ error: 'Failed to create text content' }); + } +}); + app.get('/api/content', async (req, res) => { try { const { zone, type } = req.query; @@ -172,16 +205,44 @@ app.get('/api/schedule/:zone', async (req, res) => { } }); -app.get('/api/zones', (req, res) => { - const zones = [ - { id: 'reception', name: 'Receptie', description: 'Hoofdingang en receptie' }, - { id: 'restaurant', name: 'Restaurant', description: 'Eetgelegenheid' }, - { id: 'skislope', name: 'Skibaan', description: 'Hoofdskibaan' }, - { id: 'lockers', name: 'Kluisjes', description: 'Kleedkamers en kluisjes' }, - { id: 'shop', name: 'Winkel', description: 'Ski-uitrusting winkel' }, - { id: 'all', name: 'Alle zones', description: 'Toon op alle schermen' } - ]; - res.json(zones); +app.get('/api/zones', async (req, res) => { + try { + const zones = await dbManager.getZones(); + res.json(zones); + } catch (error) { + console.error('Get zones error:', error); + res.status(500).json({ error: 'Failed to retrieve zones' }); + } +}); + +app.post('/api/zones', async (req, res) => { + try { + const { id, name, description, icon, displayOrder } = req.body; + + if (!id || !name) { + return res.status(400).json({ error: 'Zone ID and name are required' }); + } + + const zoneData = { + id: id.toLowerCase().replace(/\s+/g, '-'), + name: name, + description: description || '', + icon: icon || 'fa-map-marker-alt', + displayOrder: parseInt(displayOrder) || 0 + }; + + const zone = await dbManager.addZone(zoneData); + + io.emit('zonesUpdated', { + type: 'zone_added', + zone: zone + }); + + res.json({ success: true, zone }); + } catch (error) { + console.error('Create zone error:', error); + res.status(500).json({ error: 'Failed to create zone' }); + } }); // Weather widget data diff --git a/backend/services/ContentManager.js b/backend/services/ContentManager.js index 1aef60c..007c72e 100644 --- a/backend/services/ContentManager.js +++ b/backend/services/ContentManager.js @@ -14,6 +14,17 @@ class ContentManager { } } + async addTextContent(contentData) { + try { + const content = await this.db.addTextContent(contentData); + await this.db.addLog('content', 'Text content added', { contentId: content.id, type: 'text' }); + return content; + } catch (error) { + console.error('Error adding text content:', error); + throw error; + } + } + async getContent(zone = null, type = null) { try { return await this.db.getContent(zone, type); @@ -92,7 +103,8 @@ class ContentManager { const allowedTypes = { 'image': ['image/jpeg', 'image/png', 'image/gif', 'image/webp'], 'video': ['video/mp4', 'video/webm', 'video/ogg'], - 'livestream': ['application/x-mpegURL', 'application/vnd.apple.mpegurl'] + 'livestream': ['application/x-mpegURL', 'application/vnd.apple.mpegurl'], + 'text': ['text/plain'] }; for (const [type, mimeTypes] of Object.entries(allowedTypes)) { @@ -109,7 +121,8 @@ class ContentManager { const defaultDurations = { 'image': 10, 'video': 30, - 'livestream': 3600 // 1 hour for livestreams + 'livestream': 3600, // 1 hour for livestreams + 'text': 15 }; // For videos, estimate duration based on file size (rough approximation) diff --git a/client/index.html b/client/index.html index dae1740..0e697e5 100644 --- a/client/index.html +++ b/client/index.html @@ -4,6 +4,7 @@