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

@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SnowWorld - Narrowcasting Admin Dashboard</title>
<link rel="icon" type="image/svg+xml" href="http://localhost:3000/favicon.svg">
<link rel="stylesheet" href="styles.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
@@ -65,6 +66,7 @@
<option value="image">Afbeeldingen</option>
<option value="video">Video's</option>
<option value="livestream">Livestreams</option>
<option value="text">Tekst</option>
</select>
<button id="applyFilters" class="btn btn-secondary">Toepassen</button>
</div>
@@ -102,6 +104,9 @@
<div id="zones-tab" class="tab-content">
<div class="section-header">
<h2>Zone Overzicht</h2>
<button id="addZoneBtn" class="btn btn-primary">
<i class="fas fa-plus"></i> Zone Toevoegen
</button>
</div>
<div id="zonesGrid" class="zones-grid">
@@ -161,6 +166,7 @@
<option value="image">Afbeelding</option>
<option value="video">Video</option>
<option value="livestream">Livestream</option>
<option value="text">Tekst</option>
</select>
</div>
@@ -176,12 +182,19 @@
<input type="number" id="contentDuration" class="form-control" min="5" max="300" value="10">
</div>
<div class="form-group">
<!-- File upload field (shown for image/video) -->
<div id="fileUploadGroup" class="form-group">
<label for="contentFile">Bestand:</label>
<input type="file" id="contentFile" class="form-control" accept="image/*,video/*" required>
<input type="file" id="contentFile" class="form-control" accept="image/*,video/*">
<div id="fileInfo" class="file-info"></div>
</div>
<!-- Text content field (shown for text type) -->
<div id="textContentGroup" class="form-group" style="display: none;">
<label for="textContent">Tekst:</label>
<textarea id="textContent" class="form-control" rows="6" placeholder="Voer hier uw tekst in..."></textarea>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal()">Annuleren</button>
<button type="submit" class="btn btn-primary">Uploaden</button>
@@ -239,6 +252,111 @@
</div>
</div>
<!-- Zone Modal -->
<div id="zoneModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Zone Toevoegen</h3>
<button class="close-btn">&times;</button>
</div>
<form id="zoneForm" class="modal-body">
<div class="form-group">
<label for="zoneId">Zone ID (uniek):</label>
<input type="text" id="zoneId" class="form-control" placeholder="bijv. nieuwe-zone" required>
<small>Alleen kleine letters, cijfers en streepjes. Gebruikt in URLs.</small>
</div>
<div class="form-group">
<label for="zoneName">Zone Naam:</label>
<input type="text" id="zoneName" class="form-control" placeholder="bijv. Nieuwe Zone" required>
</div>
<div class="form-group">
<label for="zoneDescription">Beschrijving:</label>
<textarea id="zoneDescription" class="form-control" rows="3" placeholder="Optionele beschrijving van de zone..."></textarea>
</div>
<div class="form-group">
<label for="zoneDisplayOrder">Weergave Volgorde:</label>
<input type="number" id="zoneDisplayOrder" class="form-control" min="0" value="0">
<small>Lager nummer = eerder in de lijst</small>
</div>
<div class="form-group">
<label>Zone Icoon:</label>
<input type="hidden" id="zoneIcon" value="fa-map-marker-alt">
<div class="icon-selector">
<div class="icon-option selected" data-icon="fa-map-marker-alt" title="Standaard">
<i class="fas fa-map-marker-alt"></i>
</div>
<div class="icon-option" data-icon="fa-door-open" title="Ingang">
<i class="fas fa-door-open"></i>
</div>
<div class="icon-option" data-icon="fa-utensils" title="Restaurant">
<i class="fas fa-utensils"></i>
</div>
<div class="icon-option" data-icon="fa-skiing" title="Ski">
<i class="fas fa-skiing"></i>
</div>
<div class="icon-option" data-icon="fa-snowflake" title="Sneeuw">
<i class="fas fa-snowflake"></i>
</div>
<div class="icon-option" data-icon="fa-locker" title="Kluisjes">
<i class="fas fa-locker"></i>
</div>
<div class="icon-option" data-icon="fa-shopping-bag" title="Winkel">
<i class="fas fa-shopping-bag"></i>
</div>
<div class="icon-option" data-icon="fa-globe" title="Wereld">
<i class="fas fa-globe"></i>
</div>
<div class="icon-option" data-icon="fa-home" title="Thuis">
<i class="fas fa-home"></i>
</div>
<div class="icon-option" data-icon="fa-building" title="Gebouw">
<i class="fas fa-building"></i>
</div>
<div class="icon-option" data-icon="fa-mountain" title="Berg">
<i class="fas fa-mountain"></i>
</div>
<div class="icon-option" data-icon="fa-tree" title="Boom">
<i class="fas fa-tree"></i>
</div>
<div class="icon-option" data-icon="fa-sun" title="Zon">
<i class="fas fa-sun"></i>
</div>
<div class="icon-option" data-icon="fa-moon" title="Maan">
<i class="fas fa-moon"></i>
</div>
<div class="icon-option" data-icon="fa-star" title="Ster">
<i class="fas fa-star"></i>
</div>
<div class="icon-option" data-icon="fa-heart" title="Hart">
<i class="fas fa-heart"></i>
</div>
<div class="icon-option" data-icon="fa-info-circle" title="Info">
<i class="fas fa-info-circle"></i>
</div>
<div class="icon-option" data-icon="fa-exclamation-circle" title="Waarschuwing">
<i class="fas fa-exclamation-circle"></i>
</div>
<div class="icon-option" data-icon="fa-check-circle" title="Goed">
<i class="fas fa-check-circle"></i>
</div>
<div class="icon-option" data-icon="fa-users" title="Mensen">
<i class="fas fa-users"></i>
</div>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick="closeZoneModal()">Annuleren</button>
<button type="submit" class="btn btn-primary">Toevoegen</button>
</div>
</form>
</div>
</div>
<!-- Toast Notifications -->
<div id="toastContainer" class="toast-container">
<!-- Toast notifications will appear here -->

View File

@@ -50,6 +50,13 @@ class APIService {
});
}
async createTextContent(textData) {
return this.request('/content/text', {
method: 'POST',
body: JSON.stringify(textData)
});
}
async deleteContent(contentId) {
return this.request(`/content/${contentId}`, {
method: 'DELETE'
@@ -73,6 +80,13 @@ class APIService {
return this.request('/zones');
}
async createZone(zoneData) {
return this.request('/zones', {
method: 'POST',
body: JSON.stringify(zoneData)
});
}
// Weather Data
async getWeatherData() {
return this.request('/weather');

View File

@@ -62,6 +62,29 @@ class UIManager {
document.getElementById('contentFile')?.addEventListener('change', (e) => {
this.previewFile(e.target.files[0]);
});
// Content type change - show/hide appropriate fields
document.getElementById('contentType')?.addEventListener('change', (e) => {
this.handleContentTypeChange(e.target.value);
});
// Zone management
document.getElementById('addZoneBtn')?.addEventListener('click', () => {
this.openZoneModal();
});
document.getElementById('zoneForm')?.addEventListener('submit', (e) => {
e.preventDefault();
this.createZone();
});
// Icon selector
document.querySelectorAll('.icon-option').forEach(option => {
option.addEventListener('click', (e) => {
const icon = e.currentTarget.dataset.icon;
this.selectIcon(icon);
});
});
}
// Tab Management
@@ -148,13 +171,15 @@ class UIManager {
const typeIcon = {
'image': 'fa-image',
'video': 'fa-video',
'livestream': 'fa-broadcast-tower'
'livestream': 'fa-broadcast-tower',
'text': 'fa-font'
}[item.type] || 'fa-file';
const typeLabel = {
'image': 'Afbeelding',
'video': 'Video',
'livestream': 'Livestream'
'livestream': 'Livestream',
'text': 'Tekst'
}[item.type] || 'Bestand';
return `
@@ -162,6 +187,8 @@ class UIManager {
<div class="content-preview ${item.type}">
${item.type === 'image' ?
`<img src="${item.url}" alt="${item.title}" onerror="this.src='https://via.placeholder.com/300x200?text=Afbeelding'">` :
item.type === 'text' ?
`<div class="text-preview"><i class="fas ${typeIcon} fa-3x"></i><p>${item.textContent ? item.textContent.substring(0, 50) + '...' : 'Tekst content'}</p></div>` :
`<i class="fas ${typeIcon} fa-3x"></i>`
}
</div>
@@ -184,10 +211,19 @@ class UIManager {
}
// Modal Management
openContentModal() {
async openContentModal() {
const modal = document.getElementById('contentModal');
if (!modal) {
console.error('Content modal not found');
return;
}
modal.classList.add('active');
this.loadZonesSelect('contentZone');
// Reset form fields visibility before loading zones
this.handleContentTypeChange('');
// Load zones into dropdown
await this.loadZonesSelect('contentZone');
}
openScheduleModal() {
@@ -206,7 +242,17 @@ class UIManager {
// Reset forms
document.getElementById('contentUploadForm')?.reset();
document.getElementById('scheduleForm')?.reset();
document.getElementById('zoneForm')?.reset();
document.getElementById('fileInfo').innerHTML = '';
// Reset text content field
const textContent = document.getElementById('textContent');
if (textContent) {
textContent.value = '';
}
// Reset form fields visibility
this.handleContentTypeChange('');
}
// Content Upload
@@ -232,39 +278,85 @@ class UIManager {
}
}
handleContentTypeChange(type) {
const fileUploadGroup = document.getElementById('fileUploadGroup');
const textContentGroup = document.getElementById('textContentGroup');
const contentFile = document.getElementById('contentFile');
const textContent = document.getElementById('textContent');
if (type === 'text') {
if (fileUploadGroup) fileUploadGroup.style.display = 'none';
if (textContentGroup) textContentGroup.style.display = 'block';
if (contentFile) contentFile.removeAttribute('required');
if (textContent) textContent.setAttribute('required', 'required');
} else {
if (fileUploadGroup) fileUploadGroup.style.display = 'block';
if (textContentGroup) textContentGroup.style.display = 'none';
if (contentFile) contentFile.setAttribute('required', 'required');
if (textContent) textContent.removeAttribute('required');
}
}
async uploadContent() {
const form = document.getElementById('contentUploadForm');
const formData = new FormData();
const fileInput = document.getElementById('contentFile');
const title = document.getElementById('contentTitle').value;
const type = document.getElementById('contentType').value;
const zone = document.getElementById('contentZone').value;
const duration = document.getElementById('contentDuration').value;
if (!fileInput.files[0]) {
this.showToast('Selecteer een bestand', 'error');
if (!type) {
this.showToast('Selecteer een type', 'error');
return;
}
formData.append('content', fileInput.files[0]);
formData.append('title', title);
formData.append('type', type);
formData.append('zone', zone);
formData.append('duration', duration);
try {
this.showLoading('Bezig met uploaden...');
const result = await api.uploadContent(formData);
this.showLoading('Bezig met opslaan...');
if (type === 'text') {
// Handle text content
const textContent = document.getElementById('textContent').value;
if (!textContent.trim()) {
this.showToast('Voer tekst in', 'error');
this.hideLoading();
return;
}
const textData = {
title: title,
textContent: textContent,
zone: zone,
duration: parseInt(duration)
};
const result = await api.createTextContent(textData);
} else {
// Handle file upload
const fileInput = document.getElementById('contentFile');
if (!fileInput.files[0]) {
this.showToast('Selecteer een bestand', 'error');
this.hideLoading();
return;
}
const formData = new FormData();
formData.append('content', fileInput.files[0]);
formData.append('title', title);
formData.append('type', type);
formData.append('zone', zone);
formData.append('duration', duration);
const result = await api.uploadContent(formData);
}
this.closeModals();
this.clearContentCache();
await this.loadContent();
this.showToast('Content succesvol geüpload!', 'success');
this.showToast('Content succesvol opgeslagen!', 'success');
} catch (error) {
console.error('Upload error:', error);
this.showToast('Upload mislukt: ' + error.message, 'error');
this.showToast('Opslaan mislukt: ' + error.message, 'error');
} finally {
this.hideLoading();
}
@@ -368,6 +460,50 @@ class UIManager {
document.getElementById('scheduleEnd').value = endTime.toISOString().slice(0, 16);
}
async createZone() {
const zoneId = document.getElementById('zoneId').value.trim();
const zoneName = document.getElementById('zoneName').value.trim();
const zoneDescription = document.getElementById('zoneDescription').value.trim();
const zoneDisplayOrder = document.getElementById('zoneDisplayOrder').value;
const zoneIcon = document.getElementById('zoneIcon').value;
if (!zoneId || !zoneName) {
this.showToast('Zone ID en naam zijn verplicht', 'error');
return;
}
// Validate zone ID format (lowercase letters, numbers, hyphens only)
const validIdPattern = /^[a-z0-9-]+$/;
if (!validIdPattern.test(zoneId)) {
this.showToast('Zone ID mag alleen kleine letters, cijfers en streepjes bevatten', 'error');
return;
}
const zoneData = {
id: zoneId,
name: zoneName,
description: zoneDescription,
icon: zoneIcon,
displayOrder: parseInt(zoneDisplayOrder) || 0
};
try {
this.showLoading('Bezig met toevoegen...');
await api.createZone(zoneData);
this.closeModals();
this.zonesCache = null; // Clear cache
await this.loadZonesOverview();
this.showToast('Zone succesvol toegevoegd!', 'success');
} catch (error) {
console.error('Zone creation error:', error);
this.showToast('Zone toevoegen mislukt: ' + error.message, 'error');
} finally {
this.hideLoading();
}
}
// Zones Management
async loadZones() {
if (this.zonesCache) return this.zonesCache;
@@ -382,13 +518,32 @@ class UIManager {
}
async loadZonesSelect(selectId) {
const zones = await this.loadZones();
const select = document.getElementById(selectId);
if (!select) return;
try {
const zones = await this.loadZones();
const select = document.getElementById(selectId);
if (!select) {
console.error(`Select element with id '${selectId}' not found`);
return;
}
select.innerHTML = zones.map(zone =>
`<option value="${zone.id}">${zone.name}</option>`
).join('');
if (!zones || zones.length === 0) {
console.error('No zones loaded');
select.innerHTML = '<option value="">Geen zones beschikbaar</option>';
return;
}
select.innerHTML = zones.map(zone =>
`<option value="${zone.id}">${zone.name}</option>`
).join('');
console.log(`Loaded ${zones.length} zones into ${selectId}`);
} catch (error) {
console.error('Error in loadZonesSelect:', error);
const select = document.getElementById(selectId);
if (select) {
select.innerHTML = '<option value="">Fout bij laden zones</option>';
}
}
}
async loadContentSelect() {
@@ -410,19 +565,10 @@ class UIManager {
const grid = document.getElementById('zonesGrid');
if (!grid) return;
const zoneIcons = {
'reception': 'fa-door-open',
'restaurant': 'fa-utensils',
'skislope': 'fa-skiing',
'lockers': 'fa-locker',
'shop': 'fa-shopping-bag',
'all': 'fa-globe'
};
grid.innerHTML = zones.map(zone => `
<div class="zone-card">
<div class="zone-icon">
<i class="fas ${zoneIcons[zone.id] || 'fa-map-marker-alt'} fa-3x"></i>
<i class="fas ${zone.icon || 'fa-map-marker-alt'} fa-3x"></i>
</div>
<h3 class="zone-name">${zone.name}</h3>
<p class="zone-description">${zone.description}</p>
@@ -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();
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();
}
};

View File

@@ -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);
}
}
/* 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);
}
}