vky3831

MediCycle Zip Create

Nov 14th, 2025
131
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 27.77 KB | Source Code | 0 0
  1. # Creating project files and zipping them for download.
  2. from pathlib import Path, PurePosixPath
  3. import json, zipfile, os, textwrap
  4.  
  5. OUT_DIR = Path('/mnt/data/medicycle_app')
  6. OUT_DIR.mkdir(parents=True, exist_ok=True)
  7.  
  8. # index.html
  9. index_html = """<!doctype html>
  10. <html lang="en">
  11. <head>
  12.  <meta charset="utf-8" />
  13.  <meta name="viewport" content="width=device-width, initial-scale=1" />
  14.  <title>MediCycle — Medicine Reminder</title>
  15.  <!-- Bootstrap 5 -->
  16.  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
  17.  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
  18.  <link href="css/style.css" rel="stylesheet">
  19. </head>
  20. <body class="app-shell">
  21.  <nav class="navbar navbar-expand-lg navbar-light bg-light px-3">
  22.    <div class="container-fluid">
  23.      <span class="navbar-brand logo"><i class="fa-solid fa-pills me-2"></i>MediCycle</span>
  24.      <div class="d-flex gap-2 align-items-center">
  25.        <button id="toggleTheme" class="btn btn-outline-secondary btn-sm" title="Toggle theme"><i class="fa-solid fa-moon"></i></button>
  26.        <button id="exportBtn" class="btn btn-outline-primary btn-sm">Export</button>
  27.        <label class="btn btn-outline-success btn-sm mb-0">
  28.          Import <input id="importFile" type="file" accept="application/json" hidden>
  29.        </label>
  30.      </div>
  31.    </div>
  32.  </nav>
  33.  
  34.  <main class="container py-4">
  35.    <!-- Start: Welcome / Profile selection / Create profile -->
  36.    <div id="screen-profile" class="card p-4 mx-auto" style="max-width:820px; display:none">
  37.      <div id="no-data-screen">
  38.        <h4>Welcome to MediCycle</h4>
  39.        <p class="mb-3">No profiles found. Create a profile to start adding medicine routines.</p>
  40.        <button id="createProfileBtn" class="btn btn-primary">Create profile</button>
  41.      </div>
  42.  
  43.      <div id="select-profile-screen" style="display:none">
  44.        <h5>Select profile</h5>
  45.        <div id="profilesList" class="mb-3"></div>
  46.        <button id="addProfileBtn" class="btn btn-outline-primary">Add new profile</button>
  47.      </div>
  48.    </div>
  49.  
  50.    <!-- App Dashboard -->
  51.    <div id="screen-app" style="display:none">
  52.      <div class="d-flex align-items-center justify-content-between mb-3">
  53.        <div>
  54.          <h4 id="profileTitle">Profile</h4>
  55.          <div class="small-muted">Age: <span id="profileAge"></span> &nbsp;•&nbsp; ID: <span id="profileId"></span></div>
  56.        </div>
  57.        <div class="text-end">
  58.          <small class="d-block">Made by © VKY Soft</small>
  59.          <button id="switchProfileBtn" class="btn btn-link btn-sm">Switch Profile</button>
  60.          <button id="deleteProfileBtn" class="btn btn-link btn-sm text-danger">Delete Profile</button>
  61.        </div>
  62.      </div>
  63.  
  64.      <div class="card mb-3 p-3">
  65.        <div class="d-flex align-items-center justify-content-between">
  66.          <h5 class="mb-0">Dashboard</h5>
  67.          <div>
  68.            <button id="addMedicineBtn" class="btn btn-success btn-sm"><i class="fa-solid fa-plus"></i> Add Medicine</button>
  69.          </div>
  70.        </div>
  71.      </div>
  72.  
  73.      <div class="card p-2 mb-3">
  74.        <ul class="nav nav-tabs" id="mainTabs">
  75.          <li class="nav-item"><a class="nav-link active" data-tab="all">All</a></li>
  76.          <li class="nav-item"><a class="nav-link" data-tab="today">Today</a></li>
  77.          <li class="nav-item"><a class="nav-link" data-tab="history">History</a></li>
  78.        </ul>
  79.  
  80.        <div id="tabContent" class="p-3">
  81.          <div id="tab-all" class="tab-pane active">
  82.            <div id="medListAll"></div>
  83.          </div>
  84.          <div id="tab-today" class="tab-pane" style="display:none">
  85.            <div id="medListToday"></div>
  86.          </div>
  87.          <div id="tab-history" class="tab-pane" style="display:none">
  88.            <div id="historyList"></div>
  89.          </div>
  90.        </div>
  91.      </div>
  92.    </div>
  93.  
  94.    <!-- Add/Edit Medicine Modal (simple in-page screen) -->
  95.    <div id="screen-add" class="card p-3 mx-auto" style="max-width:720px; display:none">
  96.      <h5 id="addTitle">Add Medicine</h5>
  97.      <form id="medForm">
  98.        <div class="mb-2">
  99.          <label class="form-label">Medicine name</label>
  100.          <input id="medName" class="form-control" required />
  101.        </div>
  102.        <div class="mb-2">
  103.          <label class="form-label">Dosage (e.g. 2 tablets)</label>
  104.          <input id="medDosage" class="form-control" required />
  105.        </div>
  106.        <div class="mb-2">
  107.          <label class="form-label d-block">Food</label>
  108.          <div class="form-check form-check-inline"><input class="form-check-input" type="radio" name="food" id="afterFood" value="after" checked><label class="form-check-label" for="afterFood">After Food</label></div>
  109.          <div class="form-check form-check-inline"><input class="form-check-input" type="radio" name="food" id="beforeFood" value="before"><label class="form-check-label" for="beforeFood">Before Food</label></div>
  110.        </div>
  111.        <div class="mb-2 row">
  112.          <div class="col-sm-6">
  113.            <label class="form-label">Time</label>
  114.            <input id="medTime" type="time" class="form-control" required>
  115.          </div>
  116.          <div class="col-sm-6">
  117.            <label class="form-label">Cycle</label>
  118.            <select id="medCycle" class="form-select">
  119.              <option value="daily">Daily</option>
  120.              <option value="monthly">Specific date of month</option>
  121.              <option value="weekly">Specific day of week</option>
  122.            </select>
  123.          </div>
  124.        </div>
  125.  
  126.        <div id="cycleDetails" class="mb-2"></div>
  127.  
  128.        <div class="d-flex gap-2">
  129.          <button class="btn btn-primary" id="saveMedBtn">Save</button>
  130.          <button class="btn btn-secondary" id="cancelMedBtn" type="button">Cancel</button>
  131.        </div>
  132.      </form>
  133.    </div>
  134.  
  135.  </main>
  136.  
  137.  <footer class="border-top py-2 text-center small-muted">
  138.    © VKY Soft — MediCycle • Local-only (no backend) • Works on mobile & PC
  139.  </footer>
  140.  
  141.  <script src="js/app.js"></script>
  142.  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
  143. </body>
  144. </html>
  145. """
  146.  
  147. # css/style.css
  148. css = """:root { --bg: #ffffff; --text: #111827; --card: #f8fafc; }
  149. .dark { --bg: #0b1220; --text: #e6eef8; --card: #0f1724; }
  150. body { background: var(--bg); color: var(--text); }
  151. .card { background: var(--card); }
  152. .app-shell { min-height: 100vh; display:flex; flex-direction:column; }
  153. .logo { font-weight:700; letter-spacing:0.6px; }
  154. .small-muted { font-size:0.85rem; color:rgba(0,0,0,0.55); }
  155. .time-pill { font-weight:600; }
  156. .cursor-pointer { cursor:pointer; }
  157. @media (max-width:576px){ .desktop-only{ display:none } }
  158.  
  159. /* Dark-mode tweaks */
  160. :root.dark .small-muted { color: rgba(255,255,255,0.6); }
  161. """
  162.  
  163. # js/app.js - improved from user's code with passkey check and delete profile
  164. js = r"""// ---------- Data model and localStorage helpers ----------
  165. const STORAGE_KEY = 'medicycle_data_v1';
  166.  
  167. function defaultData(){
  168.  return {
  169.    profiles: [], // {id, name, age, passkey, medicines: [med objects]}
  170.    history: [], // {profileId, medId, medName, timeTakenISO, dosage}
  171.    currentProfileId: null
  172.  };
  173. }
  174.  
  175. function loadData(){
  176.  try{
  177.    const raw = localStorage.getItem(STORAGE_KEY);
  178.    if(!raw) return defaultData();
  179.    return JSON.parse(raw);
  180.  }catch(e){ console.error('loadData', e); return defaultData(); }
  181. }
  182. function saveData(data){ localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); }
  183.  
  184. // Unique id helper
  185. function uid(prefix='id'){ return prefix + '_' + Math.random().toString(36).slice(2,9); }
  186.  
  187. // ---------- DOM refs ----------
  188. const screenProfile = document.getElementById('screen-profile');
  189. const screenApp = document.getElementById('screen-app');
  190. const screenAdd = document.getElementById('screen-add');
  191.  
  192. const createProfileBtn = document.getElementById('createProfileBtn');
  193. const addProfileBtn = document.getElementById('addProfileBtn');
  194. const profilesList = document.getElementById('profilesList');
  195.  
  196. const profileTitle = document.getElementById('profileTitle');
  197. const profileAgeEl = document.getElementById('profileAge');
  198. const profileIdEl = document.getElementById('profileId');
  199. const switchProfileBtn = document.getElementById('switchProfileBtn');
  200. const deleteProfileBtn = document.getElementById('deleteProfileBtn');
  201.  
  202. const addMedicineBtn = document.getElementById('addMedicineBtn');
  203. const medListAll = document.getElementById('medListAll');
  204. const medListToday = document.getElementById('medListToday');
  205. const historyList = document.getElementById('historyList');
  206.  
  207. const medForm = document.getElementById('medForm');
  208. const medName = document.getElementById('medName');
  209. const medDosage = document.getElementById('medDosage');
  210. const medTime = document.getElementById('medTime');
  211. const medCycle = document.getElementById('medCycle');
  212. const cycleDetails = document.getElementById('cycleDetails');
  213. const saveMedBtn = document.getElementById('saveMedBtn');
  214. const cancelMedBtn = document.getElementById('cancelMedBtn');
  215. const addTitle = document.getElementById('addTitle');
  216.  
  217. const toggleTheme = document.getElementById('toggleTheme');
  218. const exportBtn = document.getElementById('exportBtn');
  219. const importFile = document.getElementById('importFile');
  220.  
  221. // Tabs
  222. document.querySelectorAll('#mainTabs .nav-link').forEach(a => {
  223.  a.addEventListener('click', ()=>{
  224.    document.querySelectorAll('#mainTabs .nav-link').forEach(x=>x.classList.remove('active'));
  225.    a.classList.add('active');
  226.    document.querySelectorAll('.tab-pane').forEach(p=>p.style.display='none');
  227.    document.getElementById('tab-'+a.dataset.tab).style.display='block';
  228.    if(a.dataset.tab === 'history') renderHistory();
  229.    if(a.dataset.tab === 'today') renderToday();
  230.    if(a.dataset.tab === 'all') renderAll();
  231.  });
  232. });
  233.  
  234. // ---------- Profile creation / selection UI ----------
  235. function showCreateProfileForm(){
  236.  // Use simple built-in prompts for quick UX
  237.  const name = prompt('Name:'); if(!name) return;
  238.  const age = prompt('Age:'); if(!age) return;
  239.  const passkey = prompt('Passkey (used to protect profile):'); if(!passkey) return;
  240.  
  241.  const data = loadData();
  242.  const id = uid('profile');
  243.  data.profiles.push({ id, name, age, passkey, medicines: [] });
  244.  data.currentProfileId = id;
  245.  saveData(data);
  246.  initApp();
  247. }
  248.  
  249. function renderProfiles(){
  250.  const data = loadData();
  251.  profilesList.innerHTML = '';
  252.  if(data.profiles.length===0){
  253.    document.getElementById('no-data-screen').style.display='block';
  254.    document.getElementById('select-profile-screen').style.display='none';
  255.  } else {
  256.    document.getElementById('no-data-screen').style.display='none';
  257.    document.getElementById('select-profile-screen').style.display='block';
  258.    data.profiles.forEach(p=>{
  259.      const div = document.createElement('div');
  260.      div.className = 'd-flex align-items-center justify-content-between mb-2 card p-2';
  261.      div.innerHTML = `<div><strong>${escapeHtml(p.name)}</strong><div class='small-muted'>Age ${p.age}</div></div>
  262.        <div>
  263.          <button class='btn btn-sm btn-primary select-profile' data-id='${p.id}'>Open</button>
  264.          <button class='btn btn-sm btn-outline-danger ms-1 delete-profile' data-id='${p.id}'>Delete</button>
  265.        </div>`;
  266.      profilesList.appendChild(div);
  267.    });
  268.    document.querySelectorAll('.select-profile').forEach(b=> b.addEventListener('click', async ()=>{
  269.      const id = b.dataset.id; const data = loadData(); const profile = data.profiles.find(x=>x.id===id);
  270.      if(!profile) return alert('Profile not found');
  271.      // ask passkey
  272.      const pass = prompt('Enter passkey for profile "'+profile.name+'":');
  273.      if(pass === null) return;
  274.      if(pass === profile.passkey){
  275.        data.currentProfileId = id; saveData(data); initApp();
  276.      } else {
  277.        alert('Wrong passkey');
  278.      }
  279.    }));
  280.    document.querySelectorAll('.delete-profile').forEach(b=> b.addEventListener('click', (e)=> {
  281.      const id = b.dataset.id; const data = loadData(); const profile = data.profiles.find(x=>x.id===id);
  282.      if(!profile) return;
  283.      if(confirm('Delete profile "'+profile.name+'"? This will remove all medicines and history for this profile.')) {
  284.        // remove profile and related history
  285.        data.profiles = data.profiles.filter(p=> p.id !== id);
  286.        data.history = data.history.filter(h=> h.profileId !== id);
  287.        if(data.currentProfileId === id) data.currentProfileId = null;
  288.        saveData(data); initApp();
  289.      }
  290.    }));
  291.  }
  292. }
  293.  
  294. // ---------- App init / rendering ----------
  295. let data = loadData();
  296. let currentProfile = null;
  297. let editingMedId = null;
  298.  
  299. function initApp(){
  300.  data = loadData();
  301.  if(!data || !data.profiles || data.profiles.length===0){
  302.    // show create profile screen
  303.    screenProfile.style.display='block';
  304.    screenApp.style.display='none';
  305.    screenAdd.style.display='none';
  306.    renderProfiles();
  307.    return;
  308.  }
  309.  
  310.  // if a currentProfileId exists, require passkey before opening automatically
  311.  if(data.currentProfileId){
  312.    const profile = data.profiles.find(p=> p.id === data.currentProfileId);
  313.    if(profile){
  314.      const pass = prompt('Enter passkey to open profile "'+profile.name+'": (cancel to choose another profile)');
  315.      if(pass === profile.passkey){
  316.        currentProfile = profile;
  317.        openAppForProfile();
  318.        return;
  319.      } else {
  320.        // wrong or cancelled -> clear currentProfileId and show selector
  321.        data.currentProfileId = null; saveData(data);
  322.        screenProfile.style.display='block'; screenApp.style.display='none'; screenAdd.style.display='none';
  323.        renderProfiles();
  324.        return;
  325.      }
  326.    } else {
  327.      data.currentProfileId = null; saveData(data);
  328.      initApp(); return;
  329.    }
  330.  } else {
  331.    // no current profile set -> show selector
  332.    screenProfile.style.display='block'; screenApp.style.display='none'; screenAdd.style.display='none';
  333.    renderProfiles();
  334.    return;
  335.  }
  336. }
  337.  
  338. function openAppForProfile(){
  339.  // show dashboard
  340.  screenProfile.style.display='none'; screenApp.style.display='block'; screenAdd.style.display='none';
  341.  profileTitle.textContent = currentProfile.name;
  342.  profileAgeEl.textContent = currentProfile.age;
  343.  profileIdEl.textContent = currentProfile.id;
  344.  renderAll(); renderToday();
  345. }
  346.  
  347. // ---------- Render lists ----------
  348. function renderAll(){
  349.  medListAll.innerHTML = '';
  350.  if(!currentProfile || currentProfile.medicines.length===0){ medListAll.innerHTML = '<div class="text-muted">No medicines added yet</div>'; return; }
  351.  currentProfile.medicines.forEach(m=>{
  352.    const card = document.createElement('div'); card.className='card p-2 mb-2';
  353.    card.innerHTML = `<div class="d-flex justify-content-between align-items-start">
  354.      <div>
  355.        <div class="h6 mb-0">${escapeHtml(m.name)}</div>
  356.        <div class="small-muted">${escapeHtml(m.dosage)} • ${m.food==='after'? 'After Food':'Before Food'}</div>
  357.        <div class="small-muted">Time: <span class='time-pill'>${m.time}</span> • Cycle: ${cycleLabel(m)}</div>
  358.      </div>
  359.      <div class="text-end">
  360.        <button class='btn btn-sm btn-outline-primary edit-med' data-id='${m.id}'>Edit</button>
  361.        <button class='btn btn-sm btn-outline-danger ms-1 del-med' data-id='${m.id}'>Delete</button>
  362.      </div>
  363.    </div>`;
  364.    medListAll.appendChild(card);
  365.  });
  366.  document.querySelectorAll('.edit-med').forEach(b=> b.addEventListener('click', ()=> openEditMedicine(b.dataset.id)));
  367.  document.querySelectorAll('.del-med').forEach(b=> b.addEventListener('click', ()=> deleteMed(b.dataset.id)));
  368. }
  369.  
  370. function renderToday(){
  371.  medListToday.innerHTML = '';
  372.  if(!currentProfile) return;
  373.  const today = new Date();
  374.  const todays = currentProfile.medicines.filter(m=> isMedicineForDate(m, today));
  375.  if(todays.length===0){ medListToday.innerHTML = '<div class="text-muted">No medicines scheduled for today</div>'; return; }
  376.  todays.forEach(m=>{
  377.    const card = document.createElement('div'); card.className='card p-2 mb-2 d-flex justify-content-between align-items-center';
  378.    // check if already taken today
  379.    const taken = wasTakenToday(currentProfile.id, m.id);
  380.    card.innerHTML = `<div>
  381.        <div class='h6 mb-0'>${escapeHtml(m.name)} ${taken? '<span class="badge bg-success ms-2">Taken</span>': ''}</div>
  382.        <div class='small-muted'>${escapeHtml(m.dosage)} • ${m.time} • ${m.food==='after'?'After Food':'Before Food'}</div>
  383.      </div>
  384.      <div>
  385.        <button class='btn btn-sm btn-success mark-taken' data-id='${m.id}' ${taken? 'disabled':''}>Mark taken</button>
  386.      </div>`;
  387.    medListToday.appendChild(card);
  388.  });
  389.  document.querySelectorAll('.mark-taken').forEach(b=> b.addEventListener('click', ()=> markTaken(b.dataset.id)));
  390. }
  391.  
  392. function renderHistory(){
  393.  historyList.innerHTML = '';
  394.  const data = loadData();
  395.  const list = data.history.filter(h=> h.profileId === currentProfile.id).sort((a,b)=> new Date(b.timeTakenISO)-new Date(a.timeTakenISO));
  396.  if(list.length===0) { historyList.innerHTML = '<div class="text-muted">No history yet</div>'; return; }
  397.  list.forEach(h=>{
  398.    const el = document.createElement('div'); el.className='card p-2 mb-2';
  399.    el.innerHTML = `<div class='d-flex justify-content-between'><div><strong>${escapeHtml(h.medName)}</strong><div class='small-muted'>${escapeHtml(h.dosage)} • ${new Date(h.timeTakenISO).toLocaleString()}</div></div>
  400.      <div><button class='btn btn-sm btn-outline-secondary btn-copy' data-iso='${h.timeTakenISO}'>Copy time</button></div></div>`;
  401.    historyList.appendChild(el);
  402.  });
  403.  document.querySelectorAll('.btn-copy').forEach(b=> b.addEventListener('click', ()=> navigator.clipboard.writeText(b.dataset.iso)));
  404. }
  405.  
  406. // ---------- Helpers: cycle label, isMedicineForDate, taken checks ----------
  407. function cycleLabel(m){
  408.  if(m.cycle==='daily') return 'Daily';
  409.  if(m.cycle==='monthly') return 'Monthly on ' + (m.monthDay||'?');
  410.  if(m.cycle==='weekly') return 'Weekly on ' + (m.weekDays? m.weekDays.join(', '):'?');
  411.  return '';
  412. }
  413.  
  414. function isMedicineForDate(m, date){
  415.  if(!m) return false;
  416.  if(m.cycle==='daily') return true;
  417.  if(m.cycle==='monthly'){
  418.    const d = Number(m.monthDay);
  419.    return d === date.getDate();
  420.  }
  421.  if(m.cycle==='weekly'){
  422.    const names = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
  423.    return m.weekDays && m.weekDays.includes(names[date.getDay()]);
  424.  }
  425.  return false;
  426. }
  427.  
  428. function wasTakenToday(profileId, medId){
  429.  const d = loadData();
  430.  const today = (new Date()).toDateString();
  431.  return d.history.some(h=> h.profileId===profileId && h.medId===medId && new Date(h.timeTakenISO).toDateString()===today);
  432. }
  433.  
  434. // ---------- Add/Edit/Delete medicine ----------
  435. function openAddMedicine(){
  436.  editingMedId = null; addTitle.textContent = 'Add Medicine'; medForm.reset(); cycleDetails.innerHTML=''; medCycle.value='daily'; renderCycleDetails();
  437.  screenProfile.style.display='none'; screenApp.style.display='none'; screenAdd.style.display='block';
  438. }
  439.  
  440. function openEditMedicine(id){
  441.  const med = currentProfile.medicines.find(x=> x.id===id); if(!med) return;
  442.  editingMedId = id; addTitle.textContent = 'Edit Medicine';
  443.  medName.value = med.name; medDosage.value = med.dosage; medTime.value = med.time; document.querySelectorAll('input[name="food"]').forEach(r=> r.checked = (r.value === med.food));
  444.  medCycle.value = med.cycle; renderCycleDetails(med);
  445.  screenProfile.style.display='none'; screenApp.style.display='none'; screenAdd.style.display='block';
  446. }
  447.  
  448. function deleteMed(id){ if(!confirm('Delete this medicine?')) return; const data = loadData(); const profile = data.profiles.find(p=> p.id===data.currentProfileId); profile.medicines = profile.medicines.filter(m=> m.id!==id); saveData(data); initApp(); }
  449.  
  450. medCycle.addEventListener('change', ()=> renderCycleDetails());
  451. function renderCycleDetails(med=null){
  452.  const v = medCycle.value; if(v==='daily'){ cycleDetails.innerHTML = '<div class="form-text">Will repeat every day</div>'; return; }
  453.  if(v==='monthly'){
  454.    const val = med? (med.monthDay||'') : '';
  455.    cycleDetails.innerHTML = `<label class='form-label'>Date of month (1-31)</label><input id='monthDay' type='number' min='1' max='31' class='form-control' value='${val}' required>`; return;
  456.  }
  457.  if(v==='weekly'){
  458.    const names = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
  459.    const selected = med && med.weekDays ? med.weekDays : [];
  460.    cycleDetails.innerHTML = '<label class="form-label d-block">Select weekdays</label>' + names.map(n=> `<div class='form-check form-check-inline'>
  461.        <input class='form-check-input week-day' type='checkbox' value='${n}' id='wd_${n}' ${selected.includes(n)? 'checked':''}>
  462.        <label class='form-check-label' for='wd_${n}'>${n.slice(0,3)}</label>
  463.      </div>`).join('');
  464.    return;
  465.  }
  466. }
  467.  
  468. medForm.addEventListener('submit', (e)=>{ e.preventDefault(); saveMedicine(); });
  469. cancelMedBtn.addEventListener('click', ()=> { screenAdd.style.display='none'; screenApp.style.display='block'; });
  470.  
  471. function saveMedicine(){
  472.  const name = medName.value.trim(); const dosage = medDosage.value.trim(); const time = medTime.value; const food = document.querySelector('input[name="food"]:checked').value;
  473.  const cycle = medCycle.value;
  474.  const data = loadData(); const profile = data.profiles.find(p=> p.id === data.currentProfileId);
  475.  let medObj = editingMedId ? profile.medicines.find(m=> m.id===editingMedId) : null;
  476.  if(!medObj){ medObj = { id: uid('med'), name, dosage, time, food, cycle }; profile.medicines.push(medObj); }
  477.  else { medObj.name = name; medObj.dosage = dosage; medObj.time = time; medObj.food = food; medObj.cycle = cycle; }
  478.  
  479.  if(cycle==='monthly'){ medObj.monthDay = Number(document.getElementById('monthDay').value); }
  480.  if(cycle==='weekly'){ medObj.weekDays = Array.from(document.querySelectorAll('.week-day:checked')).map(x=>x.value); }
  481.  
  482.  saveData(data); screenAdd.style.display='none'; screenApp.style.display='block'; initApp();
  483. }
  484.  
  485. // ---------- Mark taken ----------
  486. function markTaken(medId){
  487.  const data = loadData(); const profile = data.profiles.find(p=> p.id===data.currentProfileId); const med = profile.medicines.find(m=> m.id===medId);
  488.  data.history.push({ profileId: profile.id, medId: med.id, medName: med.name, dosage: med.dosage, timeTakenISO: new Date().toISOString() });
  489.  saveData(data); renderToday();
  490. }
  491.  
  492. // ---------- Export / Import ----------
  493. exportBtn.addEventListener('click', ()=>{
  494.  const data = loadData(); const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'});
  495.  const url = URL.createObjectURL(blob);
  496.  const a = document.createElement('a'); a.href = url; a.download = 'medicycle_data.json'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
  497. });
  498.  
  499. importFile.addEventListener('change', async (e)=>{
  500.  const f = e.target.files[0]; if(!f) return; try{
  501.    const txt = await f.text(); const imported = JSON.parse(txt);
  502.    if(!imported.profiles) throw new Error('Invalid file');
  503.    if(confirm('This will replace current data. Proceed?')){ localStorage.setItem(STORAGE_KEY, JSON.stringify(imported)); alert('Imported. Reloading view.'); initApp(); }
  504.  }catch(err){ alert('Failed to import: '+err.message); }
  505.  importFile.value='';
  506. });
  507.  
  508. // ---------- Notifications ----------
  509. let notifiedSet = new Set(); // in-memory, resets on reload
  510. function requestNotifPermission(){ if('Notification' in window) Notification.requestPermission().then(perm=> console.log('notification permission', perm)); }
  511. function checkReminders(){
  512.  const now = new Date(); const hhmm = now.toTimeString().slice(0,5);
  513.  const data = loadData();
  514.  if(!data.currentProfileId) return;
  515.  const profile = data.profiles.find(p=> p.id===data.currentProfileId); if(!profile) return;
  516.  profile.medicines.forEach(m=>{
  517.    if(m.time === hhmm && isMedicineForDate(m, now)){
  518.      const key = profile.id + '|' + m.id + '|' + now.toDateString() + '|' + hhmm;
  519.      if(notifiedSet.has(key)) return; notifiedSet.add(key);
  520.      // show notification
  521.      if('Notification' in window && Notification.permission === 'granted'){
  522.        new Notification('MediCycle: ' + m.name, { body: `${m.dosage} • ${m.food==='after'? 'After Food':'Before Food'}` });
  523.      } else {
  524.        // fallback: console log
  525.        console.log('Reminder:', m.name, m.dosage);
  526.      }
  527.    }
  528.  });
  529. }
  530.  
  531. // check every 20 seconds
  532. setInterval(checkReminders, 20000);
  533.  
  534. // ---------- Theme toggle ----------
  535. function applyTheme(isDark){ if(isDark) document.documentElement.classList.add('dark'); else document.documentElement.classList.remove('dark'); localStorage.setItem('medicycle_theme', isDark? 'dark':'light'); }
  536. toggleTheme.addEventListener('click', ()=>{ const isDark = document.documentElement.classList.toggle('dark'); localStorage.setItem('medicycle_theme', isDark? 'dark':'light'); });
  537. // apply persisted theme
  538. (function(){ const t = localStorage.getItem('medicycle_theme'); if(t==='dark') applyTheme(true); else applyTheme(false); })();
  539.  
  540. // ---------- Utility: escapeHtml ----------
  541. function escapeHtml(s){ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
  542.  
  543. // ---------- Buttons wiring ----------
  544. createProfileBtn.addEventListener('click', showCreateProfileForm);
  545. addProfileBtn.addEventListener('click', showCreateProfileForm);
  546. switchProfileBtn.addEventListener('click', ()=>{ data = loadData(); data.currentProfileId=null; saveData(data); initApp(); });
  547. addMedicineBtn.addEventListener('click', openAddMedicine);
  548. deleteProfileBtn.addEventListener('click', ()=> {
  549.  if(!currentProfile) return;
  550.  if(!confirm('Delete current profile "'+currentProfile.name+'"? This removes all medicines and history for this profile.')) return;
  551.  const data = loadData();
  552.  data.profiles = data.profiles.filter(p=> p.id !== currentProfile.id);
  553.  data.history = data.history.filter(h=> h.profileId !== currentProfile.id);
  554.  data.currentProfileId = null;
  555.  saveData(data);
  556.  currentProfile = null;
  557.  initApp();
  558. });
  559.  
  560. // Request notification permissions on first interaction (polite)
  561. document.addEventListener('click', ()=>{ if(window.Notification && Notification.permission === 'default') requestNotifPermission(); }, { once:true });
  562.  
  563. // Initialize
  564. initApp();
  565. """
  566.  
  567. # README
  568. readme = """MediCycle — Medicine Reminder (Local-only webapp)
  569. Files:
  570. - index.html
  571. - css/style.css
  572. - js/app.js
  573.  
  574. Features implemented:
  575. - Multiple profiles with passkey protection (prompt on open).
  576. - Create/Edit/Delete medicine routines (daily / monthly / weekly).
  577. - "Today" tab shows only medicines scheduled for today; you can mark them as taken.
  578. - History tab shows taken records per profile.
  579. - Export / Import JSON (local file).
  580. - Dark / Light theme toggle (persisted).
  581. - Browser notifications (requests permission once).
  582. - All data stored in browser localStorage, no backend.
  583.  
  584. How to use:
  585. 1. Open index.html in a browser (supports mobile & desktop).
  586. 2. Create a profile (name, age, passkey).
  587. 3. Add medicines and schedules.
  588. 4. Use Export to backup, Import to restore (replaces current data).
  589. """
  590.  
  591. # write files
  592. (OUT_DIR / 'index.html').write_text(index_html, encoding='utf-8')
  593. css_dir = OUT_DIR / 'css'
  594. css_dir.mkdir(exist_ok=True)
  595. (css_dir / 'style.css').write_text(css, encoding='utf-8')
  596. js_dir = OUT_DIR / 'js'
  597. js_dir.mkdir(exist_ok=True)
  598. (js_dir / 'app.js').write_text(js, encoding='utf-8')
  599. (OUT_DIR / 'README.txt').write_text(readme, encoding='utf-8')
  600.  
  601. # create zip
  602. zip_path = Path('/mnt/data/medicycle_app.zip')
  603. with zipfile.ZipFile(zip_path, 'w', compression=zipfile.ZIP_DEFLATED) as z:
  604.     for p in OUT_DIR.rglob('*'):
  605.         z.write(p, arcname=str(p.relative_to(OUT_DIR.parent)))
  606.  
  607. print("Created:", zip_path)
  608. print("Files included:")
  609. for p in OUT_DIR.rglob('*'):
  610.     print("-", p.relative_to(OUT_DIR))
  611.  
  612.  
Advertisement
Add Comment
Please, Sign In to add comment