initial commit
All checks were successful
Build and Push Image / build-and-push (push) Successful in 1m26s

This commit is contained in:
Michael Trip 2026-01-09 21:58:53 +01:00
parent 3bba1f6db6
commit b56e866071
36 changed files with 4160 additions and 0 deletions

433
app/static/css/style.css Normal file
View file

@ -0,0 +1,433 @@
/* Mobile-first CSS voor Snauw Counter */
/* Reset en basis styling */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f8f9fa;
min-height: 100vh;
}
/* Container */
.container {
max-width: 100%;
padding: 1rem;
margin: 0 auto;
}
/* Header */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1rem 0;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
/* Banner Section */
.banner-section {
text-align: center;
margin-bottom: 1rem;
}
.banner-image {
display: inline-block;
max-width: 100%;
margin: 0 auto;
}
.snauw-banner {
width: 100%;
max-width: 600px;
height: auto;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
background: #2c3e50;
}
@media (max-width: 768px) {
.snauw-banner {
max-width: 95%;
}
.banner-section {
margin-bottom: 0.5rem;
}
}
.header h1 {
font-size: 1.5rem;
font-weight: 600;
text-align: center;
}
.nav {
display: flex;
justify-content: center;
margin-top: 1rem;
gap: 1rem;
}
.nav a {
color: rgba(255,255,255,0.9);
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 20px;
transition: all 0.3s ease;
font-size: 0.9rem;
}
.nav a:hover,
.nav a.active {
background: rgba(255,255,255,0.2);
color: white;
}
/* Cards */
.card {
background: white;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1rem;
box-shadow: 0 2px 20px rgba(0,0,0,0.08);
border: 1px solid #e9ecef;
}
/* Buttons */
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
text-decoration: none;
text-align: center;
transition: all 0.3s ease;
width: 100%;
margin-bottom: 0.5rem;
}
.btn:hover {
background: #5a67d8;
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #6c757d;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-danger {
background: #dc3545;
}
.btn-danger:hover {
background: #c82333;
}
.btn-success {
background: #28a745;
}
.btn-success:hover {
background: #218838;
}
/* Forms */
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #495057;
}
input[type="email"],
input[type="password"],
input[type="number"],
input[type="datetime-local"],
textarea,
select {
width: 100%;
padding: 0.75rem;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.3s ease;
background: white;
}
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
textarea {
min-height: 100px;
resize: vertical;
}
/* Severity selector - speciale styling voor Snauw-index */
.severity-selector {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0.5rem;
margin: 1rem 0;
}
.severity-btn {
aspect-ratio: 1;
border: 2px solid #e9ecef;
background: white;
border-radius: 8px;
font-size: 1.2rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.severity-btn:hover {
border-color: #667eea;
background: #f8f9ff;
}
.severity-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
/* Snauw-index specific styling */
.snauw-phases div {
margin-bottom: 0.75rem;
padding: 0.5rem;
border-left: 3px solid #e9ecef;
padding-left: 0.75rem;
}
.snauw-phases div:nth-child(1) { border-left-color: #28a745; } /* Fase 1 - Groen */
.snauw-phases div:nth-child(2) { border-left-color: #ffc107; } /* Fase 2 - Geel */
.snauw-phases div:nth-child(3) { border-left-color: #fd7e14; } /* Fase 3 - Oranje */
.snauw-phases div:nth-child(4) { border-left-color: #dc3545; } /* Fase 4 - Rood */
.snauw-phases div:nth-child(5) { border-left-color: #6f42c1; } /* Fase 5 - Paars */
/* Color coding for severity buttons */
.severity-btn[data-severity="1"] { border-color: #28a745; color: #28a745; }
.severity-btn[data-severity="2"] { border-color: #ffc107; color: #ffc107; }
.severity-btn[data-severity="3"] { border-color: #fd7e14; color: #fd7e14; }
.severity-btn[data-severity="4"] { border-color: #dc3545; color: #dc3545; }
.severity-btn[data-severity="5"] { border-color: #6f42c1; color: #6f42c1; }
.severity-btn[data-severity="1"]:hover,
.severity-btn[data-severity="1"].active {
background: #28a745;
border-color: #28a745;
color: white;
}
.severity-btn[data-severity="2"]:hover,
.severity-btn[data-severity="2"].active {
background: #ffc107;
border-color: #ffc107;
color: white;
}
.severity-btn[data-severity="3"]:hover,
.severity-btn[data-severity="3"].active {
background: #fd7e14;
border-color: #fd7e14;
color: white;
}
.severity-btn[data-severity="4"]:hover,
.severity-btn[data-severity="4"].active {
background: #dc3545;
border-color: #dc3545;
color: white;
}
.severity-btn[data-severity="5"]:hover,
.severity-btn[data-severity="5"].active {
background: #6f42c1;
border-color: #6f42c1;
color: white;
}
/* Statistieken */
.stats-grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1.5rem;
border-radius: 12px;
text-align: center;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
display: block;
margin-bottom: 0.5rem;
}
.stat-label {
font-size: 0.9rem;
opacity: 0.9;
}
/* Chart container */
.chart-container {
position: relative;
height: 300px;
margin: 1rem 0;
}
/* Flash messages */
.flash-messages {
margin-bottom: 1rem;
}
.alert {
padding: 0.75rem 1rem;
border-radius: 8px;
margin-bottom: 0.5rem;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.alert-info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
/* Privacy notice */
.privacy-notice {
background: #fff3cd;
color: #856404;
padding: 1rem;
border-radius: 8px;
border: 1px solid #ffeaa7;
margin-bottom: 1rem;
font-size: 0.9rem;
}
/* Quick add incident floating button */
.fab {
position: fixed;
bottom: 2rem;
right: 2rem;
width: 60px;
height: 60px;
border-radius: 50%;
background: #667eea;
color: white;
border: none;
font-size: 1.5rem;
cursor: pointer;
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
z-index: 1000;
}
.fab:hover {
background: #5a67d8;
transform: scale(1.1);
}
/* Responsive design - Tablet */
@media (min-width: 768px) {
.container {
max-width: 768px;
padding: 2rem;
}
.header h1 {
font-size: 2rem;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.severity-selector {
grid-template-columns: repeat(5, 1fr);
gap: 1rem;
}
}
/* Desktop */
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
.stats-grid {
grid-template-columns: repeat(4, 1fr);
}
.btn {
width: auto;
margin-right: 0.5rem;
}
}
/* Animations */
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
#header-counter, #counter-digit-1, #counter-digit-2, #counter-digit-3 {
transition: all 0.3s ease;
font-family: 'Courier New', monospace;
text-shadow: 0 0 10px rgba(231, 76, 60, 0.5);
}
#header-counter.updated,
#counter-digit-1.updated,
#counter-digit-2.updated,
#counter-digit-3.updated {
animation: pulse 1s ease-in-out 3;
color: #f39c12 !important;
}

View file

@ -0,0 +1 @@
Not Found

BIN
app/static/img/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

573
app/static/js/app.js Normal file
View file

@ -0,0 +1,573 @@
// Snauw Counter - Frontend JavaScript
// Global variables
let selectedSeverity = null;
// Fast datetime initialization - runs as soon as script loads
function fastDateTimeInit() {
const dateTimeInput = document.getElementById('incident_time');
if (dateTimeInput) {
const now = new Date();
const formattedDateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}T${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
dateTimeInput.value = formattedDateTime;
}
}
// Try to initialize datetime immediately if elements exist
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fastDateTimeInit);
} else {
fastDateTimeInit();
}
// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Initialize date/time FIRST - before any user interaction
initializeDateTime();
trackDateTimeModification();
// Then initialize other components
initializeSeveritySelector();
initializeAnonymousUserSupport();
loadChart();
updateHeaderCounter();
// Hide privacy notice als gebruiker het al eerder heeft weggedaan
if (localStorage.getItem('privacy-notice-hidden')) {
const notice = document.getElementById('privacy-notice');
if (notice) notice.style.display = 'none';
}
});
// Initialize date/time fields with current date and time
function initializeDateTime() {
const dateTimeInput = document.getElementById('incident_time');
if (dateTimeInput) {
// Get current date and time
const now = new Date();
// Format to datetime-local format (YYYY-MM-DDTHH:mm)
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const formattedDateTime = `${year}-${month}-${day}T${hours}:${minutes}`;
dateTimeInput.value = formattedDateTime;
// Add helper text
const helpText = dateTimeInput.nextElementSibling;
if (helpText && helpText.tagName === 'SMALL') {
helpText.innerHTML = `<span style="color: #28a745;">✓ Automatisch ingevuld: ${formatDisplayTime(now)}</span>`;
}
}
}
// Format time for display
function formatDisplayTime(date) {
return date.toLocaleString('nl-NL', {
weekday: 'short',
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
});
}
// Update date/time when page becomes visible (for accuracy)
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
// Only update if the field hasn't been manually changed
const dateTimeInput = document.getElementById('incident_time');
if (dateTimeInput && !dateTimeInput.dataset.userModified) {
initializeDateTime();
}
}
});
// Track if user manually modifies the datetime field
function trackDateTimeModification() {
const dateTimeInput = document.getElementById('incident_time');
if (dateTimeInput) {
dateTimeInput.addEventListener('input', function() {
this.dataset.userModified = 'true';
const helpText = this.nextElementSibling;
if (helpText && helpText.tagName === 'SMALL') {
helpText.innerHTML = '<span style="color: #6c757d;">Handmatig aangepast</span>';
}
});
// Add reset button
const resetBtn = document.createElement('button');
resetBtn.type = 'button';
resetBtn.className = 'btn btn-secondary';
resetBtn.style.marginLeft = '0.5rem';
resetBtn.style.padding = '0.25rem 0.5rem';
resetBtn.style.fontSize = '0.8rem';
resetBtn.innerHTML = '🔄 Nu';
resetBtn.title = 'Reset naar huidige tijd';
resetBtn.onclick = function() {
delete dateTimeInput.dataset.userModified;
initializeDateTime();
};
// Only add reset button if it doesn't exist yet
if (!dateTimeInput.parentNode.querySelector('.btn.btn-secondary')) {
dateTimeInput.parentNode.insertBefore(resetBtn, dateTimeInput.nextSibling);
}
}
}
// Update date/time when page becomes visible (for accuracy)
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
// Only update if the field hasn't been manually changed
const dateTimeInput = document.getElementById('incident_time');
if (dateTimeInput && !dateTimeInput.dataset.userModified) {
initializeDateTime();
}
}
});
// Quick add incident functionality
function quickAddIncident() {
// Check if we're already on add incident page
if (window.location.pathname.includes('/add')) {
return;
}
// For anonymous users, show quick add modal
if (!document.body.dataset.isAuthenticated) {
showQuickAddModal();
} else {
// Redirect to add incident page
window.location.href = '/add-incident';
}
}
// Show quick add modal for anonymous users
function showQuickAddModal() {
const modal = document.createElement('div');
modal.className = 'modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h3>🔥 Snel incident toevoegen</h3>
<button onclick="closeModal()" class="close-btn"></button>
</div>
<div class="modal-body">
<form onsubmit="submitQuickIncident(event)">
<div class="form-group">
<label>Snauw-index (1-5):</label>
<div class="severity-selector">
${getSnauwIndexButtons()}
</div>
</div>
<div class="form-group">
<label for="quick-notes">Opmerkingen (optioneel):</label>
<textarea id="quick-notes" placeholder="Wat gebeurde er?"></textarea>
</div>
<button type="submit" class="btn">📝 Opslaan</button>
</form>
</div>
</div>
`;
document.body.appendChild(modal);
// Initialize severity selector for modal
initializeSeveritySelector();
// Add modal styles
const style = document.createElement('style');
style.textContent = `
.modal {
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: 2000;
padding: 1rem;
}
.modal-content {
background: white;
border-radius: 12px;
max-width: 500px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid #e9ecef;
}
.modal-body {
padding: 1.5rem;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6c757d;
}
`;
document.head.appendChild(style);
}
// Initialize severity selector buttons
function initializeSeveritySelector() {
const severityButtons = document.querySelectorAll('.severity-btn');
const severityInput = document.getElementById('severity-input');
severityButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active class from all buttons
severityButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
// Update selected severity
selectedSeverity = parseInt(this.dataset.severity);
// Update hidden input value if it exists
if (severityInput) {
severityInput.value = selectedSeverity;
}
console.log(`Selected severity: ${selectedSeverity}`);
});
});
}
// Update header counter with total incidents
function updateHeaderCounter() {
const counterDigit1 = document.getElementById('counter-digit-1');
const counterDigit2 = document.getElementById('counter-digit-2');
const counterDigit3 = document.getElementById('counter-digit-3');
// Also update the old header-counter for backwards compatibility
const counterElement = document.getElementById('header-counter');
if (!counterDigit1 && !counterElement) return;
let totalIncidents = 0;
if (document.body.dataset.isAuthenticated === 'true') {
// For authenticated users - would normally fetch from API
// For now, we'll use a placeholder
totalIncidents = 0;
} else {
// For anonymous users - use localStorage
const incidents = JSON.parse(localStorage.getItem('snauw_incidents') || '[]');
totalIncidents = incidents.length;
}
// Format number with leading zeros
const formattedCount = totalIncidents.toString().padStart(3, '0');
// Update individual digits if they exist (new banner)
if (counterDigit1 && counterDigit2 && counterDigit3) {
counterDigit1.textContent = formattedCount[0];
counterDigit2.textContent = formattedCount[1];
counterDigit3.textContent = formattedCount[2];
// Add pulsing animation for new incidents
if (totalIncidents > 0) {
[counterDigit1, counterDigit2, counterDigit3].forEach(digit => {
digit.style.animation = 'pulse 2s ease-in-out infinite';
});
}
}
// Update old counter element if it exists (backwards compatibility)
if (counterElement) {
counterElement.textContent = formattedCount;
// Add pulsing animation for new incidents
if (totalIncidents > 0) {
counterElement.style.animation = 'pulse 2s ease-in-out infinite';
}
}
}
// Generate Snauw-index buttons with descriptions
function getSnauwIndexButtons() {
const descriptions = [
'Lichte correctie',
'Geprikkelde waarschuwing',
'Openlijke snauw',
'Bijtende aanval',
'Explosieve uitval'
];
return [...Array(5)].map((_, i) => `
<button type="button" class="severity-btn" data-severity="${i+1}" title="Fase ${i+1}: ${descriptions[i]}">
${i+1}
</button>
`).join('');
}
// Close modal
function closeModal() {
const modal = document.querySelector('.modal');
if (modal) {
modal.remove();
}
}
// Submit quick incident for anonymous users
async function submitQuickIncident(event) {
event.preventDefault();
if (!selectedSeverity) {
alert('Selecteer een Snauw-index');
return;
}
const notes = document.getElementById('quick-notes').value;
const now = new Date();
const incident = {
severity: selectedSeverity,
notes: notes,
timestamp: now.toISOString()
};
// Store in localStorage for anonymous users
let incidents = JSON.parse(localStorage.getItem('snauw_incidents') || '[]');
incident.id = Date.now(); // Simple ID for local storage
incidents.push(incident);
localStorage.setItem('snauw_incidents', JSON.stringify(incidents));
// Update header counter
updateHeaderCounter();
// Add animation to counter
const counterElement = document.getElementById('header-counter');
if (counterElement) {
counterElement.classList.add('updated');
setTimeout(() => counterElement.classList.remove('updated'), 3000);
}
// Show success message
showNotification('✅ Incident opgeslagen!', 'success');
// Close modal
closeModal();
// Refresh statistics if on stats page
if (window.location.pathname.includes('statistics')) {
location.reload();
}
}
// Show notification
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `alert alert-${type}`;
notification.textContent = message;
notification.style.position = 'fixed';
notification.style.top = '2rem';
notification.style.right = '2rem';
notification.style.zIndex = '3000';
notification.style.minWidth = '250px';
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// Anonymous user support
function initializeAnonymousUserSupport() {
// Check if user has incidents in localStorage
const localIncidents = localStorage.getItem('snauw_incidents');
if (localIncidents && !document.body.dataset.isAuthenticated) {
const incidents = JSON.parse(localIncidents);
if (incidents.length > 0) {
showMigrationOffer();
}
}
}
// Show migration offer to anonymous users with data
function showMigrationOffer() {
const incidents = JSON.parse(localStorage.getItem('snauw_incidents') || '[]');
if (incidents.length < 3) return; // Only show after a few incidents
const lastOffer = localStorage.getItem('migration-offer-shown');
const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
if (lastOffer && parseInt(lastOffer) > oneWeekAgo) return; // Don't show too often
const banner = document.createElement('div');
banner.className = 'migration-banner';
banner.innerHTML = `
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
<strong>💾 Je hebt ${incidents.length} incidenten opgeslagen!</strong><br>
Maak een account om je gegevens veilig te bewaren en op meerdere apparaten te gebruiken.
<div style="margin-top: 0.5rem;">
<a href="/register" class="btn btn-success" style="display: inline-block; margin-right: 0.5rem;">Account maken</a>
<button onclick="hideMigrationBanner()" class="btn btn-secondary" style="display: inline-block;">Later</button>
</div>
</div>
`;
const main = document.querySelector('main.container');
main.insertBefore(banner, main.firstChild);
localStorage.setItem('migration-offer-shown', Date.now().toString());
}
// Hide migration banner
function hideMigrationBanner() {
const banner = document.querySelector('.migration-banner');
if (banner) banner.remove();
}
// Hide privacy notice
function hidePrivacyNotice() {
const notice = document.getElementById('privacy-notice');
if (notice) {
notice.style.display = 'none';
localStorage.setItem('privacy-notice-hidden', 'true');
}
}
// Chart functionality
function loadChart() {
const chartCanvas = document.getElementById('incidentsChart');
if (!chartCanvas) return;
// For anonymous users, use localStorage data
if (!document.body.dataset.isAuthenticated) {
loadAnonymousChart();
} else {
loadUserChart();
}
}
// Load chart for anonymous users
function loadAnonymousChart() {
const incidents = JSON.parse(localStorage.getItem('snauw_incidents') || '[]');
// Group by date
const dateGroups = {};
incidents.forEach(incident => {
const date = new Date(incident.timestamp).toDateString();
if (!dateGroups[date]) {
dateGroups[date] = [];
}
dateGroups[date].push(incident);
});
// Prepare chart data
const dates = Object.keys(dateGroups).slice(-7); // Last 7 days
const counts = dates.map(date => dateGroups[date].length);
const averages = dates.map(date => {
const dayIncidents = dateGroups[date];
const sum = dayIncidents.reduce((acc, inc) => acc + inc.severity, 0);
return sum / dayIncidents.length || 0;
});
renderChart(dates, counts, averages);
}
// Load chart for authenticated users (would fetch from server)
function loadUserChart() {
// This would make an API call to get user's incident data
// For now, placeholder
renderChart([], [], []);
}
// Render chart using Chart.js (simplified version without external dependency)
function renderChart(dates, counts, averages) {
const canvas = document.getElementById('incidentsChart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
// Simple bar chart implementation
// In a real implementation, you'd use Chart.js library
ctx.fillStyle = '#667eea';
ctx.fillRect(10, 10, 50, 100);
ctx.fillText('Chart placeholder - Use Chart.js in production', 10, 130);
}
// Form validation
function validateIncidentForm() {
const severity = document.getElementById('severity-input');
if (!severity || !severity.value) {
alert('Selecteer een Snauw-index');
return false;
}
const severityValue = parseInt(severity.value);
if (severityValue < 1 || severityValue > 5) {
alert('Snauw-index moet tussen 1 en 5 zijn');
return false;
}
return true;
}
// Export data (for GDPR compliance)
function exportData() {
let data;
if (document.body.dataset.isAuthenticated) {
// Would fetch from server for authenticated users
alert('Export functionaliteit wordt nog geïmplementeerd voor geregistreerde gebruikers');
return;
} else {
// Export localStorage data for anonymous users
data = {
incidents: JSON.parse(localStorage.getItem('snauw_incidents') || '[]'),
exportDate: new Date().toISOString(),
type: 'anonymous_user_data'
};
}
const dataStr = JSON.stringify(data, null, 2);
const dataBlob = new Blob([dataStr], {type: 'application/json'});
const link = document.createElement('a');
link.href = URL.createObjectURL(dataBlob);
link.download = `snauw_counter_export_${new Date().toISOString().split('T')[0]}.json`;
link.click();
}
// Delete all data (for GDPR compliance)
function deleteAllData() {
if (!confirm('Weet je zeker dat je alle gegevens wilt verwijderen? Dit kan niet ongedaan worden gemaakt.')) {
return;
}
if (document.body.dataset.isAuthenticated) {
// Would make API call for authenticated users
alert('Data verwijdering voor geregistreerde gebruikers wordt nog geïmplementeerd');
return;
} else {
// Clear localStorage for anonymous users
localStorage.removeItem('snauw_incidents');
localStorage.removeItem('privacy-notice-hidden');
localStorage.removeItem('migration-offer-shown');
showNotification('✅ Alle lokale gegevens zijn verwijderd', 'success');
setTimeout(() => {
location.reload();
}, 2000);
}
}

24
app/static/manifest.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "Snauw Counter",
"short_name": "SnauwCounter",
"description": "Bijhouden van onvriendelijk gedrag - privé en veilig",
"start_url": "/",
"display": "standalone",
"background_color": "#667eea",
"theme_color": "#667eea",
"icons": [
{
"src": "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect fill='%23667eea' width='100' height='100' rx='20'/><text y='70' x='50' text-anchor='middle' font-size='60'>📊</text></svg>",
"sizes": "192x192",
"type": "image/svg+xml"
},
{
"src": "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect fill='%23667eea' width='100' height='100' rx='20'/><text y='70' x='50' text-anchor='middle' font-size='60'>📊</text></svg>",
"sizes": "512x512",
"type": "image/svg+xml"
}
],
"orientation": "portrait",
"categories": ["productivity", "lifestyle"],
"lang": "nl"
}