This commit is contained in:
Alvin-Zilverstand
2026-02-09 10:59:59 +01:00
parent 83c1f586af
commit 10b9ba4e61
12 changed files with 991 additions and 250 deletions

View File

@@ -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) => {

View File

@@ -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

View File

@@ -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)