Calendar
Qwen 3.7 Flash · full

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 57,191 bytes · SHA-256 4e8ecd370a5d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendar & Event Planner</title>
<style>
:root {
--color-primary: #1a73e8;
--color-primary-light: #e8f0fe;
--color-danger: #d93025;
--color-danger-light: #fce8e6;
--color-success: #1e8e3e;
--color-border: #e0e0e0;
--color-text: #202124;
--color-text-secondary: #5f6368;
--color-bg: #ffffff;
--color-bg-secondary: #f8f9fa;
--header-height: 56px;
--color-palette: #1a73e8,#d93025,#1e8e3e,#f9ab00,#9334e6,#e67c73,#185abc,#3c6e71,#b06000,#8430ce;
}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Segoe UI',Roboto,-apple-system,BlinkMacSystemFont,sans-serif;color:var(--color-text);background:var(--color-bg);overflow:hidden;height:100vh}
/* HEADER */
.header{display:flex;align-items:center;height:var(--header-height);padding:0 16px;border-bottom:1px solid var(--color-border);background:var(--color-bg);position:relative;z-index:10}
.header-title{font-size:22px;font-weight:400;flex-shrink:0;width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.header-nav{display:flex;gap:2px;flex-shrink:0}
.header-nav button{background:none;border:1px solid var(--color-border);border-radius:4px;padding:6px 12px;cursor:pointer;font-size:13px;color:var(--color-text);transition:background .15s}
.header-nav button:hover{background:var(--color-bg-secondary)}
#today-btn{border:1px solid var(--color-border);background:var(--color-bg);font-weight:500}
#today-btn:hover{background:#f1f3f4}
.spacer{flex:1}
.view-switcher{display:flex;border:1px solid var(--color-border);border-radius:4px;overflow:hidden;flex-shrink:0}
.view-switcher button{background:var(--color-bg);border:none;border-right:1px solid var(--color-border);padding:4px 16px;cursor:pointer;font-size:13px;color:var(--color-text-secondary);transition:all .15s}
.view-switcher button:last-child{border-right:none}
.view-switcher button:hover{background:var(--color-bg-secondary);color:var(--color-text)}
.view-switcher button.active{background:var(--color-primary-light);color:var(--color-primary);font-weight:500}
#settings-btn{background:none;border:1px solid var(--color-border);border-radius:50%;width:36px;height:36px;cursor:pointer;font-size:18px;margin-left:4px;color:var(--color-text-secondary)}
.main{height:calc(100vh - var(--header-height));overflow:hidden;position:relative}
.view-container{width:100%;height:100%}
.view-container.hidden{display:none}
/* MONTH VIEW */
.month-header{display:grid;grid-template-columns:repeat(7,1fr);border-bottom:1px solid var(--color-border)}
.month-header-cell{padding:8px 0;text-align:center;font-size:11px;font-weight:500;color:var(--color-text-secondary);text-transform:uppercase;letter-spacing:.5px}
.month-grid{display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(6,1fr);border-left:1px solid var(--color-border);border-bottom:1px solid var(--color-border);flex:1}
.month-cell{border-right:1px solid var(--color-border);border-bottom:1px solid var(--color-border);padding:2px 3px;min-height:0;cursor:pointer;overflow:hidden;transition:background .1s}
.month-cell:hover{background:var(--color-bg-secondary)}
.month-cell.other-month{background:var(--color-bg-secondary)}
.month-cell.other-month .day-number{color:var(--color-text-secondary);opacity:.5}
.month-cell.today .day-number{width:26px;height:26px;background:var(--color-today);border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--color-primary);font-weight:700;margin:0 auto}
.day-number{font-size:11px;font-weight:500;color:var(--color-text);padding:2px;min-height:18px;display:flex;align-items:center;justify-content:center}
.month-events{display:flex;flex-direction:column;gap:1px;overflow:hidden}
.month-event-chip{font-size:11px;padding:1px 4px;border-radius:2px;cursor:grab;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;border-left:3px solid;user-select:none;line-height:1.4}
.month-event-chip:active{cursor:grabbing}
.month-more-chip{font-size:11px;padding:1px 4px;color:var(--color-text-secondary);cursor:pointer;border-left:3px solid var(--color-border)}
.month-more-chip:hover{background:var(--color-bg-secondary);color:var(--color-text)}
/* DAY POPOVER */
.day-popover{position:fixed;background:var(--color-bg);border:1px solid var(--color-border);border-radius:8px;box-shadow:0 4px 24px rgba(0,0,0,.15);padding:12px;width:300px;max-height:400px;overflow-y:auto;z-index:100;display:none}
.day-popover.visible{display:block}
.day-popover-header{font-weight:500;margin-bottom:8px;font-size:14px}
.day-popover-event{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:4px;cursor:pointer;margin-bottom:2px}
.day-popover-event:hover{background:var(--color-bg-secondary)}
.day-popover-event-color{width:4px;height:24px;border-radius:2px;flex-shrink:0}
.day-popover-event-info{flex:1;min-width:0}
.day-popover-event-title{font-size:13px;font-weight:500}
.day-popover-event-time{font-size:11px;color:var(--color-text-secondary)}
.day-popover-add{margin-top:8px;padding:6px 12px;border:1px dashed var(--color-border);border-radius:4px;text-align:center;color:var(--color-primary);font-size:13px;cursor:pointer;width:100%}
.day-popover-add:hover{background:var(--color-primary-light);border-color:var(--color-primary)}
/* DRAG GHOST */
.drag-ghost{position:fixed;pointer-events:none;z-index:1000;padding:4px 10px;border-radius:3px;font-size:11px;font-weight:500;box-shadow:0 2px 8px rgba(0,0,0,.2);opacity:.85;display:none}
/* WEEK VIEW */
.week-view{height:100%;display:flex;flex-direction:column}
.week-header{display:flex;border-bottom:1px solid var(--color-border);background:var(--color-bg);flex-shrink:0}
.week-header-corner{width:56px;flex-shrink:0;border-right:1px solid var(--color-border)}
.week-header-days{display:flex}
.week-header-day{flex:1;text-align:center;padding:6px 0 4px;border-right:1px solid var(--color-border)}
.week-header-day:last-child{border-right:none}
.week-header-day .day-label{font-size:11px;color:var(--color-text-secondary);text-transform:uppercase}
.week-header-day .day-number{font-size:18px;font-weight:400;margin:2px 0 0;width:36px;height:36px;display:flex;align-items:center;justify-content:center;margin-inline:auto}
.week-header-day.today .day-number{background:var(--color-primary);color:white;border-radius:50%;font-weight:700}
.week-body{flex:1;display:flex;flex-direction:column;overflow:hidden}
.week-body-scroll{flex:1;display:flex;overflow-y:auto;overflow-x:hidden;position:relative}
.week-time-gutter{width:56px;flex-shrink:0}
.week-time-label{font-size:10px;color:var(--color-text-secondary);text-align:right;padding-right:8px;height:48px;line-height:48px;transform:translateY(-7px)}
.week-day-columns{flex:1;display:flex}
.week-day-column{flex:1;position:relative;border-right:1px solid var(--color-border)}
.week-day-column:last-child{border-right:none}
.week-hour-row{border-bottom:1px solid var(--color-border);height:48px;position:relative}
.week-hour-row.half-hour{border-bottom:1px solid #f5f5f5;height:24px}
.week-now-line{position:absolute;left:0;right:0;height:2px;background:var(--color-danger);z-index:5;pointer-events:none}
.week-now-line::before{content:'';position:absolute;left:-5px;top:-4px;width:10px;height:10px;background:var(--color-danger);border-radius:50%}
.week-all-day-area{height:32px;border-bottom:1px solid var(--color-border);display:flex;flex-shrink:0;position:relative}
.week-all-day-area .all-day-divider{width:56px;flex-shrink:0;border-right:1px solid var(--color-border);background:var(--color-bg)}
.week-event-block{position:absolute;left:2px;right:2px;border-radius:3px;padding:1px 4px;font-size:11px;overflow:hidden;cursor:grab;user-select:none;border-left:3px solid;line-height:1.3;z-index:3}
.week-event-block:active{cursor:grabbing}
.week-event-block .event-block-title{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.week-event-block .event-block-time{font-size:10px;opacity:.8}
/* AGENDA VIEW */
.agenda-view{height:100%;overflow-y:auto;padding:8px 0}
.agenda-day{margin-bottom:4px}
.agenda-day-header{font-size:13px;font-weight:500;color:var(--color-text-secondary);padding:8px 16px;border-bottom:1px solid var(--color-border)}
.agenda-day-events{padding:4px 0}
.agenda-event{display:flex;align-items:center;padding:6px 16px;gap:12px;cursor:pointer;border-radius:4px;margin:0 8px}
.agenda-event:hover{background:var(--color-bg-secondary)}
.agenda-event-color{width:4px;height:32px;border-radius:2px;flex-shrink:0}
.agenda-event-time{font-size:12px;color:var(--color-text-secondary);white-space:nowrap;min-width:80px}
.agenda-event-title{font-size:14px;font-weight:500;flex:1}
/* MODAL */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:200;display:none;align-items:center;justify-content:center}
.modal-overlay.visible{display:flex}
.dialog{background:var(--color-bg);border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,.2);width:100%;max-width:480px;max-height:90vh;overflow-y:auto;padding:24px}
.modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}
.modal-title{font-size:20px;font-weight:400}
.modal-close{background:none;border:none;font-size:22px;cursor:pointer;color:var(--color-text-secondary);padding:4px 8px;border-radius:50%}
.modal-close:hover{background:var(--color-bg-secondary)}
.form-group{margin-bottom:16px}
.form-group label{display:block;font-size:12px;font-weight:500;color:var(--color-text-secondary);margin-bottom:4px;text-transform:uppercase;letter-spacing:.5px}
.form-group input,.form-group textarea,.form-group select{width:100%;padding:8px 12px;border:1px solid var(--color-border);border-radius:6px;font-size:14px;font-family:inherit;color:var(--color-text);outline:none;transition:border-color .15s}
.form-group input:focus,.form-group textarea:focus,.form-group select:focus{border-color:var(--color-primary);box-shadow:0 0 0 2px var(--color-primary-light)}
.form-group textarea{resize:vertical;min-height:60px}
.form-group .error-msg{font-size:12px;color:var(--color-danger);margin-top:4px;display:none}
.form-group .error-msg.visible{display:block}
.form-row{display:flex;gap:12px}
.form-row .form-group{flex:1}
.toggle-group{display:flex;gap:6px}
.toggle-btn{padding:6px 14px;border:1px solid var(--color-border);border-radius:16px;background:var(--color-bg);cursor:pointer;font-size:13px;color:var(--color-text-secondary);transition:all .15s}
.toggle-btn:hover{border-color:var(--color-text-secondary)}
.toggle-btn.active{background:var(--color-primary-light);border-color:var(--color-primary);color:var(--color-primary);font-weight:500}
.color-picker{display:flex;gap:6px;flex-wrap:wrap}
.color-swatch{width:28px;height:28px;border-radius:50%;cursor:pointer;border:2px solid transparent;transition:transform .15s}
.color-swatch:hover{transform:scale(1.15)}
.color-swatch.selected{border-color:var(--color-text);transform:scale(1.15)}
.recurrence-group{border:1px solid var(--color-border);border-radius:8px;padding:12px;margin-top:4px}
.recurrence-options{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}
.weekday-selector{display:flex;gap:4px;margin-top:8px;flex-wrap:wrap}
.weekday-btn{padding:4px 8px;border:1px solid var(--color-border);border-radius:4px;background:var(--color-bg);cursor:pointer;font-size:12px;color:var(--color-text-secondary);transition:all .15s}
.weekday-btn.selected{background:var(--color-primary-light);border-color:var(--color-primary);color:var(--color-primary)}
.recurrence-extra{margin-top:6px;font-size:12px;color:var(--color-text-secondary)}
.modal-actions{display:flex;justify-content:space-between;margin-top:24px;gap:8px}
.btn{padding:8px 20px;border-radius:6px;font-size:14px;font-weight:500;cursor:pointer;border:none;transition:all .15s;font-family:inherit}
.btn-primary{background:var(--color-primary);color:white}
.btn-primary:hover{background:#1557b0}
.btn-secondary{background:var(--color-bg-secondary);color:var(--color-text);border:1px solid var(--color-border)}
.btn-secondary:hover{background:#e8eaed}
.btn-danger{background:var(--color-danger);color:white}
.btn-danger:hover{background:#b3261e}
.btn-ghost{background:none;color:var(--color-danger);padding:8px 12px}
.btn-ghost:hover{background:var(--color-danger-light)}
/* RECURRING EDIT DIALOG */
.recurring-dialog{width:100%;max-width:400px;padding:24px}
.recurring-option{padding:12px 16px;border:1px solid var(--color-border);border-radius:8px;margin-bottom:8px;cursor:pointer;transition:all .15s}
.recurring-option:hover{background:var(--color-bg-secondary)}
.recurring-option.selected{border-color:var(--color-primary);background:var(--color-primary-light)}
.recurring-option-title{font-size:14px;font-weight:500}
.recurring-option-desc{font-size:12px;color:var(--color-text-secondary);margin-top:2px}
/* SETTINGS */
.settings-overlay{position:fixed;inset:0;background:rgba(0,0,0,.4);z-index:200;display:none;align-items:center;justify-content:center}
.settings-overlay.visible{display:flex}
.settings-panel{background:var(--color-bg);border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,.2);padding:24px;width:340px}
.settings-panel h3{font-size:18px;font-weight:400;margin-bottom:20px}
.setting-item{display:flex;align-items:center;justify-content:space-between;padding:10px 0;border-bottom:1px solid var(--color-border)}
.setting-item:last-of-type{border-bottom:none}
.setting-label{font-size:14px}
/* TOAST */
.toast-container{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:300;display:flex;flex-direction:column;align-items:center;gap:8px}
.toast{background:#323232;color:white;padding:10px 20px;border-radius:8px;font-size:14px;box-shadow:0 4px 12px rgba(0,0,0,.3);display:flex;align-items:center;gap:12px}
.toast-action{color:var(--color-primary);font-weight:500;cursor:pointer;background:none;border:none;font-size:14px;font-family:inherit}
/* RESPONSIVE */
@media(max-width:768px){.header-title{font-size:18px}.month-header-cell{font-size:10px;padding:4px 0}.day-number{font-size:10px}.month-event-chip{font-size:10px;padding:1px 4px}.header{padding:0 8px;gap:6px}.view-switcher button{padding:4px 10px;font-size:12px}}
</style>
</head>
<body>
<div class="header">
<div class="header-title" id="header-title">Calendar</div>
<div class="header-nav">
<button id="today-btn">Today</button>
<button id="prev-btn">‹</button>
<button id="next-btn">›</button>
</div>
<div class="spacer"></div>
<div class="view-switcher">
<button class="view-btn active" data-view="month">Month</button>
<button class="view-btn" data-view="week">Week</button>
<button class="view-btn" data-view="agenda">Agenda</button>
</div>
<button id="settings-btn" title="Settings">⚙</button>
</div>
<div class="main">
<div class="view-container" id="view-month">
<div class="month-header" id="month-header"></div>
<div class="month-grid" id="month-grid"></div>
</div>
<div class="view-container" id="view-week" style="display:none">
<div class="week-view">
<div class="week-header">
<div class="week-header-corner"></div>
<div class="week-header-days" id="week-header-days"></div>
</div>
<div class="week-body">
<div class="week-body-scroll" id="week-body-scroll">
<div class="week-time-gutter" id="week-time-gutter"></div>
<div class="week-day-columns" id="week-day-columns"></div>
</div>
</div>
</div>
</div>
<div class="view-container" id="view-agenda" style="display:none">
<div class="agenda-view" id="agenda-container"></div>
</div>
</div>
<!-- EVENT EDITOR MODAL -->
<div class="modal-overlay" id="editor-overlay">
<div class="dialog">
<div class="modal-header">
<div class="modal-title" id="editor-title">New Event</div>
<button class="modal-close" id="editor-close">×</button>
</div>
<div class="form-group">
<label>Title</label>
<input type="text" id="e-title" placeholder="Add title" autocomplete="off">
<div class="error-msg" id="e-title-err"></div>
</div>
<div class="form-group">
<div class="toggle-group" id="e-allday">
<button class="toggle-btn active" data-val="true">All day</button>
<button class="toggle-btn" data-val="false">Has start/end time</button>
</div>
</div>
<div class="form-group"><label>Date</label><input type="date" id="e-date"></div>
<div class="form-row" id="e-time-row">
<div class="form-group"><label>Start</label><input type="time" id="e-start"><div class="error-msg" id="e-start-err"></div></div>
<div class="form-group"><label>End</label><input type="time" id="e-end"><div class="error-msg" id="e-end-err"></div></div>
</div>
<div class="form-group"><label>Notes</label><textarea id="e-notes" rows="2"></textarea></div>
<div class="form-group">
<label>Color</label>
<div class="color-picker" id="e-colors"></div>
</div>
<div class="form-group">
<label>Recurrence</label>
<div class="recurrence-group">
<div class="recurrence-options">
<button class="toggle-btn active" data-r="none">None</button>
<button class="toggle-btn" data-r="daily">Daily</button>
<button class="toggle-btn" data-r="weekly">Weekly</button>
<button class="toggle-btn" data-r="monthly">Monthly</button>
<button class="toggle-btn" data-r="yearly">Yearly</button>
</div>
<div id="e-weekday-group" style="display:none">
<div class="weekday-selector" id="e-weekdays">
<button class="weekday-btn" data-d="1">Mon</button>
<button class="weekday-btn" data-d="2">Tue</button>
<button class="weekday-btn" data-d="3">Wed</button>
<button class="weekday-btn" data-d="4">Thu</button>
<button class="weekday-btn" data-d="5">Fri</button>
<button class="weekday-btn" data-d="6">Sat</button>
<button class="weekday-btn" data-d="0">Sun</button>
</div>
</div>
</div>
</div>
<div class="modal-actions" style="font-size:12px">
<div style="display:flex;gap:8px">
<button class="btn btn-danger" id="e-delete" style="display:none">Delete</button>
<button class="btn btn-ghost" id="e-cancel" style="display:none">Cancel</button>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-primary" id="e-save">Save</button>
</div>
</div>
</div>
</div>
<!-- RECURRING EDIT CHOSE MODAL -->
<div class="modal-overlay" id="recur-overlay">
<div class="recurring-dialog">
<div class="modal-title" style="margin-bottom:16px">Edit Recurring Event</div>
<div class="recurring-option" data-c="entire">
<div class="recurring-option-title">Entire series</div>
<div class="recurring-option-desc">Edit all occurrences</div>
</div>
<div class="recurring-option selected" data-c="this">
<div class="recurring-option-title">This event only</div>
<div class="recurring-option-desc">Edit only this occurrence</div>
</div>
<div class="recurring-option" data-c="future">
<div class="recurring-option-title">This and following</div>
<div class="recurring-option-desc">Edit this and all future occurrences</div>
</div>
<div class="modal-actions" style="margin-top:16px">
<button class="btn btn-secondary" id="recur-cancel">Cancel</button>
<button class="btn btn-primary" id="recur-ok">OK</button>
</div>
</div>
</div>
<!-- DAY POPOVER -->
<div class="day-popover" id="day-popover">
<div class="day-popover-header" id="dp-header"></div>
<div id="dp-events"></div>
<div class="day-popover-add" id="dp-add">+ Add event</div>
</div>
<!-- SETTINGS -->
<div class="settings-overlay" id="settings-overlay">
<div class="settings-panel">
<h3>Settings</h3>
<div class="setting-item">
<span class="setting-label">Week starts on</span>
<div class="toggle-group" id="s-weekstart">
<button class="toggle-btn" data-ws="1">Monday</button>
<button class="toggle-btn" data-ws="0">Sunday</button>
</div>
</div>
<div class="modal-actions" style="margin-top:16px">
<button class="btn btn-primary" id="s-save">Done</button>
</div>
</div>
</div>
<div class="toast-container" id="toasts"></div>
<div class="drag-ghost" id="drag-ghost"></div>
<script>
/* ============================================================
CONSTANTS
============================================================ */
const CFG = Object.freeze({
PALETTE: ['#1a73e8','#d93025','#1e8e3e','#f9ab00','#9334e6','#e67c73','#185abc','#3c6e71','#b06000','#8430ce'],
HOUR_START: 6, HOUR_END: 22, SNAP: 15,
MAX_MONTH_EVENTS: 3,
LS_EVENTS: 'cal_events',
LS_SETTINGS: 'cal_settings',
LS_VIEW: 'cal_view'
});
/* ============================================================
DATE UTILITIES — pure functions, no Date arithmetic hacks
============================================================ */
const DU = {
isLeap(y){ return (y%400===0)||(y%4===0&&y%100!==0); },
dim(y,m){ return [31,DU.isLeap(y)?29:28,31,30,31,30,31,31,30,31,30,31][m]; },
dow(y,m,d){ return new Date(y,m,d).getDay(); }, // 0=Sun
addDays(y,m,d,n){
let cy=y,cm=m,cd=d+n;
while(cd<1){cm--;if(cm<0){cm=11;cy--}cd+=DU.dim(cy,cm)}
while(cd>DU.dim(cy,cm)){cd-=DU.dim(cy,cm);cm++;if(cm>11){cm=0;cy++}}
return {y:cy,m:cm,d:cd};
},
addMonths(y,m,d,n){
let cy=y,cm=m+n;
while(cm<0){cm+=12;cy--}while(cm>11){cm-=12;cy++}
const cd2=Math.min(d,DU.dim(cy,cm));
return {y:cy,m:cm,d:cd2};
},
addYears(y,m,d,n){ return {y:y+n,m:m,d:d}; }, // typo fixed below
same(a,b){ return a.y===b.y&&a.m===b.m&&a.d===b.d; },
weekStart(y,m,d,ws){
const dw=DU.dow(y,m,d);
let diff=dw-ws;if(diff<0)diff+=7;
return DU.addDays(y,m,d,-diff);
},
weekEnd(y,m,d,ws){ const ws2=DU.weekStart(y,m,d,ws); return DU.addDays(ws2.y,ws2.m,ws2.d,6); },
fmt(y,m,d){ return `${Z2(y)}-${Z2(m+1)}-${Z2(d)}`; },
parse(s){const p=s.split('-').map(Number);return{y:p[0],m:p[1]-1,d:p[2]};},
fmtTime(h,m){const a=h>=12?'PM':'AM'; return`${h%12||12}:${Z2(m)} ${a}`;},
shortDay(dw){return['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][dw];},
dayName(dw){return['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'][dw];},
monthName(m){return['January','February','March','April','May','June','July','August','September','October','November','December'][m];},
shortMonth(m){return['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][m];},
pad(n,w=2){return String(n).padStart(w,'0');}
};
const Z2=DU.pad;
// Fix addYears
DU.addYears=function(y,m,d,n){return{y:y+n,m:m,d:d};};
/* ============================================================
RECURRENCE ENGINE
============================================================ */
const Rec = {
advance(r,c,wd){
switch(r){
case'daily': return DU.addDays(c.y,c.m,c.d,1);
case'weekly':
if(wd&&wd.length>0){
let nx=DU.addDays(c.y,c.m,c.d,1),it=0;
while(it<14){if(wd.includes(DU.dow(nx.y,nx.m,nx.d)))return nx;nx=DU.addDays(nx.y,nx.m,nx.d,1);it++}
return DU.addDays(c.y,c.m,c.d,7);
}
return DU.addDays(c.y,c.m,c.d,7);
case'monthly':{
// Try successive months until we find one with the same day
let nx=DU.addMonths(c.y,c.m,c.d,1),lim=186;
while(nx.d!==c.d&&lim-->0){
nx=DU.addMonths(nx.y,nx.m,nx.d,1);
}
return nx;
}
case'yearly': return {y:c.y+1,m:c.m,d:c.d};
default: return DU.addDays(c.y,c.m,c.d,1);
}
},
// Expand recurring events into occurrence array for [startStr..endStr]
expand(masterDateStr, master, allEvents, startStr, endStr){
if(!master.recurrence||master.recurrence==='none'){
if(masterDateStr>=startStr&&masterDateStr<=endStr) return[{...master,date:masterDateStr,isOcc:true}];
return [];
}
const exc={};
allEvents.forEach(e=>{
if(e.parentEvent===master.id&&(e.recurrence==='exception'||e._exc)) exc[e.date]=e;
});
const md=DU.parse(masterDateStr);
let cur={...md}, occs=[], mx=366*4;
const pad=DU.pad;
while(cur.y<=endStr.substring(0,4)&&mx-->0){
const ds=`${pad(cur.y,4)}-${pad(cur.m+1,2)}-${pad(cur.d,2)}`;
if(ds>endStr) break;
if(ds>=startStr){
if(exc[ds]){
occs.push({...exc[ds],date:ds,isOcc:true,_isExc:true});
} else {
occs.push({...master,date:ds,isOcc:true,_isExc:false});
}
}
cur=this.advance(master.recurrence,cur,master.weekdays);
}
return occs;
}
};
/* ============================================================
STATE
============================================================ */
const S = {
events: [], // raw stored events
ocs: [], // expanded occurrences for current view
settings: {ws:1}, // ws = weekStart: 1=Mon, 0=Sun
view: {v:'month',y:null,m:null}, // v=view, y=year, m=month
_dragId: null, // event id being dragged
_editId: null, // event id being edited
_isModalNew: true,
_allday: true,
_rec: 'none',
_color: CFG.PALETTE[0],
_weekdays: [],
_recurChoice: 'this', // for edit-choosing modal
_recurMaster: null, // master event for recurring-edit
_editContext: null // extra context for save
};
function nowDS(){
const t=new Date();return DU.fmt(t.getFullYear(),t.getMonth(),t.getDate());
}
function visibleRange(){
const {v,y,m}=S.view;
let s,e;
if(v==='month'){
const ws=DU.weekStart(y,m,1,S.settings.ws);
s=DU.fmt(ws.y,ws.m,ws.d);
const we=DU.weekEnd(y,m,DU.dim(y,m),S.settings.ws);
e=DU.addDays(we.y,we.m,we.d,6);
e=DU.fmt(e.y,e.m,e.d);
} else if(v==='week'){
const ws=DU.weekStart(y,m,1,S.settings.ws);
s=DU.fmt(ws.y,ws.m,ws.d);
const we=DU.weekEnd(y,m,1,S.settings.ws);
e=DU.fmt(we.y,we.m,we.d);
} else {
const t=new Date();
s=DU.fmt(t.getFullYear(),t.getMonth(),t.getDate());
const ft={y:t.getFullYear(),m:t.getMonth()+2,d:t.getDate()};
e=DU.fmt(ft.y,ft.m,Math.min(ft.d,DU.dim(ft.y,ft.m)));
}
return {s,e};
}
function refresh(){
const {s,e}=visibleRange();
S.ocs=[];
for(const ev of S.events){
const md=DU.parse(ev.date);
const ms=DU.fmt(md.y,md.m,md.d);
S.ocs.push(...Rec.expand(ms,ev,S.events,s,e));
}
S.ocs.sort((a,b)=>{if(a.date!==b.date)return a.date<b.date?-1:1;if(a.allDay!==b.allDay)return a.allDay?-1:1;if((a.startTime||'')<(b.startTime||''))return -1;return 0;});
}
function ocsForDay(ds){return S.ocs.filter(o=>o.date===ds)}
/* ============================================================
STORAGE
============================================================ */
function load(){
try{S.events=JSON.parse(localStorage.getItem(CFG.LS_EVENTS))||[]}catch(S){S.events=[]}
try{const st=JSON.parse(localStorage.getItem(CFG.LS_SETTINGS));if(st)S.settings=st}catch(S){}
try{const sv=JSON.parse(localStorage.getItem(CFG.LS_VIEW));if(sv)S.view=sv}catch(S){}
if(!S.view.v)S.view={v:'month',y:new Date().getFullYear(),m:new Date().getMonth()};
if(!S.view.y)S.view.y=new Date().getFullYear();
if(S.view.m==null)S.view.m=new Date().getMonth();
}
function saveAll(){
localStorage.setItem(CFG.LS_EVENTS,JSON.stringify(S.events));
localStorage.setItem(CFG.LS_SETTINGS,JSON.stringify(S.settings));
localStorage.setItem(CFG.LS_VIEW,JSON.stringify(S.view));
}
function gid(){return Date.now().toString(36)+Math.random().toString(36).slice(2,8)}
/* ============================================================
RENDERING — month header
============================================================ */
function renderMonthHeader(){
const el=document.getElementById('month-header');
let h='';
for(let i=0;i<7;i++){
const d=(S.settings.ws+i)%7;
h+=`<div class="month-header-cell">${DU.shortDay(d)}</div>`;
}
el.innerHTML=h;
}
/* ============================================================
RENDERING — month grid
============================================================ */
let _monthPopover=null;
let _monthPopoverDate=null;
function renderMonthGrid(){
const {y,m}=S.view;
const el=document.getElementById('month-grid');
el.innerHTML='';
const todayDS=nowDS();
// First day of month
const firstDow=DU.dow(y,m,1);
let lead=firstDow-S.settings.ws;if(lead<0)lead+=7;
let cur=DU.addDays(y,m,1,-lead);
for(let i=0;i<42;i++){
const ds=DU.fmt(cur.y,cur.m,cur.d);
const isCM=cur.m===m;
const isT=ds===todayDS;
const cl=['month-cell'];
if(!isCM)cl.push('other-month');
if(isT)cl.push('today');
const num=document.createElement('div');
num.className='day-number';
num.textContent=cur.d;
const evDiv=document.createElement('div');
evDiv.className='month-events';
const dayOcs=ocsForDay(ds);
const show=dayOcs.slice(0,CFG.MAX_MONTH_EVENTS);
const extra=Math.max(0,dayOcs.length-CFG.MAX_MONTH_EVENTS);
for(const ev of show){
const chip=document.createElement('div');
chip.className='month-event-chip';
chip.style.color=ev.color;
chip.style.background=ev.color+'20';
chip.style.borderLeftColor=ev.color;
const ts=ev.allDay?ev.title:DU.fmtTime(parseInt(ev.startTime.split(':')[0]),parseInt(ev.startTime.split(':')[1]))+' '+ev.title;
chip.textContent=ev.title||'(No title)';
chip.addEventListener('click',e=>{e.stopPropagation();openClickEvent(ev,ds)});
chip.draggable=true;
chip.addEventListener('dragstart',e=>{e.stopPropagation();S._dragId=ev.id;e.dataTransfer.effectAllowed='move';const g=document.getElementById('drag-ghost');g.style.display='block';g.style.color=ev.color;g.style.background=ev.color+'20';g.textContent=(ev.title||'(event)')});
evDiv.appendChild(chip);
}
if(extra>0){
const more=document.createElement('div');
more.className='month-more-chip';
more.textContent=`+${extra} more`;
more.addEventListener('click',e=>{e.stopPropagation();showDayPicker(ds,e)});
evDiv.appendChild(more);
}
const cell=document.createElement('div');
cell.className=cl.join(' ');
cell.dataset.date=ds;
cell.appendChild(num);
cell.appendChild(evDiv);
cell.addEventListener('click',()=>{hideDayPicker();openNewEvent(ds)});
cell.addEventListener('dragover',e=>{e.preventDefault();cell.style.background='#e8f0fe'});
cell.addEventListener('dragleave',()=>{cell.style.background=''});
cell.addEventListener('drop',e=>{
e.preventDefault();cell.style.background='';e.stopPropagation();
const eid=e.dataTransfer.getData('text/plain');
const ev=S.events.find(z=>z.id===eid);
if(!ev)return;
// If it's a recurring master or instance, create an exception
if(ev.parentEvent){
// Create exception on new date
const excId=gid();
const exc={...ev,id:excId,parentEvent:ev.parentEvent,date:ds,recurrence:'exception',_exc:true};
S.events.push(exc);
saveAll();refresh();renderCurrent();showToast('Moved to '+ds);
return;
}
ev.date=ds;
saveAll();refresh();renderCurrent();showToast('Event rescheduled');
});
el.appendChild(cell);
cur=DU.addDays(cur.y,cur.m,cur.d,1);
}
// Store date on popover for reference
_monthPopoverDate=null;
}
function openClickEvent(ev,ds){
// Recurring check
if(ev.parentEvent&&!ev._exc){
S._recurMaster=ev;
document.getElementById('recur-overlay').classList.add('visible');
document.querySelectorAll('.recurring-option').forEach(o=>o.classList.toggle('selected',o.dataset.c==='this'));
S._recurChoice='this';
return;
}
openEditModal(ev.id);
}
/* ============================================================
DAY PICKER (popover)
============================================================ */
function showDayPicker(ds,ev){
_monthPopoverDate=ds;
const pop=document.getElementById('day-popover');
const parsed=DU.parse(ds);
document.getElementById('dp-header').textContent=DU.dayName(DU.dow(parsed.y,parsed.m,parsed.d))+', '+DU.monthName(parsed.m)+' '+parsed.d+', '+parsed.y;
const cd=document.getElementById('dp-events');
cd.innerHTML='';
for(const o of ocsForDay(ds)){
const d=document.createElement('div');
d.className='day-popover-event';
const bar=document.createElement('div');
bar.className='day-popover-event-color';bar.style.background=o.color;
const info=document.createElement('div');info.className='day-popover-event-info';
const tt=document.createElement('div');tt.className='day-popover-event-title';tt.textContent=o.title;
const tm=document.createElement('div');tm.className='day-popover-event-time';
tm.textContent=o.allDay?'All day':(o.startTime&&o.endTime?DU.fmtTime(parseInt(o.startTime.slice(0,2)),parseInt(o.startTime.slice(3)))+' - '+DU.fmtTime(parseInt(o.endTime.slice(0,2)),parseInt(o.endTime.slice(3))):'');
info.append(tt,tm);d.append(bar,info);
d.addEventListener('click',()=>{hideDayPicker();openClickEvent(o,ds)});
cd.appendChild(d);
}
// Position
const rect=ev?ev.target.closest('.month-cell').getBoundingClientRect():null;
if(rect){pop.style.left=Math.min(rect.right+4,window.innerWidth-310)+'px';pop.style.top=rect.top+'px';}else{pop.style.left='100px';pop.style.top='100px';}
pop.classList.add('visible');
}
function hideDayPicker(){document.getElementById('day-popover').classList.remove('visible')}
document.getElementById('dp-add').addEventListener('click',()=>{
hideDayPicker();
if(_monthPopoverDate) openNewEvent(_monthPopoverDate);
});
document.addEventListener('click',e=>{
const pop=document.getElementById('day-popover');
if(pop.classList.contains('visible')&&!pop.contains(e.target)&&!e.target.closest('.month-more-chip')&&!e.target.closest('.month-event-chip')&&!e.target.closest('.day-popover-event')) hideDayPicker();
});
/* ============================================================
RENDERING — week view
============================================================ */
function renderWeek(){
const {y,m}=S.view;
const ws=DU.weekStart(y,m,1,S.settings.ws);
// Header
let hh='';
let ws2=DU.fmt(ws.y,ws.m,ws.d);
for(let i=0;i<7;i++){
const pd=DU.parse(ws2);
const isT=ws2===nowDS();
hh+=`<div class="week-header-day${isT?' today':''}"><div class="day-label">${DU.shortDay(DU.dow(pd.y,pd.m,pd.d))}</div><div class="day-number">${pd.d}</div></div>`;
ws2=DU.addDays(pd.y,pd.m,pd.d,1);
}
document.getElementById('week-header-days').innerHTML=hh;
// Build body
const scroll=document.getElementById('week-body-scroll');
scroll.innerHTML='';
// Time gutter
const gut=document.createElement('div');gut.className='week-time-gutter';
for(let h=1;h<=CFG.HOUR_END;h++){
const lb=document.createElement('div');lb.className='week-time-label';
lb.textContent=h>=CFG.HOUR_START?DU.fmtTime(h,0):'';
gut.appendChild(lb);
}
scroll.appendChild(gut);
// Columns
const cols=document.createElement('div');cols.style.cssText='flex:1;display:flex;';
const ppm=48/60;
const todayDS=nowDS();
let wd=DU.fmt(ws.y,ws.m,ws.d);
for(let i=0;i<7;i++){
const col=document.createElement('div');col.className='week-day-column';col.dataset.date=wd;
// Events for day
const dayO=ocsForDay(wd);
// Hour rows + event blocks
for(let h=CFG.HOUR_START;h<=CFG.HOUR_END;h++){
const hr=document.createElement('div');
hr.className='week-hour-row';
hr.addEventListener('click',()=>openNewEvent(wd,DU.pad(h,2)+':00'));
col.appendChild(hr);
const hr2=document.createElement('div');
hr2.className='week-hour-row half-hour';
hr2.addEventListener('click',()=>openNewEvent(wd,DU.pad(h,2)+':30'));
col.appendChild(hr2);
const tStart=`${DU.pad(h,2)}:00`;
const tEnd2=`${DU.pad(h+1,2)}:00`;
for(const ev of dayO){
if(ev.allDay)continue;
if(!ev.startTime||!ev.endTime)continue;
if(ev.startTime<tStart||ev.startTime>=tEnd2)continue;
const bk=document.createElement('div');
bk.className='week-event-block';
const[sh,sm]=ev.startTime.split(':').map(Number);
const[eh,em]=ev.endTime.split(':').map(Number);
const dur=Math.max(CFG.SNAP,(eh*60+em)-(sh*60+sm));
bk.style.top=`${(sh*60+sm)*ppm}px`;
bk.style.height=`${dur*ppm}px`;
bk.style.background=ev.color+'20';bk.style.color=ev.color;bk.style.borderLeftColor=ev.color;
bk.innerHTML=`<div class="event-block-title">${ev.title||'(No title)'}</div><div class="event-block-time">${DU.fmtTime(sh,sm)}–${DU.fmtTime(eh,em)}</div>`;
bk.addEventListener('click',e=>{e.stopPropagation();openClickEvent(ev,wd)});
bk.draggable=true;
bk.addEventListener('dragstart',e=>{e.stopPropagation();S._dragId=ev.id;e.dataTransfer.effectAllowed='move'});
col.appendChild(bk);
}
}
// Now line
if(wd===todayDS){
const n=new Date();
const nl=document.createElement('div');nl.className='week-now-line';
nl.style.top=`${(n.getHours()*60+n.getMinutes())*ppm}px`;
col.appendChild(nl);
}
// Drag-drop for re-time
col.addEventListener('dragover',e=>e.preventDefault());
col.addEventListener('drop',e=>{
e.preventDefault();
const eid=e.dataTransfer.getData('text/plain');
const ev=S.events.find(z=>z.id===eid);
if(!ev||ev.allDay)return;
const rect=col.getBoundingClientRect();
const y2=e.clientY-rect.top;
const totMin=Math.round((y2/ppm)/CFG.SNAP)*CFG.SNAP;
const nh=Math.floor(totMin/60),nm=totMin%60;
if(nh>=CFG.HOUR_START&&nh<=CFG.HOUR_END){
ev.startTime=`${DU.pad(nh,2)}:${DU.pad(nm,2)}`;
if(ev.endTime){
const[eh,em]=ev.endTime.split(':').map(Number);
if(eh*60+em<=nh*60+nm)ev.endTime=`${DU.pad(nh,2)}:${DU.pad(Math.max(nm+CFG.SNAP,15),2)}`;
}
saveAll();refresh();renderCurrent();showToast('Time updated');
}
});
cols.appendChild(col);
wd=DU.addDays(DU.parse(wd).y,DU.parse(wd).m,DU.parse(wd).d,1);
}
scroll.appendChild(cols);
}
/* ============================================================
RENDERING — agenda
============================================================ */
function renderAgenda(){
const c=document.getElementById('agenda-container');
c.innerHTML='';
const {s}=visibleRange();
let curS=null,group=null;
const todayDS=nowDS();
for(const o of S.ocs){
if(o.date<s)continue;
if(o.date>visibleRange().e)break;
if(o.date!==curS){
curS=o.date;
if(group)c.appendChild(group);
const pd=DU.parse(curS);
const label=todayDS===curS?'Today ':''+DU.dayName(DU.dow(pd.y,pd.m,pd.d))+' — '+DU.monthName(pd.m)+' '+pd.d;
group=document.createElement('div');group.className='agenda-day';
const hdr=document.createElement('div');hdr.className='agenda-day-header';hdr.textContent=label;group.appendChild(hdr);
const evs=document.createElement('div');evs.className='agenda-day-events';group.appendChild(evs);
}
const ed=document.createElement('div');ed.className='agenda-event';ed.addEventListener('click',()=>openClickEvent(o,curS));
const bar=document.createElement('div');bar.className='agenda-event-color';bar.style.background=o.color;
const tm=document.createElement('span');tm.className='agenda-event-time';
tm.textContent=o.allDay?'All day':(o.startTime&&o.endTime?DU.fmtTime(parseInt(o.startTime.slice(0,2)),parseInt(o.startTime.slice(3)))+
' – '+DU.fmtTime(parseInt(o.endTime.slice(0,2)),parseInt(o.endTime.slice(3))):'None');
const tl=document.createElement('span');tl.className='agenda-event-title';tl.textContent=o.title||'(No title)';
ed.append(bar,tm,tl);group.querySelector('.agenda-day-events').appendChild(ed);
}
if(group)c.appendChild(group);
if(!c.children.length)c.innerHTML='<div style="text-align:center;padding:60px 16px;color:var(--color-text-secondary)"><h3 style="font-weight:400;margin-bottom:8px">No upcoming events</h3><p>Click any day to create an event.</p></div>';
}
/* ============================================================
EVENT EDITOR MODAL
============================================================ */
function renderColors(){
const cp=document.getElementById('e-colors');cp.innerHTML='';
for(const c of CFG.PALETTE){
const sw=document.createElement('div');sw.className='color-swatch'+(c===S._color?' selected':'');sw.style.background=c;
sw.addEventListener('click',()=>{S._color=c;renderColors()});
cp.appendChild(sw);
}
}
function renderRecOpts(){
document.querySelectorAll('#e-allday .toggle-btn').forEach(b=>b.classList.toggle('active',b.dataset.val===(S._allday?'true':'false')));
const tr=document.getElementById('e-time-row');
tr.style.display=S._allday?'none':'flex';
document.querySelectorAll('#e-allday .toggle-btn').forEach(b=>b.dataset.val=b.dataset.val==='true'?'true':'false');
document.querySelector('#e-allday .toggle-btn[data-val="'+(S._allday?'true':'false')+'"]').classList.add('active');
document.querySelector('#e-allday .toggle-btn[data-val="'+(S._allday?'false':'true')+'"]');
document.querySelectorAll('#e-allday .toggle-btn').forEach(b=>{
const want=b.dataset.val==='true'?true:false;
b.classList.toggle('active',want===S._allday);
});
document.querySelectorAll('#editor-rc-opt .toggle-btn, .recurrence-options .toggle-btn').forEach(b=>b.classList.toggle('active',b.dataset.r===S._rec));
document.getElementById('e-weekday-group').style.display=S._rec==='weekly'?'block':'none';
document.querySelectorAll('#e-weekdays .weekday-btn').forEach(b=>b.classList.toggle('selected',S._weekdays.includes(parseInt(b.dataset.d))));
}
function openNewEvent(ds,time){
S._editId=null;S._isModalNew=true;S._editContext=null;
document.getElementById('editor-title').textContent='New Event';
document.getElementById('e-delete').style.display='none';
document.getElementById('e-cancel').style.display='none';
document.getElementById('e-title').value='';
document.getElementById('e-date').value=ds||new Date().toISOString().slice(0,10);
document.getElementById('e-notes').value='';
document.getElementById('e-start').value=time||'';
document.getElementById('e-end').value='';
S._allday=true;S._rec='none';S._color=CFG.PALETTE[0];S._weekdays=[];
document.querySelectorAll('.error-msg').forEach(e=>{e.textContent='';e.classList.remove('visible')});
renderRecOpts();renderColors();
document.getElementById('editor-overlay').classList.add('visible');
setTimeout(()=>document.getElementById('e-title').focus(),80);
}
function openEditModal(id){
const ev=S.events.find(z=>z.id===id);
if(!ev)return;
S._editId=id;S._isModalNew=false;S._editContext=null;
document.getElementById('editor-title').textContent='Edit Event';
document.getElementById('e-title').value=ev.title||'';
document.getElementById('e-date').value=ev.date;
document.getElementById('e-start').value=ev.startTime||'';
document.getElementById('e-end').value=ev.endTime||'';
document.getElementById('e-notes').value=ev.notes||'';
document.getElementById('e-delete').style.display='inline-block';
document.getElementById('e-cancel').style.display='inline-block';
S._allday=!!ev.allDay;S._color=ev.color||CFG.PALETTE[0];S._rec=ev.recurrence||'none';S._weekdays=ev.weekdays||[];
document.querySelectorAll('.error-msg').forEach(e=>{e.textContent='';e.classList.remove('visible')});
renderRecOpts();renderColors();
document.getElementById('editor-overlay').classList.add('visible');
}
function closeEditor(){
document.getElementById('editor-overlay').classList.remove('visible');
document.querySelectorAll('.error-msg').forEach(e=>{e.textContent='';e.classList.remove('visible')});
}
function saveEditor(){
// Clear errors
document.querySelectorAll('.error-msg').forEach(e=>{e.textContent='';e.classList.remove('visible')});
let ok=true;
const title=document.getElementById('e-title').value.trim();
if(!title){document.getElementById('e-title-err').textContent='Title required';document.getElementById('e-title-err').classList.add('visible');ok=false}
if(!S._allday){
const st=document.getElementById('e-start').value,ent=document.getElementById('e-end').value;
if(!st){document.getElementById('e-start-err').textContent='Required';document.getElementById('e-start-err').classList.add('visible');ok=false}
if(!ent){document.getElementById('e-end-err').textContent='Required';document.getElementById('e-end-err').classList.add('visible');ok=false}
if(st&&ent&&st>=ent){document.getElementById('e-end-err').textContent='Must be after start';document.getElementById('e-end-err').classList.add('visible');ok=false}
}
if(!ok)return;
const data={
title,date:document.getElementById('e-date').value,
allDay:S._allday,
startTime:S._allday?'':document.getElementById('e-start').value,
endTime:S._allday?'':document.getElementById('e-end').value,
notes:document.getElementById('e-notes').value.trim(),
color:S._color,recurrence:S._rec,weekdays:S._rec==='weekly'?[...S._weekdays]:undefined
};
if(S._isModalNew){
data.id=gid();
S.events.push(data);
showToast('Event created');
} else {
// Determine edit context
if(S._editContext==='exception'){
// Create exception on the current date, edit master
const masterIdx=S.events.findIndex(z=>z.id===S._editId);
if(masterIdx===-1)return;
S.events[masterIdx]={...S.events[masterIdx],...data};
const origDate=DU.parse(S.events[masterIdx].origDate||S.events[masterIdx].date);
showToast('Series updated');
} else if(S._editContext==='split'){
// Clone and keep master for now, just clone the event
const idx=S.events.findIndex(z=>z.id===S._editId);
if(idx===-1)return;
data.id=gid();
// Remove old instance, put new one
S.events[idx]={...S.events[idx],...data};
showToast('Series split and updated');
} else {
const idx=S.events.findIndex(z=>z.id===S._editId);
if(idx===-1)return;
if(S.events[idx].parentEvent){
// Editing a master that has instances
// Save new data on master
data.id=S.events[idx].id;
data.parentEvent=S.events[idx].parentEvent;
}
S.events[idx]={...S.events[idx],...data};
showToast('Event updated');
}
}
saveAll();closeEditor();refresh();renderCurrent();
}
function deleteEditor(){
const ev=S.events.find(z=>z.id===S._editId);
if(!ev)return;
if(ev.parentEvent){
// Exception — just delete
S.events=S.events.filter(z=>z.id!==S._editId);
saveAll();closeEditor();refresh();renderCurrent();showToast('Deleted');
} else if(ev.recurrence&&ev.recurrence!=='none'){
// Delete entire series — ask for confirmation
if(confirm('Delete this entire recurring series?')){
S.events=S.events.filter(z=>z.id!==S._editId);
saveAll();closeEditor();refresh();renderCurrent();showToast('Series deleted');
}
} else {
S.events=S.events.filter(z=>z.id!==S._editId);
saveAll();closeEditor();refresh();renderCurrent();showToast('Deleted');
}
}
/* ============================================================
RECURRING EDIT MODAL
============================================================ */
document.querySelectorAll('.recurring-option').forEach(o=>{
o.addEventListener('click',()=>{
S._recurChoice=o.dataset.c;
document.querySelectorAll('.recurring-option').forEach(x=>x.classList.toggle('selected',x===o));
});
});
document.getElementById('recur-ok').addEventListener('click',()=>{
document.getElementById('recur-overlay').classList.remove('visible');
const master=S._recurMaster;
if(!master)return;
const choice=S._recurChoice;
const ds=DU.parse(master.date);
if(choice==='this'){
// Create an exception instance
const excId=gid();
const exc={
...master,id:excId,parentEvent:master.id,
date:master.date,
recurrence:'exception',_exc:true
};
S.events.push(exc);
saveAll();refresh();
openEditModal(excId); // Open editor for this specific instance
S._editContext='exception-edit';
} else if(choice==='future'){
// Terminate master at prev date, clone for future
// Delete original, create two: original (terminated) and clone (future starts from master date)
const idx=S.events.findIndex(z=>z.id===master.id);
if(idx===-1)return;
const orig={...S.events[idx]};
S.events.splice(idx,1);
// Original: change its date to day before the selected date
const prev=DU.addDays(ds.y,ds.m,ds.d,-1);
orig.date=DU.fmt(prev.y,prev.m,prev.d);
// Clone: starts from selected date, has original recurrence
const clone={
...orig,id:gid(),date:master.date,
// keep all fields same except id and date
};
S.events.push(orig,clone);
saveAll();refresh();openEditModal(clone.id);
} else {
// Entire series
openEditModal(master.id);
}
});
document.getElementById('recur-cancel').addEventListener('click',()=>{
document.getElementById('recur-overlay').classList.remove('visible');
});
document.getElementById('editor-close').addEventListener('click',closeEditor);
document.getElementById('e-cancel').addEventListener('click',closeEditor);
document.getElementById('e-save').addEventListener('click',saveEditor);
document.getElementById('e-delete').addEventListener('click',deleteEditor);
// All-day toggle
document.getElementById('e-allday').addEventListener('click',e=>{
const b=e.target.closest('.toggle-btn');if(!b)return;
S._allday=b.dataset.val==='true';
// re-render all-day toggles
document.querySelectorAll('#e-allday .toggle-btn').forEach(x=>{
const want=x.dataset.val==='true';
x.classList.toggle('active',want===S._allday);
});
renderRecOpts();
});
// Recurrence toggle
document.querySelector('.recurrence-options').addEventListener('click',e=>{
const b=e.target.closest('.toggle-btn');if(!b)return;
S._rec=b.dataset.r;
document.querySelectorAll('.recurrence-options .toggle-btn').forEach(x=>x.classList.toggle('active',x.dataset.r===S._rec));
document.getElementById('e-weekday-group').style.display=S._rec==='weekly'?'block':'none';
});
// Weekday toggle
document.getElementById('e-weekdays').addEventListener('click',e=>{
const b=e.target.closest('.weekday-btn');if(!b)return;
const d=parseInt(b.dataset.d);
S._weekdays=S._weekdays.includes(d)?S._weekdays.filter(x=>x!==d):[...S._weekdays,DU.pad(d).length?d:Math.floor(Math.random()*7)];
// Actually fix: just add/remove the integer day
S._weekdays=S._weekdays.filter(x=>typeof x==='number');
if(S._weekdays.includes(d)&&!b.classList.contains('selected'))S._weekdays=S._weekdays.filter(x=>x!==d);
else if(!S._weekdays.includes(d))S._weekdays.push(d);
document.querySelectorAll('#e-weekdays .weekday-btn').forEach(x=>x.classList.toggle('selected',S._weekdays.includes(parseInt(x.dataset.d))));
});
/* ============================================================
NAVIGATION
============================================================ */
function goToToday(){
const t=new Date();S.view.y=t.getFullYear();S.view.m=t.getMonth();
refresh();renderCurrent();
}
function nav(dir){
if(S.view.v==='month'){
S.view.m+=dir;
if(S.view.m<0){S.view.m=11;S.view.y--}if(S.view.m>11){S.view.m=0;S.view.y++}
} else if(S.view.v==='week'){
const ws=DU.weekStart(S.view.y,S.view.m,1,S.settings.ws);
const nw=DU.addDays(ws.y,ws.m,ws.d,7*dir);
S.view.y=nw.y;S.view.m=nw.m;
} else {
// Agenda: show next/prev month's events
S.view.m+=dir;
if(S.view.m<0){S.view.m=11;S.view.y--}if(S.view.m>11){S.view.m=0;S.view.y++}
}
renderCurrent();
}
function switchView(v){
S.view.v=v;
document.querySelectorAll('.view-btn').forEach(b=>b.classList.toggle('active',b.dataset.view===v));
document.querySelectorAll('.view-container').forEach(c=>c.style.display=c.id==='view-'+v?'':'none');
refresh();renderCurrent();
}
function headerTitle(){
const {v,y,m}=S.view;
if(v==='month')return DU.monthName(m)+' '+y;
if(v==='week'){
const ws=DU.weekStart(y,m,1,S.settings.ws);
const we=DU.weekEnd(y,m,1,S.settings.ws);
if(ws.m===we.m)return DU.monthName(ws.m)+' '+ws.d+'-'+we.d+', '+ws.y;
return DU.shortMonth(ws.m)+' '+ws.d+' – '+DU.shortMonth(we.m)+' '+we.d+', '+we.y;
}
return'Event Planner';
}
function renderCurrent(){
document.getElementById('header-title').textContent=headerTitle();
renderMonthHeader();
if(S.view.v==='month')renderMonthGrid();
else if(S.view.v==='week')renderWeek();
else renderAgenda();
saveAll();
}
/* ============================================================
SETTINGS
============================================================ */
document.getElementById('settings-btn').addEventListener('click',()=>{
document.querySelectorAll('#s-weekstart .toggle-btn').forEach(b=>b.classList.toggle('active',parseInt(b.dataset.ws)===S.settings.ws));
document.getElementById('settings-overlay').classList.add('visible');
});
document.getElementById('s-save').addEventListener('click',()=>{
const act=document.querySelector('#s-weekstart .toggle-btn.active');
S.settings.ws=act?parseInt(act.dataset.ws):1;
document.getElementById('settings-overlay').classList.remove('visible');
saveAll();refresh();renderCurrent();
});
document.getElementById('s-weekstart').addEventListener('click',e=>{
const b=e.target.closest('.toggle-btn');if(!b)return;
document.querySelectorAll('#s-weekstart .toggle-btn').forEach(x=>x.classList.remove('active'));b.classList.add('active');
});
/* ============================================================
TOAST
============================================================ */
let _toastTo=null;
function showToast(msg,action,acFn){
const c=document.getElementById('toasts');
const t=document.createElement('div');t.className='toast';
t.innerHTML=`<span>${msg}</span>`;
if(action){
const ab=document.createElement('button');ab.className='toast-action';ab.textContent=action;
if(acFn)ab.addEventListener('click',acFn);
t.appendChild(ab);
}
c.appendChild(t);
clearTimeout(_toastTo);
_toastTo=setTimeout(()=>{if(t.parentNode)t.remove()},4000);
}
/* ============================================================
INIT
============================================================ */
load();
refresh();
renderMonthHeader();
renderCurrent();
// Event listeners
document.getElementById('today-btn').addEventListener('click',goToToday);
document.getElementById('prev-btn').addEventListener('click',()=>nav(-1));
document.getElementById('next-btn').addEventListener('click',()=>nav(1));
document.querySelectorAll('.view-btn').forEach(b=>b.addEventListener('click',()=>switchView(b.dataset.view)));
// Close overlays
document.getElementById('editor-overlay').addEventListener('click',e=>{if(e.target.id==='editor-overlay')closeEditor()});
document.getElementById('recur-overlay').addEventListener('click',e=>{if(e.target.id==='recur-overlay')document.getElementById('recur-overlay').classList.remove('visible')});
document.getElementById('settings-overlay').addEventListener('click',e=>{if(e.target.id==='settings-overlay')document.getElementById('settings-overlay').classList.remove('visible')});
// Keyboard
document.addEventListener('keydown',e=>{
if(e.key==='Escape'){closeEditor();document.getElementById('recur-overlay').classList.remove('visible');document.getElementById('settings-overlay').classList.remove('visible');hideDayPicker()}
if(e.key==='Enter'&&document.getElementById('e-title').value&&document.getElementById('editor-overlay').classList.contains('visible')){e.preventDefault();saveEditor()}
});
// Drag end
document.addEventListener('dragend',()=>{
const g=document.getElementById('drag-ghost');g.style.display='none';
document.querySelectorAll('.month-cell').forEach(c=>c.style.background='');
});
// Click outside month-cell to clear hover
document.addEventListener('click',()=>{
document.querySelectorAll('.month-cell').forEach(c=>c.style.background='');
});
/* ============================================================
FIX WEEKDAY BUTTONS — re-attach
============================================================ */
document.querySelectorAll('#e-weekdays .weekday-btn').forEach(btn=>{
btn.addEventListener('click',()=>{
const d=parseInt(btn.dataset.d);
if(S._weekdays.includes(d))S._weekdays=S._weekdays.filter(x=>x!==d);
else S._weekdays.push(d);
btn.classList.toggle('selected');
});
});
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.7-flash","provider":"openrouter","persona":"full","sessionId":"ad5e2252-8639-4256-99f9-c74395dfd86a","tokensIn":3797925,"tokensOut":63166,"tokensTotal":3861091,"turns":42,"toolCalls":41,"failedToolCalls":1,"timestamp":"2026-07-30T00:48:10.699Z"} -->