<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>24/7 Circular TimeSlots Timer</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
background: #111827;
color: #eee;
font-family: sans-serif;
padding: 20px;
max-width: 500px;
margin: 0 auto;
}
h1 {
font-size: 18px;
margin-bottom: 10px;
text-align: center;
}
/* Day Navigation */
.day-navigation {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 15px;
gap: 10px;
}
.nav-btn {
background: #374151;
color: #eee;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.day-slider {
display: flex;
overflow-x: auto;
gap: 8px;
padding: 10px 0;
scrollbar-width: none;
}
.day-slider::-webkit-scrollbar {
display: none;
}
.day-tab {
padding: 8px 12px;
background: #374151;
border-radius: 8px;
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
}
.day-tab.active {
background: #06b6d4;
color: #111;
font-weight: bold;
}
.current-day-indicator {
text-align: center;
margin-bottom: 10px;
font-size: 14px;
color: #fbbf24;
}
/* Canvas Wrapper for Swiping */
.canvas-container-outer {
position: relative;
width: 320px;
height: 320px;
margin: 0 auto 20px auto;
overflow: hidden;
border-radius: 50%;
}
.canvas-wrapper {
display: flex;
width: 100%;
height: 100%;
overflow-x: scroll;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
background: #111;
border-radius: 50%;
}
.canvas-wrapper::-webkit-scrollbar {
display: none;
}
.day-canvas-item {
flex-shrink: 0;
width: 320px;
height: 320px;
display: flex;
justify-content: center;
align-items: center;
scroll-snap-align: center;
position: relative;
}
.day-canvas-item canvas {
width: 320px;
height: 320px;
border-radius: 50%;
display: block;
}
/* Central overlay elements */
.circle-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.time {
font-size: 20px;
font-weight: bold;
color: #06b6d4;
text-align: center;
}
.dateInfo {
font-size: 12px;
color: #fbbf24;
text-align: center;
line-height: 1.3;
margin-top: 5px;
}
.moon {
font-size: 26px;
margin-bottom: -5px;
}
.moonText {
font-size: 12px;
color: #fbbf24;
}
#sunTimes {
margin-top: 8px;
text-align: center;
font-size: 14px;
color: #fbbf24;
}
#geoMsg {
margin-top: 4px;
text-align: center;
font-size: 13px;
color: #ef4444;
}
form {
margin-top: 15px;
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
}
input[type="time"] {
padding: 6px;
border-radius: 6px;
border: none;
}
button {
padding: 6px 10px;
border-radius: 6px;
border: none;
cursor: pointer;
}
.addBtn {
flex-basis: 100%;
max-width: 150px;
margin-top: 5px;
background: #06b6d4;
color: #111;
}
ul {
list-style: none;
padding: 0;
margin-top: 15px;
}
li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
margin-bottom: 6px;
background: #1f2937;
border-radius: 6px;
flex-wrap: wrap;
gap: 6px;
}
.controls {
display: flex;
gap: 6px;
}
.btn {
padding: 4px 8px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 12px;
}
.delBtn {
background: #ef4444;
color: white;
}
.editBtn {
background: #f59e0b;
color: white;
}
.toggleBtn {
background: #06b6d4;
color: #111;
}
.daysContainer {
display: flex;
gap: 2px;
margin: 4px 0;
}
.dayBtn {
width: 20px;
height: 20px;
border-radius: 3px;
border: none;
cursor: pointer;
font-size: 11px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
}
.dayBtn.on {
background: #10b981;
color: white;
}
.dayBtn.off {
background: #6b7280;
color: #9ca3af;
}
.slotInfo {
display: flex;
flex-direction: column;
}
.sun-based-indicator {
color: #fbbf24;
font-size: 11px;
margin-top: 2px;
}
/* New styles for sunrise/sunset form */
.sun-form {
margin-top: 15px;
padding: 12px;
background: #1f2937;
border-radius: 8px;
}
.sun-form h3 {
margin: 0 0 10px 0;
font-size: 16px;
color: #fbbf24;
text-align: center;
}
.sun-form-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
}
.sun-form-row label {
flex: 1;
min-width: 120px;
}
.sun-form-row select {
flex: 1;
min-width: 120px;
padding: 6px;
border-radius: 6px;
border: none;
background: #374151;
color: #eee;
}
.sun-form-row .addBtn {
margin-top: 5px;
}
/* Export section */
.export-section {
margin-top: 20px;
padding: 12px;
background: #1f2937;
border-radius: 8px;
}
.export-section h3 {
margin: 0 0 10px 0;
font-size: 16px;
color: #fbbf24;
text-align: center;
}
.export-buttons {
display: flex;
gap: 10px;
justify-content: center;
}
.export-btn {
background: #10b981;
color: white;
}
.import-btn {
background: #f59e0b;
color: white;
}
/* Footer */
footer {
margin-top: 30px;
text-align: center;
font-size: 12px;
color: #6b7280;
}
/* Location button */
.location-btn {
background: #8b5cf6;
color: white;
margin-top: 10px;
}
</style>
</head>
<body>
<h1>24/7 Circular TimeSlots Timer</h1>
<div class="day-navigation">
<button class="nav-btn" id="prevDay">â†</button>
<div class="day-slider" id="daySlider"></div>
<button class="nav-btn" id="nextDay">→</button>
</div>
<div class="current-day-indicator" id="currentDayIndicator"></div>
<div class="canvas-container-outer">
<div class="canvas-wrapper" id="canvasWrapper"></div>
<div class="circle-overlay">
<div class="moon" id="moonIcon">🌕</div>
<div class="moonText" id="moonText">Volle Maan</div>
<div class="time" id="centerTime">--:--</div>
<div class="dateInfo" id="dateInfo">--</div>
</div>
</div>
<div id="sunTimes">Zonsopgang: --:-- | Zonsondergang: --:--</div>
<div id="geoMsg"></div>
<button class="location-btn" id="getLocationBtn">Gebruik mijn locatie voor zonsopgang/ondergang</button>
<div class="sun-form">
<h3>Voeg tijdslot toe op basis van zonsopgang/ondergang</h3>
<div class="sun-form-row">
<label for="sunEvent">Gebeurtenis:</label>
<select id="sunEvent">
<option value="sunrise">Zonsopgang</option>
<option value="sunset">Zonsondergang</option>
</select>
</div>
<div class="sun-form-row">
<label for="sunOffset">Offset:</label>
<select id="sunOffset">
<option value="-120">2 uur voor</option>
<option value="-90">1,5 uur voor</option>
<option value="-60">1 uur voor</option>
<option value="-45">45 minuten voor</option>
<option value="-30">30 minuten voor</option>
<option value="-15">15 minuten voor</option>
<option value="0">Precies</option>
<option value="15">15 minuten na</option>
<option value="30">30 minuten na</option>
<option value="45">45 minuten na</option>
<option value="60">1 uur na</option>
<option value="90">1,5 uur na</option>
<option value="120">2 uur na</option>
</select>
</div>
<div class="sun-form-row">
<label for="sunDuration">Duur:</label>
<select id="sunDuration">
<option value="15">15 minuten</option>
<option value="30">30 minuten</option>
<option value="45">45 minuten</option>
<option value="60">1 uur</option>
<option value="90">1,5 uur</option>
<option value="120">2 uur</option>
<option value="180">3 uur</option>
<option value="240">4 uur</option>
</select>
</div>
<div class="daysContainer">
<button type="button" class="dayBtn on" data-day="0">M</button>
<button type="button" class="dayBtn on" data-day="1">T</button>
<button type="button" class="dayBtn on" data-day="2">W</button>
<button type="button" class="dayBtn on" data-day="3">T</button>
<button type="button" class="dayBtn on" data-day="4">F</button>
<button type="button" class="dayBtn on" data-day="5">S</button>
<button type="button" class="dayBtn on" data-day="6">S</button>
</div>
<button type="button" class="addBtn" id="addSunSlot">Voeg SunSlot toe</button>
</div>
<form id="addForm">
<h3>Voeg tijdslot toe op basis van timepicker</h3>
<input type="time" id="startTime" required>
<input type="time" id="endTime" required>
<div class="daysContainer">
<button type="button" class="dayBtn on" data-day="0">M</button>
<button type="button" class="dayBtn on" data-day="1">T</button>
<button type="button" class="dayBtn on" data-day="2">W</button>
<button type="button" class="dayBtn on" data-day="3">T</button>
<button type="button" class="dayBtn on" data-day="4">F</button>
<button type="button" class="dayBtn on" data-day="5">S</button>
<button type="button" class="dayBtn on" data-day="6">S</button>
</div>
<button type="submit" class="addBtn">Add TimeSlot</button>
</form>
<ul id="slotList"></ul>
<div class="export-section">
<h3>Exporteer/Importeer Schema</h3>
<div class="export-buttons">
<button class="export-btn" id="exportBtn">Export naar JSON</button>
<button class="import-btn" id="importBtn">Importeer van JSON</button>
</div>
<textarea id="jsonData" placeholder="JSON data komt hier..." style="width:100%; height:100px; margin-top:10px; display:none; background:#374151; color:#eee; border:none; border-radius:6px; padding:8px;"></textarea>
</div>
<footer>
<p>Copyright 2025, Dirk Luberth Dijkman Bangert 30 1619GJ Andijk The Netherlands</p>
<p>Voor dagelijks weektijdschema voor bewatering of verlichting</p>
https://m.facebook.com/luberth.dijkman/
<br><br>
https://codepen.io/ldijkman/pens/public
</footer>
<script>
// Constants for canvas dimensions
const CANVAS_WIDTH = 320;
const CANVAS_HEIGHT = 320;
const RADIUS = CANVAS_WIDTH / 2 - 30;
const CENTER_X = CANVAS_WIDTH / 2;
const CENTER_Y = CANVAS_HEIGHT / 2;
const STORAGE_KEY = "dagcirkel_slots_v13"; // Incremented version for new features
const dayLetters = ["M", "T", "W", "T", "F", "S", "S"];
const dayNames = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
const dayNamesNL = ["Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag", "Zaterdag", "Zondag"];
// Application state
let slots = [];
let sunrise = "07:00", sunset = "19:00";
let currentViewDay = new Date().getDay(); // 0 (Sunday) to 6 (Saturday)
let scrollTimeout;
const canvasWrapper = document.getElementById("canvasWrapper");
// Utility functions
const TimeUtils = {
toMin: function(t) {
const [h, m] = t.split(":").map(Number);
return h * 60 + m;
},
toHHMM: function(mins) {
const h = String(Math.floor(mins / 60)).padStart(2, "0");
const m = String(mins % 60).padStart(2, "0");
return `${h}:${m}`;
},
isInSlot: function(mins, slot) {
const s = this.toMin(slot.start), e = this.toMin(slot.end);
return s < e ? (mins >= s && mins < e) : (mins >= s || mins < e);
},
toLocalHHMM: function(d) {
return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
},
formatTimeDiff: function(mins) {
const h = Math.floor(mins / 60);
const m = mins % 60;
return (h > 0 ? h + "h " : "") + m + "m";
},
getISOWeek: function(date) {
const tmp = new Date(date.getTime());
tmp.setHours(0, 0, 0, 0);
tmp.setDate(tmp.getDate() + 3 - (tmp.getDay() + 6) % 7);
const week1 = new Date(tmp.getFullYear(), 0, 4);
return 1 + Math.round(((tmp - week1) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7);
}
};
// Data management functions
const DataManager = {
save: function() {
slots.sort((a, b) => TimeUtils.toMin(a.start) - TimeUtils.toMin(b.start));
localStorage.setItem(STORAGE_KEY, JSON.stringify(slots));
},
load: function() {
const data = localStorage.getItem(STORAGE_KEY);
if (data) {
slots = JSON.parse(data);
// Ensure all slots have days array (for backward compatibility)
slots.forEach(slot => {
if (!slot.days) {
slot.days = [true, true, true, true, true, true, true];
}
});
} else {
// Default slots
slots = [
{id: 1, start: "06:00", end: "08:00", enabled: true, days: [true, true, true, true, true, true, true]},
{id: 2, start: "12:00", end: "13:30", enabled: true, days: [true, true, true, true, true, true, true]},
{id: 3, start: "19:00", end: "22:00", enabled: true, days: [true, true, true, true, true, true, true]}
];
this.save();
}
},
exportToJson: function() {
const jsonData = JSON.stringify(slots, null, 2);
const textarea = document.getElementById("jsonData");
textarea.value = jsonData;
textarea.style.display = "block";
// Select the text for easy copying
textarea.select();
textarea.setSelectionRange(0, 99999); // For mobile devices
// Copy to clipboard
document.execCommand("copy");
alert("Schema gekopieerd naar klembord!");
},
importFromJson: function() {
const textarea = document.getElementById("jsonData");
const jsonData = textarea.value.trim();
if (!jsonData) {
alert("Voer JSON data in om te importeren");
return;
}
try {
const importedSlots = JSON.parse(jsonData);
// Validate the imported data structure
if (Array.isArray(importedSlots) && importedSlots.every(s => s.id && s.start && s.end)) {
slots = importedSlots;
this.save();
renderCanvases();
renderList();
textarea.style.display = "none";
alert("Schema succesvol geïmporteerd!");
} else {
alert("Ongeldige JSON structuur");
}
} catch (e) {
alert("Ongeldige JSON: " + e.message);
}
}
};
// Canvas rendering functions
const CanvasRenderer = {
drawMarkers: function(ctx) {
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// Draw all tickmarks first (quarters, halves, hours)
for (let mins = 0; mins < 1440; mins += 15) {
const angle = mins / 1440 * 2 * Math.PI - Math.PI / 2;
let inner, outer, lineWidth;
if (mins % 60 === 0) {
// Hour marks
inner = RADIUS - 10;
outer = RADIUS + 10;
lineWidth = 2;
} else if (mins % 30 === 0) {
// Half-hour marks
inner = RADIUS - 10;
outer = RADIUS + 4;
lineWidth = 1;
} else {
// Quarter-hour marks (15 and 45 minutes)
inner = RADIUS - 10;
outer = RADIUS + 0;
lineWidth = .5;
}
const x1 = CENTER_X + Math.cos(angle) * inner, y1 = CENTER_Y + Math.sin(angle) * inner;
const x2 = CENTER_X + Math.cos(angle) * outer, y2 = CENTER_Y + Math.sin(angle) * outer;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.strokeStyle = "#aaa";
ctx.lineWidth = lineWidth;
ctx.stroke();
}
// Draw hour labels
for (let hour = 0; hour < 24; hour++) {
const mins = hour * 60;
const angle = mins / 1440 * 2 * Math.PI - Math.PI / 2;
ctx.fillStyle = "#ccc";
ctx.font = "12px sans-serif";
const labelX = CENTER_X + Math.cos(angle) * (RADIUS + 20),
labelY = CENTER_Y + Math.sin(angle) * (RADIUS + 20);
ctx.fillText(hour.toString(), labelX, labelY);
}
},
drawDayNight: function(ctx) {
const srMin = TimeUtils.toMin(sunrise), ssMin = TimeUtils.toMin(sunset);
const srAngle = srMin / 1440 * 2 * Math.PI - Math.PI / 2;
const ssAngle = ssMin / 1440 * 2 * Math.PI - Math.PI / 2;
ctx.beginPath();
ctx.arc(CENTER_X, CENTER_Y, RADIUS - 30, 0, 2 * Math.PI);
ctx.strokeStyle = "#1e3a8a";
ctx.lineWidth = 12;
ctx.stroke();
ctx.beginPath();
if (srMin < ssMin) {
ctx.arc(CENTER_X, CENTER_Y, RADIUS - 30, srAngle, ssAngle);
} else {
ctx.arc(CENTER_X, CENTER_Y, RADIUS - 30, srAngle, 1.5 * Math.PI);
ctx.arc(CENTER_X, CENTER_Y, RADIUS - 30, -Math.PI / 2, ssAngle);
}
ctx.strokeStyle = "#fbbf24";
ctx.lineWidth = 12;
ctx.stroke();
},
drawCircularText: function(ctx, text, radius, centerAngle) {
ctx.save();
ctx.translate(CENTER_X, CENTER_Y);
ctx.rotate(centerAngle);
ctx.fillStyle = "#ccc";
ctx.font = "14px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const chars = [...text];
const totalWidth = chars.reduce((w, ch) => w + ctx.measureText(ch).width, 0);
let offset = -(totalWidth / radius) / 2;
chars.forEach(char => {
const w = ctx.measureText(char).width;
const angle = w / radius;
ctx.rotate(offset + angle / 2);
ctx.save();
ctx.translate(0, -radius);
ctx.fillText(char, 0, 0);
ctx.restore();
offset = angle / 2;
});
ctx.restore();
},
drawCircle: function(canvasEl, dayIndexToDraw) {
const ctx = canvasEl.getContext("2d");
ctx.clearRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
ctx.beginPath();
ctx.arc(CENTER_X, CENTER_Y, RADIUS, 0, 2 * Math.PI);
ctx.strokeStyle = "#e0e0e0";
ctx.lineWidth = 24;
ctx.stroke();
this.drawDayNight(ctx);
const now = new Date();
const mins = now.getHours() * 60 + now.getMinutes();
let stateColor = "grey";
let inAnySlot = false;
slots.forEach(slot => {
// Only draw slots for the specified dayIndexToDraw
if (!isSlotActiveOnDay(slot, dayIndexToDraw)) return;
const sMin = TimeUtils.toMin(slot.start), eMin = TimeUtils.toMin(slot.end);
// If the canvas being drawn is the *actual* current day, highlight active slots
const isActualCurrentDay = (dayIndexToDraw === now.getDay());
const active = isActualCurrentDay && TimeUtils.isInSlot(mins, slot);
if (active) inAnySlot = true;
const color = active ? "limegreen" : "red";
const startAngle = sMin / 1440 * 2 * Math.PI - Math.PI / 2;
const endAngle = eMin / 1440 * 2 * Math.PI - Math.PI / 2;
ctx.beginPath();
if (sMin < eMin) {
ctx.arc(CENTER_X, CENTER_Y, RADIUS, startAngle, endAngle);
} else {
ctx.arc(CENTER_X, CENTER_Y, RADIUS, startAngle, 1.5 * Math.PI);
ctx.arc(CENTER_X, CENTER_Y, RADIUS, -Math.PI / 2, endAngle);
}
ctx.strokeStyle = color;
ctx.lineWidth = 24;
ctx.stroke();
});
this.drawMarkers(ctx);
// Draw current time indicator only if this is the actual current day
if (dayIndexToDraw === now.getDay()) {
if (slots.some(s => isSlotActiveOnDay(s, dayIndexToDraw))) {
stateColor = inAnySlot ? "limegreen" : "red";
}
const angle = mins / 1440 * 2 * Math.PI - Math.PI / 2;
const innerR = 40;
ctx.beginPath();
ctx.moveTo(CENTER_X + Math.cos(angle) * innerR, CENTER_Y + Math.sin(angle) * innerR);
ctx.lineTo(CENTER_X + Math.cos(angle) * RADIUS, CENTER_Y + Math.sin(angle) * RADIUS);
ctx.strokeStyle = stateColor;
ctx.lineWidth = 4;
ctx.stroke();
}
this.drawCircularText(ctx, "Evening", RADIUS - 20, -Math.PI / 4);
this.drawCircularText(ctx, "Night", RADIUS - 20, Math.PI / 4);
this.drawCircularText(ctx, "Morning", RADIUS - 20, 3 * Math.PI / 4);
this.drawCircularText(ctx, "Afternoon", RADIUS - 20, -3 * Math.PI / 4);
}
};
// Helper functions
function isSlotActiveOnDay(slot, dayIndex) {
if (!slot.enabled) return false;
// Convert Date.getDay() (0=Sun, 1=Mon) to our array index (0=Mon, 6=Sun)
const ourDayIndex = (dayIndex === 0) ? 6 : dayIndex - 1;
return slot.days[ourDayIndex];
}
function getNextStateChangeForDay(now, dayIndex) {
const minsNow = now.getHours() * 60 + now.getMinutes();
let nextChange = null;
slots.forEach(slot => {
if (!slot.enabled) return;
if (!isSlotActiveOnDay(slot, dayIndex)) return;
const s = TimeUtils.toMin(slot.start), e = TimeUtils.toMin(slot.end);
const sDelta = (s - minsNow + 1440) % 1440;
const eDelta = (e - minsNow + 1440) % 1440;
if (nextChange === null || sDelta < nextChange.mins) {
nextChange = {mins: sDelta, type: 'ON', time: slot.start};
}
if (nextChange === null || eDelta < nextChange.mins) {
nextChange = {mins: eDelta, type: 'OFF', time: slot.end};
}
});
return nextChange ? `In ${TimeUtils.formatTimeDiff(nextChange.mins)} -> ${nextChange.type} at ${nextChange.time}` : "--";
}
function updateCenterOverlay(displayDayIndex) {
const now = new Date();
const currentDayOfWeek = now.getDay();
const mins = now.getHours() * 60 + now.getMinutes();
document.getElementById("centerTime").textContent = TimeUtils.toHHMM(mins);
const isToday = (displayDayIndex === currentDayOfWeek);
// Convert Date.getDay() (0=Sun, 1=Mon) to our array index (0=Mon, 6=Sun) for dayNamesNL
const dayName = dayNamesNL[(displayDayIndex === 0) ? 6 : displayDayIndex - 1];
const options = { day: 'numeric', month: 'long', year: 'numeric' };
let dateStr;
let weekNum;
if (isToday) {
dateStr = now.toLocaleDateString('nl-NL', options);
weekNum = TimeUtils.getISOWeek(now);
} else {
// For other days, calculate the date for that day
const targetDate = new Date(now);
const diff = displayDayIndex - currentDayOfWeek;
targetDate.setDate(now.getDate() + diff);
dateStr = targetDate.toLocaleDateString('nl-NL', options);
weekNum = TimeUtils.getISOWeek(targetDate);
}
const nextChangeText = getNextStateChangeForDay(now, displayDayIndex);
document.getElementById("dateInfo").innerHTML =
`${dayName}${isToday ? ' (Vandaag)' : ''}<br>${dateStr}<br>Week ${weekNum}<br>Next: ${nextChangeText}`;
document.getElementById("sunTimes").textContent = `SunRise: ${sunrise} | SunSet: ${sunset}`;
}
// This function creates and draws the canvases for the previous, current, and next day
function renderCanvases() {
canvasWrapper.innerHTML = '';
canvasWrapper.style.scrollBehavior = 'auto';
const daysToRender = [];
// Calculate previous, current, and next day indices (0-6)
const prevDay = (currentViewDay - 1 + 7) % 7;
const nextDay = (currentViewDay + 1) % 7;
daysToRender.push({ dayIndex: prevDay, isCurrent: false });
daysToRender.push({ dayIndex: currentViewDay, isCurrent: true });
daysToRender.push({ dayIndex: nextDay, isCurrent: false });
daysToRender.forEach((dayData, index) => {
const dayCanvasItem = document.createElement('div');
dayCanvasItem.className = 'day-canvas-item';
dayCanvasItem.dataset.dayIndex = dayData.dayIndex;
const canvasEl = document.createElement('canvas');
canvasEl.width = CANVAS_WIDTH;
canvasEl.height = CANVAS_HEIGHT;
dayCanvasItem.appendChild(canvasEl);
canvasWrapper.appendChild(dayCanvasItem);
CanvasRenderer.drawCircle(canvasEl, dayData.dayIndex);
});
// Scroll to the middle canvas immediately after creation
canvasWrapper.scrollLeft = CANVAS_WIDTH;
canvasWrapper.style.scrollBehavior = 'smooth';
updateCenterOverlay(currentViewDay);
}
function renderDayNavigation() {
const daySlider = document.getElementById("daySlider");
daySlider.innerHTML = "";
const today = new Date().getDay();
for (let i = 0; i < 7; i++) {
const dayTab = document.createElement("div");
dayTab.className = `day-tab ${i === currentViewDay ? 'active' : ''}`;
// Convert Date.getDay() (0=Sun, 1=Mon) to our array index (0=Mon, 6=Sun) for display
dayTab.textContent = dayNamesNL[(i === 0) ? 6 : i - 1];
dayTab.onclick = (function(index) {
return function() {
currentViewDay = index;
renderDayNavigation();
renderCanvases();
renderList();
};
})(i);
daySlider.appendChild(dayTab);
}
// Update current day indicator
const isToday = currentViewDay === today;
document.getElementById("currentDayIndicator").textContent =
`Bekijken: ${dayNamesNL[(currentViewDay === 0) ? 6 : currentViewDay - 1]}${isToday ? ' (Vandaag)' : ''}`;
// Auto-scroll to center the current day tab
const activeTab = daySlider.querySelector('.day-tab.active');
if (activeTab) {
const scrollLeft = activeTab.offsetLeft - (daySlider.offsetWidth / 2) + (activeTab.offsetWidth / 2);
daySlider.scrollLeft = scrollLeft;
}
}
function renderList() {
const ul = document.getElementById("slotList");
ul.innerHTML = "";
slots.sort((a, b) => TimeUtils.toMin(a.start) - TimeUtils.toMin(b.start));
// Convert currentViewDay to our day index (0=Monday, 6=Sunday)
const viewDayArrayIndex = (currentViewDay === 0) ? 6 : currentViewDay - 1;
slots.forEach(slot => {
const li = document.createElement("li");
const slotInfo = document.createElement("div");
slotInfo.className = "slotInfo";
const timeSpan = document.createElement("span");
timeSpan.textContent = `${slot.start} -> ${slot.end}`;
timeSpan.style.color = slot.enabled ? "#eee" : "#666";
slotInfo.appendChild(timeSpan);
// Show sun-based indicator if applicable
if (slot.sunBased) {
const sunIndicator = document.createElement("div");
sunIndicator.className = "sun-based-indicator";
const eventName = slot.sunEvent === "sunrise" ? "Zonsopgang" : "Zonsondergang";
const offsetText = slot.sunOffset > 0 ? `${slot.sunOffset} min na` :
slot.sunOffset < 0 ? `${Math.abs(slot.sunOffset)} min voor` : "Precies";
sunIndicator.textContent = `${eventName} ${offsetText} (${slot.sunDuration} min)`;
slotInfo.appendChild(sunIndicator);
}
// Days toggle buttons
const daysContainer = document.createElement("div");
daysContainer.className = "daysContainer";
dayLetters.forEach((letter, index) => {
const dayBtn = document.createElement("button");
dayBtn.className = `dayBtn ${slot.days[index] ? 'on' : 'off'}`;
dayBtn.textContent = letter;
dayBtn.disabled = !slot.enabled;
dayBtn.onclick = () => {
slot.days[index] = !slot.days[index];
dayBtn.className = `dayBtn ${slot.days[index] ? 'on' : 'off'}`;
DataManager.save();
renderCanvases();
};
daysContainer.appendChild(dayBtn);
});
slotInfo.appendChild(daysContainer);
li.appendChild(slotInfo);
const ctr = document.createElement("div");
ctr.className = "controls";
const tgl = document.createElement("button");
tgl.textContent = "Toggle";
tgl.className = "btn toggleBtn";
tgl.onclick = () => {
slot.enabled = !slot.enabled;
DataManager.save();
renderList();
renderCanvases();
};
const edit = document.createElement("button");
edit.textContent = "Edit";
edit.className = "btn editBtn";
edit.onclick = () => {
li.innerHTML = "";
const startInput = document.createElement("input");
startInput.type = "time";
startInput.value = slot.start;
const endInput = document.createElement("input");
endInput.type = "time";
endInput.value = slot.end;
// Days toggle buttons in edit mode
const editDaysContainer = document.createElement("div");
editDaysContainer.className = "daysContainer";
dayLetters.forEach((letter, index) => {
const dayBtn = document.createElement("button");
dayBtn.className = `dayBtn ${slot.days[index] ? 'on' : 'off'}`;
dayBtn.textContent = letter;
dayBtn.onclick = () => {
slot.days[index] = !slot.days[index];
dayBtn.className = `dayBtn ${slot.days[index] ? 'on' : 'off'}`;
};
editDaysContainer.appendChild(dayBtn);
});
const saveBtn = document.createElement("button");
saveBtn.textContent = "Save";
saveBtn.className = "btn toggleBtn";
saveBtn.onclick = () => {
slot.start = startInput.value;
slot.end = endInput.value;
// Clear sun-based properties when manually editing
delete slot.sunBased;
delete slot.sunEvent;
delete slot.sunOffset;
delete slot.sunDuration;
DataManager.save();
renderList();
renderCanvases();
};
li.appendChild(startInput);
li.appendChild(endInput);
li.appendChild(editDaysContainer);
li.appendChild(saveBtn);
};
const del = document.createElement("button");
del.textContent = "Delete";
del.className = "btn delBtn";
del.onclick = () => {
slots = slots.filter(s => s.id !== slot.id);
DataManager.save();
renderList();
renderCanvases();
};
ctr.appendChild(tgl);
ctr.appendChild(edit);
ctr.appendChild(del);
li.appendChild(ctr);
ul.appendChild(li);
});
}
// Initialize form day buttons
function initFormDays() {
const formDays = document.querySelectorAll('#addForm .dayBtn, .sun-form .dayBtn');
formDays.forEach(btn => {
btn.classList.add('on');
btn.onclick = () => {
btn.classList.toggle('on');
btn.classList.toggle('off');
};
});
}
// Function to recalculate sun-based slots
function recalculateSunBasedSlots() {
slots.forEach(slot => {
if (slot.sunBased) {
// Calculate start time based on sunrise/sunset and offset
const baseTime = slot.sunEvent === "sunrise" ? sunrise : sunset;
const baseMins = TimeUtils.toMin(baseTime);
const startMins = (baseMins + slot.sunOffset + 1440) % 1440;
const endMins = (startMins + slot.sunDuration) % 1440;
slot.start = TimeUtils.toHHMM(startMins);
slot.end = TimeUtils.toHHMM(endMins);
}
});
DataManager.save();
renderCanvases();
renderList();
}
// Get user location for sunrise/sunset data
function getUserLocation() {
if (!navigator.geolocation) {
document.getElementById("geoMsg").textContent = "Geolocatie wordt niet ondersteund door deze browser.";
return;
}
document.getElementById("geoMsg").textContent = "Locatie ophalen...";
navigator.geolocation.getCurrentPosition(
position => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
fetchSunTimes(lat, lon);
},
error => {
let message = "Kan locatie niet ophalen: ";
switch(error.code) {
case error.PERMISSION_DENIED:
message += "Gebruiker heeft locatie geweigerd.";
break;
case error.POSITION_UNAVAILABLE:
message += "Locatie informatie is niet beschikbaar.";
break;
case error.TIMEOUT:
message += "Locatie aanvraag is verlopen.";
break;
default:
message += "Onbekende fout.";
}
document.getElementById("geoMsg").textContent = message;
}
);
}
// Add sunrise/sunset based slot
document.getElementById("addSunSlot").addEventListener("click", () => {
const event = document.getElementById("sunEvent").value;
const offset = parseInt(document.getElementById("sunOffset").value);
const duration = parseInt(document.getElementById("sunDuration").value);
// Get selected days from form
const formDays = document.querySelectorAll('.sun-form .dayBtn');
const days = Array.from(formDays).map(btn => btn.classList.contains('on'));
// Calculate start time based on sunrise/sunset and offset
const baseTime = event === "sunrise" ? sunrise : sunset;
const baseMins = TimeUtils.toMin(baseTime);
const startMins = (baseMins + offset + 1440) % 1440;
const endMins = (startMins + duration) % 1440;
const startTime = TimeUtils.toHHMM(startMins);
const endTime = TimeUtils.toHHMM(endMins);
slots.push({
id: Date.now(),
start: startTime,
end: endTime,
enabled: true,
days: days,
sunBased: true,
sunEvent: event,
sunOffset: offset,
sunDuration: duration
});
DataManager.save();
renderList();
renderCanvases();
});
// Original form submission
document.getElementById("addForm").addEventListener("submit", e => {
e.preventDefault();
const st = document.getElementById("startTime").value;
const ed = document.getElementById("endTime").value;
if (!st || !ed) return;
// Get selected days from form
const formDays = document.querySelectorAll('#addForm .dayBtn');
const days = Array.from(formDays).map(btn => btn.classList.contains('on'));
slots.push({id: Date.now(), start: st, end: ed, enabled: true, days: days});
DataManager.save();
renderList();
renderCanvases();
e.target.reset();
// Reset form days to all on
formDays.forEach(btn => {
btn.classList.add('on');
btn.classList.remove('off');
});
});
function fetchSunTimes(lat, lon) {
fetch(`https://api.sunrise-sunset.org/json?lat=${lat}&lng=${lon}&formatted=0`)
.then(res => res.json())
.then(data => {
if (data.status === "OK") {
sunrise = TimeUtils.toLocalHHMM(new Date(data.results.sunrise));
sunset = TimeUtils.toLocalHHMM(new Date(data.results.sunset));
document.getElementById("geoMsg").textContent = "";
// Recalculate all sun-based slots when sun times change
recalculateSunBasedSlots();
}
})
.catch(() => {
document.getElementById("geoMsg").textContent = "Kon sunrise/sunset niet ophalen.";
});
}
function updateMoonPhase() {
const phases = ["🌑","🌒","🌓","🌔","🌕","🌖","🌗","🌘"];
const names = ["Nieuwe Maan","Wassende Sikkel","Eerste Kwartier","Wassende Maan",
"Volle Maan","Afnemende Maan","Laatste Kwartier","Afnemende Sikkel"];
const now = new Date();
const lp = new Date(Date.UTC(2000, 0, 6, 18, 14));
const diff = now - lp;
const days = diff / 1000 / 60 / 60 / 24;
const lunations = days / 29.53058867;
const index = Math.floor((lunations - Math.floor(lunations)) * 8 + 0.5) % 8;
document.getElementById("moonIcon").textContent = phases[index];
document.getElementById("moonText").textContent = names[index];
}
// Navigation button handlers for endless scrolling
document.getElementById("prevDay").addEventListener("click", () => {
currentViewDay = (currentViewDay - 1 + 7) % 7;
renderDayNavigation();
renderCanvases();
renderList();
});
document.getElementById("nextDay").addEventListener("click", () => {
currentViewDay = (currentViewDay + 1) % 7;
renderDayNavigation();
renderCanvases();
renderList();
});
// Handle canvas wrapper scrolling/swiping
canvasWrapper.addEventListener('scroll', () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
const scrolledToDayIndex = Math.round(canvasWrapper.scrollLeft / CANVAS_WIDTH);
if (scrolledToDayIndex === 0) {
currentViewDay = (currentViewDay - 1 + 7) % 7;
} else if (scrolledToDayIndex === 2) {
currentViewDay = (currentViewDay + 1) % 7;
}
// If currentViewDay changed, re-render to ensure correct neighbors and list are shown
if (scrolledToDayIndex !== 1) {
renderDayNavigation();
renderCanvases();
renderList();
}
// Always update overlay as it might show different day's info
updateCenterOverlay(currentViewDay);
}, 150);
});
// Export/Import functionality
document.getElementById("exportBtn").addEventListener("click", () => {
DataManager.exportToJson();
});
document.getElementById("importBtn").addEventListener("click", () => {
const textarea = document.getElementById("jsonData");
if (textarea.style.display === "none") {
textarea.style.display = "block";
textarea.value = "";
textarea.focus();
} else {
DataManager.importFromJson();
}
});
// Location button handler
document.getElementById("getLocationBtn").addEventListener("click", getUserLocation);
// Initial setup
DataManager.load();
initFormDays();
renderDayNavigation();
renderCanvases();
renderList();
updateMoonPhase();
// Update current time pointer and moon phase every 15 seconds
setInterval(() => {
const currentCanvas = canvasWrapper.querySelector(`.day-canvas-item[data-day-index="${currentViewDay}"] canvas`);
if (currentCanvas) {
CanvasRenderer.drawCircle(currentCanvas, currentViewDay);
}
updateCenterOverlay(currentViewDay);
updateMoonPhase();
}, 15000);
</script>
</body>
</html>
/* Add your styles here */
// Add your code here