Spaces:
Running
Running
| <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> | |
| ; | |
| 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> | |