DailyPal-Interactive / index.html
MonaZYX's picture
Update index.html
986b064 verified
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DailyPal Interactive Prototype</title>
<style>
:root {
--w:430px; --h:520px; --r:30px;
--stroke:#E5E5EA; --bg:#F2F2F7;
--green:#34C759; --blue:#007AFF;
--dark:#000; --sub:#8E8E93;
}
body {
background:#111;
font-family:SF Pro Display, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:10px;
min-height:100vh;
margin:0;
padding:16px;
}
.watch svg {
border-radius:var(--r);
box-shadow:0 20px 60px rgba(0,0,0,.5);
max-width:100%;
height:auto;
background:#000;
}
#bar {
color:#f5f5f7;
font-size:14px;
font-weight:500;
text-align:center;
max-width:430px;
width:100%;
padding:4px 12px 8px;
margin:0 auto 4px;
border-bottom:1px solid #2c2c2e;
}
.btnlike { cursor:pointer; }
svg .btnlike {
transition: transform 0.08s ease-out;
transform-origin: center;
}
svg .btnlike:active {
transform:scale(0.96);
}
#routine-editor {
display:none;
flex-direction:column;
gap:6px;
width:min(430px,100%);
background:#2c2c2e;
padding:8px 10px 10px;
border-radius:12px;
color:#f2f2f7;
font-size:13px;
box-shadow:0 10px 30px rgba(0,0,0,.5);
border:1px solid #3a3a3c;
}
#routine-editor .header-row {
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:2px;
}
#routine-editor .header-row span {
font-size:12px;
color:#d1d1d6;
}
#slotLabel {
font-size:13px;
font-weight:600;
color:#ffffff;
}
#routine-editor .row {
display:flex;
gap:6px;
}
#routine-editor .row > div {
flex:1;
}
#routine-editor label {
display:block;
font-size:11px;
color:#d1d1d6;
margin-bottom:2px;
}
#routine-editor input {
width:100%;
padding:6px 8px;
border-radius:8px;
border:1px solid #3a3a3c;
background:#000;
color:#f2f2f7;
font-size:13px;
box-sizing:border-box;
}
.time-picker {
display:flex;
flex-direction:column;
gap:2px;
}
.wheel-row {
display:flex;
gap:4px;
}
.wheel {
flex:1;
max-height:88px;
overflow-y:auto;
background:#000;
border-radius:10px;
border:1px solid #3a3a3c;
padding:4px 0;
scrollbar-width:none;
}
.wheel::-webkit-scrollbar {
display:none;
}
.wheel-option {
text-align:center;
padding:4px 0;
color:#8e8e93;
font-size:13px;
}
.wheel-option.selected {
color:#ffffff;
font-weight:600;
background:linear-gradient(to bottom, rgba(255,255,255,0.12), rgba(255,255,255,0.02));
}
</style>
<body>
<div id="bar">
Press 0:Home, 1:Dashboard, 2:Popup, 3:Scheduler, 4:Weekly, 5:Settings, 6:Routine
</div>
<div id="screen" class="watch"></div>
<div id="routine-editor">
<div class="header-row">
<span>Edit routine</span>
<span id="slotLabel">Morning</span>
</div>
<div class="row">
<div>
<label for="medNameInput">Medication</label>
<input id="medNameInput" type="text" placeholder="e.g., Levothyroxine">
</div>
<div class="time-picker">
<label>Time</label>
<div class="wheel-row">
<div id="hourWheel" class="wheel"></div>
<div id="minuteWheel" class="wheel"></div>
<div id="ampmWheel" class="wheel"></div>
</div>
</div>
</div>
</div>
<script>
"use strict";
const W = 430, H = 520, R = 30;
let lastReminderAction = null;
let quickLogTimeText = null;
let highlightTakenRow = false;
let selectedDayIndex = null;
let patternSource = "auto";
let meds = {
morning: { name: "Aspirin", time: "8:00 AM" },
noon: { name: "Vitamin D", time: "12:30 PM" },
evening: { name: "Fish oil", time: "9:00 PM" }
};
let currentDoseKey = "morning";
let medLastAlertDates = {
morning: null,
noon: null,
evening: null
};
const WEEK_VALUES = [0.92, 0.6, 0.84, 0.72, 0.35, 0.52, 0.64];
const DAY_SHORT = ["M", "T", "W", "T", "F", "S", "S"];
const DAY_DETAILS = [
"Monday: 3/3 doses logged — strong start to the week.",
"Tuesday: missed the evening dose; mornings look consistent.",
"Wednesday: all doses on time.",
"Thursday: 1 late-night miss; consider moving the evening dose earlier.",
"Friday: pattern similar to Tuesday — night dose is hardest.",
"Saturday: weekend routine changed; only 1 of 3 doses logged.",
"Sunday: back on track with 2/3 doses."
];
const DAY_HEADLINES = [
"Mon: 3/3 doses — strong start.",
"Tue: missed the evening dose.",
"Wed: all doses on time.",
"Thu: late-night dose was missed.",
"Fri: night dose still the hardest.",
"Sat: weekend routine broke the habit.",
"Sun: 2/3 doses — recovery."
];
const DAY_STATS = ["3/3","2/3","3/3","2/3","1/3","1/3","2/3"];
const PATTERN_DESCRIPTIONS = {
auto: "Auto pattern based on your recent weeks.",
lastWeek: "Pinned to your last week’s routine.",
olderWeeks:"Using your last 2–3 weeks instead of this week’s outliers."
};
let isPreviewPopup = false;
let audioCtx = null;
function timeStringToMinutes(text) {
if (!text) return null;
const m = text.match(/^\s*(\d{1,2}):(\d{2})\s*(AM|PM)\s*$/i);
if (!m) return null;
let h = parseInt(m[1], 10);
const min = parseInt(m[2], 10);
const ap = m[3].toUpperCase();
if (ap === "PM" && h !== 12) h += 12;
if (ap === "AM" && h === 12) h = 0;
return h * 60 + min;
}
function parseTimeComponents(text) {
const m = text && text.match(/^\s*(\d{1,2}):(\d{2})\s*(AM|PM)\s*$/i);
if (!m) return { h:8, m:0, ampm:"AM" };
return { h:parseInt(m[1],10), m:parseInt(m[2],10), ampm:m[3].toUpperCase() };
}
function initAudio() {
if (!audioCtx) {
const AC = window.AudioContext || window.webkitAudioContext;
if (!AC) return;
audioCtx = new AC();
}
if (audioCtx.state === "suspended") audioCtx.resume();
}
function playClick() {
try {
initAudio();
if (!audioCtx) return;
const ctx = audioCtx;
const oscHi = ctx.createOscillator();
const oscLo = ctx.createOscillator();
const gain = ctx.createGain();
oscHi.type = "triangle";
oscLo.type = "sine";
oscHi.frequency.setValueAtTime(1800, ctx.currentTime);
oscLo.frequency.setValueAtTime(320, ctx.currentTime);
gain.gain.setValueAtTime(0.20, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.07);
oscHi.connect(gain);
oscLo.connect(gain);
gain.connect(ctx.destination);
oscHi.start(); oscLo.start();
oscHi.stop(ctx.currentTime + 0.08);
oscLo.stop(ctx.currentTime + 0.08);
} catch(e) {}
}
const homeSVG =
`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
<defs>
<style>
.time { font:700 22px "SF Pro Display, Helvetica, Arial"; fill:#FFF; }
.label{ font:600 12px "SF Pro Display, Helvetica, Arial"; fill:#FFF; }
</style>
</defs>
<rect x="0" y="0" rx="${R}" ry="${R}" width="${W}" height="${H}" fill="#000"/>
<text class="time" x="20" y="38">9:41</text>
<g id="icon-dailypal" class="btnlike" cursor="pointer">
<circle cx="115" cy="210" r="38" fill="#34C759"/>
<rect x="100" y="195" width="30" height="24" rx="6" fill="#FFFFFF"/>
<circle cx="132" cy="205" r="5" fill="#34C759"/>
<text class="label" x="115" y="260" text-anchor="middle">DailyPal</text>
</g>
<g>
<circle cx="215" cy="180" r="28" fill="#FF3B30"/>
<circle cx="280" cy="230" r="32" fill="#FF9500"/>
<circle cx="190" cy="270" r="26" fill="#0A84FF"/>
<circle cx="260" cy="305" r="24" fill="#BF5AF2"/>
<circle cx="158" cy="330" r="22" fill="#64D2FF"/>
<circle cx="320" cy="190" r="22" fill="#FFD60A"/>
</g>
</svg>`;
function frameStart(title) {
return `
<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
<defs>
<style>
.title { font:700 22px "SF Pro Display, Helvetica, Arial"; fill:#000; }
.section { font:600 17px "SF Pro Display, Helvetica, Arial"; fill:#000; }
.body { font:500 14px "SF Pro Display, Helvetica, Arial"; fill:#000; }
.note { font:500 12px "SF Pro Display, Helvetica, Arial"; fill:#8E8E93; }
.btn { font:700 16px "SF Pro Display, Helvetica, Arial"; fill:#000; }
</style>
</defs>
<rect x="0" y="0" rx="${R}" ry="${R}" width="${W}" height="${H}" fill="#F2F2F7" stroke="#E5E5EA"/>
<text class="note" x="22" y="24">9:41</text>
<text class="title" x="22" y="44">${title}</text>
`;
}
function frameEnd() { return "</svg>"; }
function pill(x,y,w,h,label,id,fill="#FFFFFF",stroke="#E5E5EA") {
return `
<g id="${id}" class="btnlike">
<rect x="${x}" y="${y}" width="${w}" height="${h}" rx="18" fill="${fill}" stroke="${stroke}"/>
<text class="btn" x="${x + w/2}" y="${y + h/2 + 6}" text-anchor="middle">${label}</text>
</g>`;
}
function toggle(x,y,on,id) {
const fill = on ? "#34C759" : "#E9E9ED";
const knob = x + (on ? 48 : 18);
return `
<g id="${id}" class="btnlike">
<rect x="${x}" y="${y}" width="66" height="32" rx="16" fill="${fill}" stroke="#E5E5EA"/>
<circle cx="${knob}" cy="${y+16}" r="13" fill="#FFFFFF" stroke="#E5E5EA"/>
</g>`;
}
function checkbox(x,y,checked,label,id) {
const mark = checked
? `<path d="M ${x+4} ${y+12} l 6 6 l 10 -12" stroke="#007AFF" stroke-width="2" fill="none"/>`
: "";
return `
<g id="${id}" class="btnlike">
<rect x="${x}" y="${y}" width="24" height="24" rx="6" fill="#FFFFFF" stroke="#E5E5EA"/>
${mark}
<text class="body" x="${x+32}" y="${y+17}">${label}</text>
</g>`;
}
function slider(x,y,w,label,value,id) {
const minutes = timeStringToMinutes(value);
const start = 7*60;
const end = 22*60;
let pos;
if (minutes == null) pos = 0.65;
else if (minutes <= start) pos = 0;
else if (minutes >= end) pos = 1;
else pos = (minutes-start)/(end-start);
const knobX = x + w * pos;
return `
<g id="${id}" class="slider" data-x="${x}" data-w="${w}">
<text class="section" x="${x}" y="${y-12}">${label}</text>
<rect x="${x}" y="${y}" width="${w}" height="6" rx="3" fill="#E5E7EB"/>
<circle cx="${knobX}" cy="${y+3}" r="10" fill="#FFFFFF" stroke="#E5E5EA"/>
<text class="note" id="${id}-val" x="${x+w}" y="${y-12}" text-anchor="end">${value}</text>
</g>`;
}
function weeklyBars(x,y,w,h,vals) {
let out = `<g>`;
const bw = w / (vals.length * 1.6);
const gap = bw * 0.6;
for (let i=0;i<vals.length;i++) {
const v = vals[i];
const color = v>0.75 ? "#B8E6C1" : (v>0.45 ? "#FFDDA6" : "#FFB3A8");
const bx = x + i * (bw + gap);
const bh = v * h;
const by = y + (h - bh);
out += `<rect id="daybar-${i}" class="btnlike" data-index="${i}"
x="${bx}" y="${by}" width="${bw}" height="${bh}" rx="4"
fill="${color}" stroke="#E5E5EA"/>`;
}
out += `</g>`;
return out;
}
function patternSegmentControl(y) {
const width = W - 44;
const gap = 4;
const segW = (width - 2*gap) / 3;
const options = [
{ key:"auto", label:"Auto" },
{ key:"lastWeek", label:"Last wk"},
{ key:"olderWeeks",label:"2–3 wks"}
];
let x = 22;
let s = "";
options.forEach(opt=>{
const active = (patternSource === opt.key);
const fill = active ? "#007AFF" : "#FFFFFF";
const text = active ? "#FFFFFF" : "#000000";
const id = "pattern_" + opt.key;
s += `
<g id="${id}" class="btnlike">
<rect x="${x}" y="${y}" width="${segW}" height="32" rx="16" fill="${fill}" stroke="#E5E5EA"/>
<text class="body" x="${x+segW/2}" y="${y+21}" text-anchor="middle" fill="${text}">${opt.label}</text>
</g>`;
x += segW + gap;
});
return s;
}
function dashboard() {
const med = meds[currentDoseKey];
let s = frameStart(`Next dose • ${med.time}`);
s += `<text class="note" x="22" y="66">2 of 3 done today — Doing well! · ${med.name}</text>`;
function row(y,label,right,highlight) {
const fill = highlight ? "#E9F8EF" : "#FFFFFF";
return `
<g>
<rect x="18" y="${y}" width="${W-36}" height="64" rx="16" fill="${fill}" stroke="#E5E5EA"/>
<circle cx="40" cy="${y+32}" r="10" fill="#E9F8EF" stroke="#E5E5EA"/>
<path d="M 36 ${y+32} l 6 6 l 10 -12" stroke="#34C759" stroke-width="2" fill="none"/>
<text class="section" x="58" y="${y+37}">${label}</text>
<text class="note" x="${W-28}" y="${y+37}" text-anchor="end">8:00 AM</text>
</g>`;
}
s += row(82, "Taken", "8:00 AM", highlightTakenRow);
s += row(154,"Snoozed","12:00 PM",false);
s += row(226,"Skipped","6:00 PM", false);
let y = 304;
if (lastReminderAction) {
const labelMap = {
taken: "Marked as “Taken”",
snoozed: "Marked as “Snoozed”",
skipped: "Marked as “Skipped”"
};
const msg = labelMap[lastReminderAction] || "Updated";
s += `
<g id="btn_dashboard_undo" class="btnlike">
<rect x="18" y="${y}" width="${W-36}" height="44" rx="12" fill="#F2F2F7" stroke="#E5E5EA"/>
<text class="body" x="28" y="${y+26}">${msg}</text>
<text class="body" x="${W-28}" y="${y+26}" text-anchor="end" fill="#007AFF">Undo</text>
</g>`;
y += 56;
}
if (quickLogTimeText) {
s += `<text class="note" x="22" y="${y+20}">Last quick log at ${quickLogTimeText}</text>`;
y += 36;
}
s += pill(18, y, W-36, 60, "I took it", "btn_dashboard_took", "#E9F8EF");
y += 78;
s += `<text class="note" x="22" y="${y}">Weekly summary</text>`;
s += pill(18, y+10, W-36, 44, "Open Summary", "btn_dashboard_summary");
s += frameEnd();
return s;
}
function popup() {
const med = meds[currentDoseKey];
const title = isPreviewPopup ? "Preview alert" : "Time to take medication";
let s = frameStart(title);
const cy = isPreviewPopup ? 70 : 54;
if (isPreviewPopup) {
s += `<text class="note" x="22" y="60">Preview only — actions here will not change your log.</text>`;
}
s += `
<rect x="22" y="${cy}" width="${W-44}" height="96" rx="16" fill="#FFFFFF" stroke="#E5E5EA"/>
<circle cx="46" cy="${cy+48}" r="12" fill="#F2F2F5" stroke="#E5E5EA"/>
<text class="section" x="70" y="${cy+42}">${med.name}</text>
<text class="note" x="70" y="${cy+62}">1 dose at ${med.time}</text>`;
const by = cy + 108;
s += pill(22, by, 120, 52, "Snooze", "btn_popup_snooze", "#FFF7CC");
s += pill(154, by, 120, 52, "Skip", "btn_popup_skip", "#FFE9E7", "#F0C2C2");
s += pill(286, by, 120, 52, "Taken", "btn_popup_taken", "#E9F8EF", "#B9E6C6");
s += frameEnd();
return s;
}
function scheduler() {
let s = frameStart("Adaptive Scheduler");
s += slider(22, 78, W-44, "Morning", meds.morning.time, "sld_morning");
s += slider(22, 132, W-44, "Noon", meds.noon.time, "sld_noon");
s += slider(22, 186, W-44, "Evening", meds.evening.time, "sld_evening");
s += `<text class="section" x="22" y="236">Quiet Hours 10:00 PM–7:00 AM</text>`;
s += toggle(W-22-66, 216, true, "tgl_quiet");
s += `<text class="section" x="22" y="284">Auto-learn from your responses</text>`;
s += toggle(W-22-66, 264, true, "tgl_autolearn");
s += `<text class="section" x="22" y="332">Pattern window</text>`;
s += patternSegmentControl(342);
s += `<text class="note" x="22" y="392">
${PATTERN_DESCRIPTIONS[patternSource]}
</text>`;
s += pill(22, 400, 180, 56, "Save", "btn_scheduler_save", "#E9F8EF");
s += pill(W-22-180, 400, 180, 56, "Cancel","btn_scheduler_cancel");
s += `<text id="btn_scheduler_reset" class="note btnlike" x="22" y="472" fill="#007AFF">
Reset to default schedule
</text>`;
s += frameEnd();
return s;
}
function weekly() {
let s = frameStart("Weekly Summary");
s += weeklyBars(56, 70, W-112, 150, WEEK_VALUES);
const bw = (W-112) / (DAY_SHORT.length * 1.6);
const gap = bw * 0.6;
for (let i=0;i<DAY_SHORT.length;i++) {
const x = 56 + i*(bw+gap) + bw/2;
s += `<text class="note" x="${x}" y="230" text-anchor="middle">${DAY_SHORT[i]}</text>`;
}
const baseY = 252;
const summary = (selectedDayIndex == null)
? "You missed 2 doses this week — mostly at night."
: DAY_HEADLINES[selectedDayIndex];
s += `<text class="body" x="22" y="${baseY}">${summary}</text>`;
s += `<text class="note" x="22" y="${baseY+22}">
${PATTERN_DESCRIPTIONS[patternSource]}
</text>`;
s += pill(22, baseY+48, W-44, 48, "See details", "btn_weekly_details");
s += pill(22, baseY+108, W-44, 56, "Back to Dashboard", "btn_weekly_backdash");
s += frameEnd();
return s;
}
function weeklyDetails() {
let s = frameStart("Weekly Details");
s += `<text class="note" x="22" y="66">Daily breakdown for this week.</text>`;
const startY = 86;
const cardH = 40;
for (let i=0;i<7;i++) {
const v = WEEK_VALUES[i];
const color = v>0.75 ? "#E9F8EF" : (v>0.45 ? "#FFF5E5" : "#FFE9E7");
const y = startY + i*(cardH+6);
s += `
<g id="detail-day-${i}" class="btnlike">
<rect x="18" y="${y}" width="${W-36}" height="${cardH}" rx="14" fill="${color}" stroke="#E5E5EA"/>
<text class="section" x="32" y="${y+26}">${DAY_SHORT[i]}</text>
<text class="body" x="90" y="${y+22}">${DAY_STATS[i]} doses</text>
<text class="note" x="90" y="${y+36}">${DAY_DETAILS[i]}</text>
</g>`;
}
const bottomY = startY + 7*(cardH+6) + 10;
s += `<text class="note" x="22" y="${bottomY}">
Tap a day to jump back to the summary focused on that day.
</text>`;
s += pill(22, bottomY+18, W-44, 56, "Back to Summary", "btn_weekly_summary_back");
s += frameEnd();
return s;
}
function settings() {
let s = frameStart("Reminder Settings");
s += `<text class="section" x="22" y="80">Alert style</text>`;
s += checkbox(22, 92, true, "Vibration", "chk_vib");
s += checkbox(22, 128, false, "Sound", "chk_sound");
s += checkbox(22, 164, false, "Light", "chk_light");
s += `<line x1="22" y1="200" x2="${W-22}" y2="200" stroke="#E5E5EA"/>`;
s += `<text class="section" x="22" y="224">Show pop-ups</text>`;
s += toggle(W-22-66, 204, false, "tgl_popups");
s += `<text class="note" x="22" y="248">Disable sounds and pop-ups during presentations, etc.</text>`;
s += `<line x1="22" y1="272" x2="${W-22}" y2="272" stroke="#E5E5EA"/>`;
s += `<text class="section" x="22" y="296">Pattern window</text>`;
s += patternSegmentControl(306);
s += `<text class="note" x="22" y="356">
${PATTERN_DESCRIPTIONS[patternSource]}
</text>`;
s += `<line x1="22" y1="364" x2="${W-22}" y2="364" stroke="#E5E5EA"/>`;
s += `<text class="section" x="22" y="388">Preview & routine</text>`;
const halfW = (W-44-8)/2;
s += `
<g id="btn_settings_routine" class="btnlike">
<rect x="22" y="398" width="${halfW}" height="32" rx="16" fill="#FFFFFF" stroke="#E5E5EA"/>
<text class="body" x="${22+halfW/2}" y="419" text-anchor="middle">My routine</text>
</g>
<g id="btn_settings_preview" class="btnlike">
<rect x="${22+halfW+8}" y="398" width="${halfW}" height="32" rx="16" fill="#E9F8EF" stroke="#E5E5EA"/>
<text class="body" x="${22+halfW+8+halfW/2}" y="419" text-anchor="middle">Preview alert</text>
</g>`;
s += pill(22, 442, W-44, 56, "Done", "btn_settings_done");
s += frameEnd();
return s;
}
function routine() {
let s = frameStart("My routine");
s += `<text class="note" x="22" y="66">Tell DailyPal what you usually take and when.</text>`;
function row(y,key,label) {
const m = meds[key];
const line = m.name ? `${m.name} · ${m.time}` : "Tap to add a supplement";
const fill = currentDoseKey === key ? "#E9F8EF" : "#FFFFFF";
return `
<g id="edit_${key}" class="btnlike">
<rect x="18" y="${y}" width="${W-36}" height="64" rx="16" fill="${fill}" stroke="#E5E5EA"/>
<text class="section" x="32" y="${y+30}">${label}</text>
<text class="body" x="32" y="${y+48}">${line}</text>
<text class="note" x="${W-28}" y="${y+36}" text-anchor="end">Edit</text>
</g>`;
}
s += row(92, "morning","Morning");
s += row(170, "noon", "Noon");
s += row(248, "evening","Evening");
s += `<text class="note" x="22" y="326">
These names and times appear on alerts, the dashboard, and the scheduler.
</text>`;
s += pill(22, 350, W-44, 56, "Back to Dashboard", "btn_routine_back");
s += frameEnd();
return s;
}
const container = document.getElementById("screen");
const routineEditor = document.getElementById("routine-editor");
const nameInput = document.getElementById("medNameInput");
const hourWheel = document.getElementById("hourWheel");
const minuteWheel = document.getElementById("minuteWheel");
const ampmWheel = document.getElementById("ampmWheel");
function scrollToSelected(w) {
const sel = w.querySelector(".wheel-option.selected");
if (!sel) return;
const top = sel.offsetTop - w.clientHeight/2 + sel.clientHeight/2;
w.scrollTop = top;
}
function updateTimeFromWheel() {
if (!hourWheel || !minuteWheel || !ampmWheel) return;
const hEl = hourWheel.querySelector(".wheel-option.selected");
const mEl = minuteWheel.querySelector(".wheel-option.selected");
const aEl = ampmWheel.querySelector(".wheel-option.selected");
if (!hEl || !mEl || !aEl) return;
const h = parseInt(hEl.dataset.value,10);
const m = parseInt(mEl.dataset.value,10);
const ap = aEl.dataset.value;
const mm = String(m).padStart(2,"0");
meds[currentDoseKey].time = `${h}:${mm} ${ap}`;
render("routine");
}
function buildWheel(w, values, selectedVal) {
w.innerHTML = "";
values.forEach(v=>{
const d = document.createElement("div");
d.textContent = v.label;
d.dataset.value = v.value;
d.className = "wheel-option" + (v.value === selectedVal ? " selected" : "");
d.addEventListener("click", ()=>{
w.querySelectorAll(".wheel-option").forEach(o=>o.classList.remove("selected"));
d.classList.add("selected");
updateTimeFromWheel();
});
w.appendChild(d);
});
scrollToSelected(w);
}
function initTimeWheelForCurrentDose() {
if (!hourWheel || !minuteWheel || !ampmWheel) return;
const t = parseTimeComponents(meds[currentDoseKey].time);
const hours = [];
for (let i=1;i<=12;i++) hours.push({value:String(i),label:String(i)});
const minutes = [];
for (let m=0;m<60;m++) minutes.push({value:String(m),label:String(m).padStart(2,"0")});
const ap = [{value:"AM",label:"AM"},{value:"PM",label:"PM"}];
buildWheel(hourWheel,hours,String(t.h));
buildWheel(minuteWheel,minutes,String(t.m));
buildWheel(ampmWheel,ap,t.ampm);
}
function syncRoutineEditor() {
if (!routineEditor) return;
const slotLabel = document.getElementById("slotLabel");
const m = meds[currentDoseKey];
if (slotLabel) {
const k = currentDoseKey;
slotLabel.textContent = k.charAt(0).toUpperCase() + k.slice(1);
}
if (nameInput) nameInput.value = m.name || "";
initTimeWheelForCurrentDose();
}
function render(screenName) {
const map = {
home: homeSVG,
dashboard: dashboard(),
popup: popup(),
scheduler: scheduler(),
weekly: weekly(),
weeklyDetails: weeklyDetails(),
settings: settings(),
routine: routine()
};
container.innerHTML = map[screenName];
const svg = container.querySelector("svg");
if (!svg) return;
svg.style.width = W + "px";
svg.style.height = H + "px";
if (routineEditor) {
if (screenName === "routine") {
routineEditor.style.display = "flex";
syncRoutineEditor();
} else {
routineEditor.style.display = "none";
}
}
function bindClick(selector, handler) {
const el = svg.querySelector(selector);
if (el) el.addEventListener("click", handler);
}
bindClick("#icon-dailypal", ()=>render("dashboard"));
bindClick("#btn_dashboard_summary",()=>render("weekly"));
bindClick("#btn_weekly_backdash", ()=>render("dashboard"));
bindClick("#btn_weekly_details", ()=>render("weeklyDetails"));
bindClick("#btn_weekly_summary_back",()=>render("weekly"));
bindClick("#btn_settings_done", ()=>render("dashboard"));
bindClick("#btn_routine_back", ()=>render("dashboard"));
bindClick("#btn_scheduler_save", ()=>render("dashboard"));
bindClick("#btn_scheduler_cancel", ()=>render("dashboard"));
bindClick("#btn_popup_taken", ()=>{
if (isPreviewPopup) {
isPreviewPopup = false;
render("settings");
} else {
lastReminderAction = "taken";
render("dashboard");
}
});
bindClick("#btn_popup_snooze", ()=>{
if (isPreviewPopup) {
isPreviewPopup = false;
render("settings");
} else {
lastReminderAction = "snoozed";
render("dashboard");
}
});
bindClick("#btn_popup_skip", ()=>{
if (isPreviewPopup) {
isPreviewPopup = false;
render("settings");
} else {
lastReminderAction = "skipped";
render("dashboard");
}
});
bindClick("#btn_dashboard_undo", ()=>{
lastReminderAction = null;
render("dashboard");
});
bindClick("#btn_dashboard_took", ()=>{
const now = new Date();
let h = now.getHours();
let m = now.getMinutes();
const ampm = h>=12 ? "PM" : "AM";
const h12 = ((h+11)%12)+1;
const mm = String(m).padStart(2,"0");
quickLogTimeText = `${h12}:${mm} ${ampm}`;
lastReminderAction = "taken";
highlightTakenRow = true;
render("dashboard");
setTimeout(()=>{ highlightTakenRow = false; render("dashboard"); }, 260);
});
bindClick("#btn_settings_routine", ()=>render("routine"));
bindClick("#btn_settings_preview", ()=>{
isPreviewPopup = true;
render("popup");
});
bindClick("#btn_scheduler_reset", ()=>{
if (window.confirm("Reset schedule to default times?")) {
meds.morning.time = "8:00 AM";
meds.noon.time = "12:30 PM";
meds.evening.time = "9:00 PM";
render("scheduler");
}
});
for (let i=0;i<7;i++) {
bindClick(`#daybar-${i}`, ()=>{
selectedDayIndex = i;
render("weekly");
});
}
for (let i=0;i<7;i++) {
bindClick(`#detail-day-${i}`, ()=>{
selectedDayIndex = i;
render("weekly");
});
}
["auto","lastWeek","olderWeeks"].forEach(key=>{
bindClick(`#pattern_${key}`, ()=>{
patternSource = key;
if (screenName === "settings") render("settings");
else if (screenName === "scheduler") render("scheduler");
});
});
svg.querySelectorAll('g[id^="tgl_"]').forEach(t => {
t.addEventListener('click', () => {
const rect = t.querySelector('rect');
const knob = t.querySelector('circle');
const x = parseFloat(rect.getAttribute('x'));
const isOn = rect.getAttribute('fill') === '#34C759';
if (isOn) {
rect.setAttribute('fill', '#E9E9ED');
knob.setAttribute('cx', x + 18);
} else {
rect.setAttribute('fill', '#34C759');
knob.setAttribute('cx', x + 48);
}
});
});
svg.querySelectorAll(".slider").forEach(g=>{
const x = parseFloat(g.getAttribute("data-x"));
const w = parseFloat(g.getAttribute("data-w"));
const circle = g.querySelector("circle");
const label = g.querySelector("text.note[id$='-val']");
let dragging = false;
function setFromClientX(clientX) {
const pt = svg.createSVGPoint(); pt.x = clientX; pt.y = 0;
const loc = pt.matrixTransform(svg.getScreenCTM().inverse());
let nx = Math.max(x, Math.min(x+w, loc.x));
circle.setAttribute("cx", nx);
const ratio = (nx - x)/w;
const minutes = Math.round(7*60 + ratio*(15*60));
const hr = Math.floor(minutes/60);
const min = minutes % 60;
const ampm = hr>=12 ? "PM" : "AM";
const hr12 = ((hr+11)%12)+1;
const mm = String(min).padStart(2,"0");
const text = `${hr12}:${mm} ${ampm}`;
if (label) label.textContent = text;
if (g.id === "sld_morning") meds.morning.time = text;
if (g.id === "sld_noon") meds.noon.time = text;
if (g.id === "sld_evening") meds.evening.time = text;
}
circle.addEventListener("mousedown", e=>{ dragging=true; e.preventDefault(); });
svg.addEventListener("mousemove", e=>{ if (dragging) setFromClientX(e.clientX); });
window.addEventListener("mouseup", ()=>{ dragging=false; });
circle.addEventListener("touchstart", e=>{ dragging=true; e.preventDefault(); });
svg.addEventListener("touchmove", e=>{
if (dragging && e.touches && e.touches[0]) setFromClientX(e.touches[0].clientX);
}, {passive:false});
window.addEventListener("touchend", ()=>{ dragging=false; });
g.addEventListener("click", e=>{
const clientX = (e.touches && e.touches[0]) ? e.touches[0].clientX : e.clientX;
setFromClientX(clientX);
});
});
["morning","noon","evening"].forEach(key=>{
bindClick(`#edit_${key}`, ()=>{
currentDoseKey = key;
render("routine");
});
});
}
function checkRealtimeReminders() {
const now = new Date();
const minutesNow = now.getHours() * 60 + now.getMinutes();
const today = now.toISOString().slice(0,10);
const keys = ["morning","noon","evening"];
for (let i=0;i<keys.length;i++) {
const key = keys[i];
const m = meds[key];
const tMin = timeStringToMinutes(m.time);
if (tMin == null) continue;
if (tMin === minutesNow && medLastAlertDates[key] !== today) {
medLastAlertDates[key] = today;
currentDoseKey = key;
isPreviewPopup = false;
render("popup");
break;
}
}
}
render("home");
setInterval(checkRealtimeReminders, 1000);
document.addEventListener("keydown", e=>{
const ae = document.activeElement;
if (ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable)) {
return;
}
if (e.key === "0") render("home");
if (e.key === "1") render("dashboard");
if (e.key === "2") { isPreviewPopup=false; render("popup"); }
if (e.key === "3") render("scheduler");
if (e.key === "4") render("weekly");
if (e.key === "5") render("settings");
if (e.key === "6") render("routine");
});
document.addEventListener("mousedown", e=>{
const btn = e.target.closest(".btnlike");
if (btn) playClick();
});
if (nameInput) {
nameInput.addEventListener("input", e=>{
meds[currentDoseKey].name = e.target.value;
render("routine");
});
}
</script>
</body>
</html>