initial commit
All checks were successful
Build and Push Image / build-and-push (push) Successful in 1m26s
All checks were successful
Build and Push Image / build-and-push (push) Successful in 1m26s
This commit is contained in:
parent
3bba1f6db6
commit
b56e866071
36 changed files with 4160 additions and 0 deletions
433
app/static/css/style.css
Normal file
433
app/static/css/style.css
Normal 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;
|
||||
}
|
||||
1
app/static/images/snauw-banner.png
Normal file
1
app/static/images/snauw-banner.png
Normal file
|
|
@ -0,0 +1 @@
|
|||
Not Found
|
||||
BIN
app/static/img/banner.png
Normal file
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
573
app/static/js/app.js
Normal 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
24
app/static/manifest.json
Normal 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"
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue