<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Family Calendar</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #000;
--panel: #09090b;
--panel-2: #000;
--text: #fff;
--muted: #71717a;
--line: rgba(255,255,255,.1);
--line-strong: rgba(255,255,255,.35);
--button-bg: #fff;
--button-text: #000;
--input-bg: #000;
--shadow: rgba(255,255,255,.06);
--radius-xl: 28px;
--radius-lg: 22px;
--radius-md: 16px;
}
body.light-mode {
--bg: #f7f7f5;
--panel: #ffffff;
--panel-2: #f4f4f2;
--text: #050505;
--muted: #737373;
--line: rgba(0,0,0,.1);
--line-strong: rgba(0,0,0,.35);
--button-bg: #000;
--button-text: #fff;
--input-bg: #fbfbfa;
--shadow: rgba(0,0,0,.08);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: 'Poppins', system-ui, sans-serif;
transition: background .18s ease, color .18s ease;
}
button, input, textarea, select { font: inherit; }
button { cursor: pointer; }
.font-scotch {
font-family: 'Scotch Display', 'Playfair Display', Georgia, serif;
}
.app {
width: 100%;
max-width: 1040px;
min-height: 100vh;
margin: 0 auto;
padding: 20px 16px 96px;
}
.panel-shell {
display: grid;
grid-template-columns: 1fr;
gap: 0;
align-items: start;
}
.panel[draggable="true"] {
cursor: grab;
}
.panel.dragging {
opacity: .48;
cursor: grabbing;
}
.drag-handle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 999px;
border: 1px solid var(--line);
color: var(--muted);
background: var(--panel-2);
flex: 0 0 auto;
}
body.fullscreen-panel .panel {
display: none;
}
body.fullscreen-panel .panel.active-fullscreen-panel {
display: block;
min-height: calc(100vh - 116px);
margin-bottom: 0;
}
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius-xl);
padding: 18px;
margin-bottom: 16px;
transition: background .18s ease, border-color .18s ease;
}
.hero { box-shadow: 0 20px 80px var(--shadow); }
.top-row, .section-head, .month-row, .note-title-row, .todo-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.hero-actions {
display: flex;
gap: 8px;
align-items: center;
}
.eyebrow {
margin: 0 0 4px;
font-size: 11px;
letter-spacing: .28em;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
}
h1, h2, h3, p { margin: 0; }
h1 {
font-size: 44px;
line-height: .95;
letter-spacing: -0.04em;
}
h2 {
font-size: 30px;
line-height: 1;
letter-spacing: -0.03em;
}
.subtext {
margin-top: 6px;
font-size: 13px;
color: var(--muted);
}
.icon-btn, .add-btn, .month-btn {
border: 0;
transition: transform .12s ease, opacity .12s ease, background .18s ease, color .18s ease;
}
.icon-btn:active, .add-btn:active, .month-btn:active, .person-chip:active, .day:active {
transform: scale(.96);
}
.icon-btn {
width: 44px;
height: 44px;
display: grid;
place-items: center;
border-radius: 999px;
background: var(--button-bg);
color: var(--button-text);
font-size: 17px;
flex: 0 0 auto;
}
.icon-btn svg {
width: 18px;
height: 18px;
}
.chips {
display: flex;
gap: 8px;
overflow-x: auto;
padding: 16px 0 2px;
scrollbar-width: none;
}
.chips::-webkit-scrollbar { display: none; }
.person-chip {
flex: 0 0 auto;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--panel-2);
color: var(--text);
padding: 9px 15px;
font-size: 13px;
font-weight: 600;
transition: background .18s ease, color .18s ease, border-color .18s ease;
}
.person-chip.active {
background: var(--button-bg);
color: var(--button-text);
border-color: var(--button-bg);
}
.input-row {
display: flex;
gap: 8px;
margin-top: 14px;
}
input, textarea, select {
width: 100%;
min-width: 0;
border: 1px solid var(--line);
background: var(--input-bg);
color: var(--text);
border-radius: var(--radius-md);
padding: 13px 14px;
outline: none;
font-size: 14px;
transition: background .18s ease, color .18s ease, border-color .18s ease;
}
textarea {
resize: none;
min-height: 92px;
}
input::placeholder, textarea::placeholder { color: var(--muted); opacity: .75; }
input:focus, textarea:focus, select:focus { border-color: var(--line-strong); }
.add-btn {
border-radius: var(--radius-md);
background: var(--button-bg);
color: var(--button-text);
border: 1px solid var(--button-bg);
padding: 0 16px;
min-height: 48px;
font-size: 14px;
font-weight: 700;
white-space: nowrap;
}
.month-row { margin-bottom: 18px; }
.month-btn {
border: 1px solid var(--line);
background: var(--panel-2);
color: var(--text);
padding: 9px 12px;
border-radius: 999px;
font-size: 13px;
}
.month-title { text-align: center; }
.calendar-icon { margin-bottom: 3px; color: var(--muted); font-size: 17px; }
.weekdays, .calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
}
.weekdays {
margin-bottom: 8px;
text-align: center;
font-size: 11px;
color: var(--muted);
font-weight: 700;
letter-spacing: .16em;
}
.calendar-grid { gap: 6px; }
.day, .blank-day { min-height: 56px; }
.day {
border-radius: 16px;
border: 1px solid var(--line);
background: var(--panel-2);
color: var(--text);
text-align: left;
padding: 8px;
transition: background .18s ease, color .18s ease, border-color .18s ease;
}
.day.today { border-color: var(--line-strong); }
.day.selected {
background: var(--button-bg);
color: var(--button-text);
border-color: var(--button-bg);
}
.day-number {
display: block;
font-size: 14px;
font-weight: 700;
}
.dot-row {
display: flex;
flex-wrap: wrap;
gap: 3px;
margin-top: 7px;
}
.dot {
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--text);
}
.day.selected .dot { background: var(--button-text); }
.note-list {
display: grid;
gap: 9px;
margin: 16px 0;
}
.empty {
border: 1px dashed var(--line-strong);
border-radius: var(--radius-lg);
padding: 16px;
color: var(--muted);
font-size: 14px;
}
.note-card {
border-radius: var(--radius-lg);
border: 1px solid var(--line);
padding: 15px;
}
.note-white { background: #fff; color: #000; border-color: #fff; }
.note-gray { background: #52525b; color: #fff; border-color: #52525b; }
.note-slate { background: #18181b; color: #fff; border-color: #3f3f46; }
.note-outline { background: #000; color: #fff; border-color: #fff; }
body.light-mode .note-outline { background: #fff; color: #000; border-color: #000; }
.note-person {
margin-bottom: 4px;
font-size: 11px;
letter-spacing: .14em;
text-transform: uppercase;
opacity: .65;
font-weight: 700;
}
.note-details {
margin-top: 5px;
font-size: 13px;
opacity: .78;
}
.delete-note, .delete-todo {
border: 0;
background: transparent;
color: inherit;
opacity: .65;
padding: 4px;
font-size: 16px;
}
.note-form {
background: var(--panel-2);
border: 1px solid var(--line);
border-radius: var(--radius-lg);
padding: 12px;
display: grid;
gap: 8px;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.full-btn { width: 100%; min-height: 48px; }
.todo-input-row {
display: flex;
gap: 8px;
margin: 16px 0;
}
.todo-list {
display: grid;
gap: 9px;
}
.todo-item {
display: flex;
align-items: center;
gap: 12px;
border: 1px solid var(--line);
background: var(--panel-2);
border-radius: var(--radius-lg);
padding: 12px;
transition: background .18s ease, border-color .18s ease;
}
.check-btn {
border: 0;
background: transparent;
color: var(--text);
font-size: 20px;
width: 26px;
height: 26px;
padding: 0;
}
.todo-text {
flex: 1;
min-width: 0;
font-size: 14px;
}
.todo-item.done .todo-text {
color: var(--muted);
text-decoration: line-through;
}
.focus-pill {
border: 1px solid var(--line);
border-radius: 999px;
padding: 5px 10px;
color: var(--muted);
font-size: 12px;
display: none;
}
body.focus-mode .calendar-section,
body.focus-mode .notes-section { display: none; }
body.focus-mode .focus-pill { display: inline-block; }
.bottom-nav {
position: fixed;
left: 50%;
bottom: 14px;
transform: translateX(-50%);
z-index: 50;
width: min(520px, calc(100% - 24px));
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
padding: 8px;
border: 1px solid var(--line);
border-radius: 999px;
background: color-mix(in srgb, var(--panel) 92%, transparent);
box-shadow: 0 18px 60px var(--shadow);
backdrop-filter: blur(18px);
}
.nav-btn {
display: grid;
place-items: center;
gap: 2px;
min-height: 48px;
border: 0;
border-radius: 999px;
background: transparent;
color: var(--muted);
font-size: 10px;
font-weight: 700;
}
.nav-btn svg {
width: 17px;
height: 17px;
}
.nav-btn.active {
background: var(--button-bg);
color: var(--button-text);
}
@media (min-width: 768px) {
.app {
padding: 32px 24px 104px;
}
.panel-shell {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.panel {
margin-bottom: 0;
}
.hero {
grid-column: 1 / -1;
}
.calendar-section,
.notes-section,
.todo-section {
min-width: 0;
}
body.fullscreen-panel .panel.active-fullscreen-panel {
grid-column: 1 / -1;
}
}
@media (min-width: 640px) {
.app { padding-top: 32px; }
}
</style>
</head>
<body>
<main class="app">
<div class="panel-shell" id="panelShell">
<section class="panel hero" data-panel="home" draggable="true">
<div class="top-row">
<div>
<p class="eyebrow">Family planner</p>
<h1 class="font-scotch">Calendar</h1>
<p class="subtext" id="activeScheduleText">Viewing all schedules</p>
</div>
<div class="hero-actions">
<span class="drag-handle" aria-label="Drag panel">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M8 6h.01M16 6h.01M8 12h.01M16 12h.01M8 18h.01M16 18h.01"></path>
</svg>
</span>
<button class="icon-btn" id="themeToggleBtn" aria-label="Toggle theme"></button>
<button class="icon-btn" id="toggleCalendarBtn" aria-label="Hide calendar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 5h18"></path>
<path d="M7 3v4"></path>
<path d="M17 3v4"></path>
<rect x="3" y="5" width="18" height="16" rx="2"></rect>
<path d="M8 11h.01"></path>
<path d="M12 11h.01"></path>
<path d="M16 11h.01"></path>
<path d="M8 15h.01"></path>
<path d="M12 15h.01"></path>
</svg>
</button>
</div>
</div>
<div class="chips" id="peopleChips"></div>
<div class="input-row">
<input id="personInput" type="text" placeholder="Add person or pet" />
<button class="add-btn" id="addPersonBtn">Add</button>
</div>
</section>
<section class="panel calendar-section" data-panel="calendar" draggable="true">
<div class="month-row">
<button class="month-btn" id="prevMonthBtn" aria-label="Previous month">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M15 18l-6-6 6-6"></path>
</svg>
</button>
<div class="month-title">
<div class="calendar-icon">▦</div>
<h2 class="font-scotch" id="monthTitle"></h2>
</div>
<button class="month-btn" id="nextMonthBtn" aria-label="Next month">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M9 18l6-6-6-6"></path>
</svg>
</button>
</div>
<div class="weekdays">
<div>S</div><div>M</div><div>T</div><div>W</div><div>T</div><div>F</div><div>S</div>
</div>
<div class="calendar-grid" id="calendarGrid"></div>
</section>
<section class="panel notes-section" data-panel="notes" draggable="true">
<div>
<h2 class="font-scotch">Notes</h2>
<p class="subtext" id="selectedDateLabel"></p>
</div>
<div class="note-list" id="noteList"></div>
<div class="note-form">
<input id="noteTitleInput" type="text" placeholder="Note title" />
<textarea id="noteDetailsInput" placeholder="Details"></textarea>
<div class="form-grid">
<select id="notePersonSelect"></select>
<select id="noteColorSelect">
<option value="white">White</option>
<option value="gray">Gray</option>
<option value="slate">Slate</option>
<option value="outline">Outline</option>
</select>
</div>
<button class="add-btn full-btn" id="addNoteBtn">+ Add note</button>
</div>
</section>
<section class="panel todo-section" data-panel="todo" draggable="true">
<div class="section-head">
<div>
<h2 class="font-scotch">To-do list</h2>
<p class="subtext">Use calendar toggle to focus on tasks only.</p>
</div>
<span class="focus-pill">Focus mode</span>
</div>
<div class="todo-input-row">
<input id="todoInput" type="text" placeholder="Add a task" />
<button class="add-btn" id="addTodoBtn">+</button>
</div>
<div class="todo-list" id="todoList"></div>
</section>
</div>
<nav class="bottom-nav" aria-label="Panel navigation">
<button class="nav-btn active" data-target-panel="all" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="3" width="7" height="7" rx="1"></rect>
<rect x="14" y="3" width="7" height="7" rx="1"></rect>
<rect x="3" y="14" width="7" height="7" rx="1"></rect>
<rect x="14" y="14" width="7" height="7" rx="1"></rect>
</svg>
All
</button>
<button class="nav-btn" data-target-panel="calendar" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="3" y="5" width="18" height="16" rx="2"></rect>
<path d="M16 3v4M8 3v4M3 10h18"></path>
</svg>
Calendar
</button>
<button class="nav-btn" data-target-panel="notes" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M4 4h16v16H4z"></path>
<path d="M8 8h8M8 12h8M8 16h5"></path>
</svg>
Notes
</button>
<button class="nav-btn" data-target-panel="todo" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M9 11l3 3L22 4"></path>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</svg>
To-do
</button>
</nav>
</main>
<script>
const state = {
people: [
{ id: 'leia', name: 'Leia' },
{ id: 'lucas', name: 'Lucas' },
{ id: 'mom', name: 'Mom' },
{ id: 'dad', name: 'Dad' },
{ id: 'hiro', name: 'Hiro' }
],
activePersonId: 'all',
currentMonth: new Date(),
selectedDate: toDateKey(new Date()),
showCalendar: true,
theme: 'light',
notes: [
{
id: uid(),
date: toDateKey(new Date()),
personId: 'leia',
title: 'School pickup',
details: 'Bring extra snack and water bottle.',
color: 'white'
},
{
id: uid(),
date: toDateKey(new Date()),
personId: 'hiro',
title: 'Walk Hiro',
details: 'Evening walk after dinner.',
color: 'outline'
}
],
todos: [
{ id: uid(), text: "Check tomorrow's schedule", done: false },
{ id: uid(), text: "Pack Leia's bag", done: true }
]
};
const peopleChips = document.getElementById('peopleChips');
const activeScheduleText = document.getElementById('activeScheduleText');
const personInput = document.getElementById('personInput');
const addPersonBtn = document.getElementById('addPersonBtn');
const toggleCalendarBtn = document.getElementById('toggleCalendarBtn');
const themeToggleBtn = document.getElementById('themeToggleBtn');
const prevMonthBtn = document.getElementById('prevMonthBtn');
const nextMonthBtn = document.getElementById('nextMonthBtn');
const monthTitle = document.getElementById('monthTitle');
const calendarGrid = document.getElementById('calendarGrid');
const selectedDateLabel = document.getElementById('selectedDateLabel');
const noteList = document.getElementById('noteList');
const noteTitleInput = document.getElementById('noteTitleInput');
const noteDetailsInput = document.getElementById('noteDetailsInput');
const notePersonSelect = document.getElementById('notePersonSelect');
const noteColorSelect = document.getElementById('noteColorSelect');
const addNoteBtn = document.getElementById('addNoteBtn');
const todoInput = document.getElementById('todoInput');
const addTodoBtn = document.getElementById('addTodoBtn');
const todoList = document.getElementById('todoList');
const panelShell = document.getElementById('panelShell');
const navButtons = document.querySelectorAll('.nav-btn');
const SUN_ICON = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="4"></circle>
<path d="M12 2v2"></path>
<path d="M12 20v2"></path>
<path d="M4.93 4.93l1.41 1.41"></path>
<path d="M17.66 17.66l1.41 1.41"></path>
<path d="M2 12h2"></path>
<path d="M20 12h2"></path>
<path d="M4.93 19.07l1.41-1.41"></path>
<path d="M17.66 6.34l1.41-1.41"></path>
</svg>
`;
const MOON_ICON = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
`;
const CALENDAR_ICON = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 5h18"></path>
<path d="M7 3v4"></path>
<path d="M17 3v4"></path>
<rect x="3" y="5" width="18" height="16" rx="2"></rect>
<path d="M8 11h.01"></path>
<path d="M12 11h.01"></path>
<path d="M16 11h.01"></path>
<path d="M8 15h.01"></path>
<path d="M12 15h.01"></path>
</svg>
`;
const CALENDAR_OFF_ICON = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 5h18"></path>
<path d="M7 3v4"></path>
<path d="M17 3v4"></path>
<rect x="3" y="5" width="18" height="16" rx="2"></rect>
<path d="M3 3l18 18"></path>
</svg>
`;
addPersonBtn.addEventListener('click', addPerson);
personInput.addEventListener('keydown', event => {
if (event.key === 'Enter') addPerson();
});
toggleCalendarBtn.addEventListener('click', toggleCalendar);
themeToggleBtn.addEventListener('click', toggleTheme);
prevMonthBtn.addEventListener('click', () => changeMonth(-1));
nextMonthBtn.addEventListener('click', () => changeMonth(1));
addNoteBtn.addEventListener('click', addNote);
addTodoBtn.addEventListener('click', addTodo);
todoInput.addEventListener('keydown', event => {
if (event.key === 'Enter') addTodo();
});
navButtons.forEach(button => {
button.addEventListener('click', () => setActivePanel(button.dataset.targetPanel));
});
let draggedPanel = null;
panelShell.addEventListener('dragstart', event => {
const panel = event.target.closest('.panel[draggable="true"]');
if (!panel) return;
draggedPanel = panel;
panel.classList.add('dragging');
event.dataTransfer.effectAllowed = 'move';
});
panelShell.addEventListener('dragend', () => {
if (draggedPanel) draggedPanel.classList.remove('dragging');
draggedPanel = null;
});
panelShell.addEventListener('dragover', event => {
event.preventDefault();
const afterElement = getDragAfterElement(panelShell, event.clientY);
if (!draggedPanel) return;
if (afterElement == null) {
panelShell.appendChild(draggedPanel);
} else {
panelShell.insertBefore(draggedPanel, afterElement);
}
});
function setActivePanel(panelName) {
document.body.classList.toggle('fullscreen-panel', panelName !== 'all');
document.querySelectorAll('.panel').forEach(panel => {
panel.classList.toggle('active-fullscreen-panel', panel.dataset.panel === panelName);
});
navButtons.forEach(button => {
button.classList.toggle('active', button.dataset.targetPanel === panelName);
});
if (panelName !== 'all') {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}
function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.panel[draggable="true"]:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset, element: child };
}
return closest;
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
function uid() {
return Date.now().toString(36) + Math.random().toString(36).slice(2);
}
function toDateKey(date) {
return date.toISOString().slice(0, 10);
}
function parseDateKey(key) {
return new Date(key + 'T00:00:00');
}
function getVisibleNotes() {
return state.notes.filter(note => state.activePersonId === 'all' || note.personId === state.activePersonId);
}
function getPersonName(id) {
return state.people.find(person => person.id === id)?.name || 'Schedule';
}
function addPerson() {
const name = personInput.value.trim();
if (!name) return;
const id = name.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '-' + Date.now();
state.people.push({ id, name });
state.activePersonId = id;
personInput.value = '';
render();
}
function toggleCalendar() {
state.showCalendar = !state.showCalendar;
document.body.classList.toggle('focus-mode', !state.showCalendar);
toggleCalendarBtn.innerHTML = state.showCalendar ? CALENDAR_ICON : CALENDAR_OFF_ICON;
toggleCalendarBtn.setAttribute('aria-label', state.showCalendar ? 'Hide calendar' : 'Show calendar');
if (!state.showCalendar) {
setActivePanel('todo');
} else {
setActivePanel('all');
}
}
function toggleTheme() {
state.theme = state.theme === 'dark' ? 'light' : 'dark';
document.body.classList.toggle('light-mode', state.theme === 'light');
themeToggleBtn.innerHTML = state.theme === 'light' ? SUN_ICON : MOON_ICON;
themeToggleBtn.setAttribute(
'aria-label',
state.theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'
);
}
function changeMonth(direction) {
state.currentMonth = new Date(
state.currentMonth.getFullYear(),
state.currentMonth.getMonth() + direction,
1
);
renderCalendar();
}
function addNote() {
const title = noteTitleInput.value.trim();
const details = noteDetailsInput.value.trim();
if (!title) return;
state.notes.push({
id: uid(),
date: state.selectedDate,
personId: notePersonSelect.value,
title,
details,
color: noteColorSelect.value
});
noteTitleInput.value = '';
noteDetailsInput.value = '';
renderCalendar();
renderNotes();
}
function deleteNote(id) {
state.notes = state.notes.filter(note => note.id !== id);
renderCalendar();
renderNotes();
}
function addTodo() {
const text = todoInput.value.trim();
if (!text) return;
state.todos.unshift({ id: uid(), text, done: false });
todoInput.value = '';
renderTodos();
}
function toggleTodo(id) {
state.todos = state.todos.map(todo => todo.id === id ? { ...todo, done: !todo.done } : todo);
renderTodos();
}
function deleteTodo(id) {
state.todos = state.todos.filter(todo => todo.id !== id);
renderTodos();
}
function renderPeople() {
peopleChips.innerHTML = '';
const allButton = document.createElement('button');
allButton.className = 'person-chip' + (state.activePersonId === 'all' ? ' active' : '');
allButton.textContent = 'All';
allButton.addEventListener('click', () => {
state.activePersonId = 'all';
render();
});
peopleChips.appendChild(allButton);
state.people.forEach(person => {
const button = document.createElement('button');
button.className = 'person-chip' + (state.activePersonId === person.id ? ' active' : '');
button.textContent = person.name;
button.addEventListener('click', () => {
state.activePersonId = person.id;
render();
});
peopleChips.appendChild(button);
});
activeScheduleText.textContent = state.activePersonId === 'all'
? 'Viewing all schedules'
: 'Viewing ' + getPersonName(state.activePersonId) + "'s schedule";
}
function renderPersonSelect() {
notePersonSelect.innerHTML = '';
state.people.forEach(person => {
const option = document.createElement('option');
option.value = person.id;
option.textContent = person.name;
notePersonSelect.appendChild(option);
});
if (state.activePersonId !== 'all') {
notePersonSelect.value = state.activePersonId;
}
}
function renderCalendar() {
const year = state.currentMonth.getFullYear();
const month = state.currentMonth.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const startOffset = firstDay.getDay();
const visibleNotes = getVisibleNotes();
const todayKey = toDateKey(new Date());
monthTitle.textContent = state.currentMonth.toLocaleDateString(undefined, {
month: 'long',
year: 'numeric'
});
calendarGrid.innerHTML = '';
for (let i = 0; i < startOffset; i++) {
const blank = document.createElement('div');
blank.className = 'blank-day';
calendarGrid.appendChild(blank);
}
for (let day = 1; day <= lastDay.getDate(); day++) {
const date = new Date(year, month, day);
const dateKey = toDateKey(date);
const dayNotes = visibleNotes.filter(note => note.date === dateKey);
const button = document.createElement('button');
button.className = 'day';
if (dateKey === todayKey) button.classList.add('today');
if (dateKey === state.selectedDate) button.classList.add('selected');
button.innerHTML = `
<span class="day-number">${day}</span>
<span class="dot-row">${dayNotes.slice(0, 3).map(() => '<span class="dot"></span>').join('')}</span>
`;
button.addEventListener('click', () => {
state.selectedDate = dateKey;
renderCalendar();
renderNotes();
});
calendarGrid.appendChild(button);
}
}
function renderNotes() {
const visibleNotes = getVisibleNotes().filter(note => note.date === state.selectedDate);
selectedDateLabel.textContent = parseDateKey(state.selectedDate).toLocaleDateString(undefined, {
weekday: 'long',
month: 'long',
day: 'numeric'
});
noteList.innerHTML = '';
if (visibleNotes.length === 0) {
const empty = document.createElement('div');
empty.className = 'empty';
empty.textContent = 'No notes for this date yet.';
noteList.appendChild(empty);
return;
}
visibleNotes.forEach(note => {
const article = document.createElement('article');
article.className = 'note-card note-' + note.color;
article.innerHTML = `
<div class="note-title-row">
<div>
<p class="note-person">${escapeHtml(getPersonName(note.personId))}</p>
<h3>${escapeHtml(note.title)}</h3>
${note.details ? `<p class="note-details">${escapeHtml(note.details)}</p>` : ''}
</div>
<button class="delete-note" aria-label="Delete note">×</button>
</div>
`;
article.querySelector('.delete-note').addEventListener('click', () => deleteNote(note.id));
noteList.appendChild(article);
});
}
function renderTodos() {
todoList.innerHTML = '';
state.todos.forEach(todo => {
const item = document.createElement('div');
item.className = 'todo-item' + (todo.done ? ' done' : '');
item.innerHTML = `
<button class="check-btn" aria-label="Toggle task">${todo.done ? '✓' : '○'}</button>
<span class="todo-text">${escapeHtml(todo.text)}</span>
<button class="delete-todo" aria-label="Delete task">🗑</button>
`;
item.querySelector('.check-btn').addEventListener('click', () => toggleTodo(todo.id));
item.querySelector('.delete-todo').addEventListener('click', () => deleteTodo(todo.id));
todoList.appendChild(item);
});
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
function render() {
renderPeople();
renderPersonSelect();
renderCalendar();
renderNotes();
renderTodos();
}
document.body.classList.add('light-mode');
themeToggleBtn.innerHTML = SUN_ICON;
themeToggleBtn.setAttribute('aria-label', 'Switch to dark mode');
toggleCalendarBtn.innerHTML = CALENDAR_ICON;
setActivePanel('all');
render();
</script>
</body>
</html>