ag098636

Peptide Tracker

Jun 3rd, 2026
22
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 45.71 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <title>Retatrutide Dosing Tracker</title>
  7. <script src="https://cdn.tailwindcss.com"></script>
  8. <link rel="preconnect" href="https://fonts.googleapis.com" />
  9. <link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Syne:wght@700;800&display=swap" rel="stylesheet" />
  10. <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
  11. <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
  12. <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.5/babel.min.js"></script>
  13. <style>
  14. body { background:#0a0e1a; color:#e2e8f0; font-family:'DM Mono','Courier New',monospace; margin:0; }
  15. ::-webkit-scrollbar{width:4px} ::-webkit-scrollbar-track{background:#0a0e1a} ::-webkit-scrollbar-thumb{background:#2d3748;border-radius:2px}
  16. .vial-bar{transition:width .6s ease}
  17. input[type=date],input[type=number],input[type=text]{color-scheme:dark}
  18. input:focus{outline:none}
  19. .row-hover:hover{background:rgba(255,255,255,0.025)!important}
  20. .icon-btn{cursor:pointer;border:none;background:none;color:#6b7280;transition:color .15s,transform .15s;padding:2px 4px;font-size:14px;line-height:1}
  21. .icon-btn:hover{color:#e2e8f0;transform:scale(1.15)}
  22. .tab-btn{transition:all .15s;cursor:pointer;border:none;font-family:'DM Mono',monospace}
  23. </style>
  24. </head>
  25. <body>
  26. <div id="root"></div>
  27. <script type="text/babel">
  28. const { useState, useMemo, useCallback, useRef } = React;
  29.  
  30. // ── helpers ──────────────────────────────────────────────────────────────────
  31.  
  32. function addDays(dateStr, n) {
  33. const d = new Date(dateStr + "T12:00:00");
  34. d.setDate(d.getDate() + n);
  35. return d.toISOString().slice(0, 10);
  36. }
  37.  
  38. function assignVials(doses, vials) {
  39. const supply = vials.map(v => ({ ...v, remaining: v.size }));
  40. let vi = 0;
  41. return doses.map(dose => {
  42. let needed = dose.mg;
  43. const pulls = [];
  44. while (needed > 0.001 && vi < supply.length) {
  45. const vial = supply[vi];
  46. const take = Math.min(needed, vial.remaining);
  47. if (take > 0.001) {
  48. pulls.push({ vialId: vial.id, amt: Math.round(take * 100) / 100 });
  49. vial.remaining = Math.round((vial.remaining - take) * 100) / 100;
  50. needed = Math.round((needed - take) * 100) / 100;
  51. }
  52. if (vial.remaining < 0.001) vi++;
  53. }
  54. return { ...dose, pulls };
  55. });
  56. }
  57.  
  58. function getPhase(mg) {
  59. if (mg <= 4) return "Titration";
  60. if (mg <= 6) return "Mid";
  61. return "Maintenance";
  62. }
  63.  
  64. const PHASE_COLORS = { Titration: "#4ade80", Mid: "#facc15", Maintenance: "#f97316" };
  65.  
  66. function buildInitialDoses() {
  67. const doses = [
  68. { id: 1, date: "2026-05-20", mg: 1.5, done: true },
  69. { id: 2, date: "2026-05-26", mg: 2.0, done: true },
  70. { id: 3, date: "2026-06-01", mg: 4.0, done: true },
  71. ];
  72. let nextId = 4;
  73. let d = "2026-06-08";
  74. for (let i = 0; i < 3; i++) { doses.push({ id: nextId++, date: d, mg: 4.0, done: false }); d = addDays(d, 7); }
  75. for (let i = 0; i < 4; i++) { doses.push({ id: nextId++, date: d, mg: 6.0, done: false }); d = addDays(d, 7); }
  76. const usedSoFar = doses.reduce((s, x) => s + x.mg, 0);
  77. let remaining = 312 - usedSoFar;
  78. while (remaining >= 8) { doses.push({ id: nextId++, date: d, mg: 8.0, done: false }); remaining -= 8; d = addDays(d, 7); }
  79. return doses;
  80. }
  81.  
  82. function buildInitialVials() {
  83. return [
  84. { id: "V1", size: 12 },
  85. ...Array.from({ length: 20 }, (_, i) => ({ id: `V${i + 2}`, size: 15 })),
  86. ];
  87. }
  88.  
  89. // ── inline input styles ───────────────────────────────────────────────────────
  90.  
  91. const inputStyle = {
  92. background: "transparent",
  93. border: "none",
  94. borderBottom: "1px solid #374151",
  95. color: "#e2e8f0",
  96. fontFamily: "'DM Mono', monospace",
  97. fontSize: 12,
  98. padding: "1px 2px",
  99. width: "100%",
  100. borderRadius: 0,
  101. };
  102.  
  103. // ── sub-components ────────────────────────────────────────────────────────────
  104.  
  105. function StatCard({ label, value }) {
  106. return (
  107. <div style={{ background:"#111827", border:"1px solid #1f2937", borderRadius:8, padding:"10px 16px", flex:"1 1 90px", minWidth:80 }}>
  108. <div style={{ fontSize:10, opacity:.4, letterSpacing:"0.14em", textTransform:"uppercase", marginBottom:4 }}>{label}</div>
  109. <div style={{ fontSize:20, fontFamily:"'Syne',sans-serif", fontWeight:700 }}>{value}</div>
  110. </div>
  111. );
  112. }
  113.  
  114. function ScheduleView({ doses, vials, onUpdate, onDelete, onDeleteMany, onAdd, onAddBulk }) {
  115. const today = new Date().toISOString().slice(0, 10);
  116. const schedule = useMemo(() => assignVials(doses, vials), [doses, vials]);
  117. const [editingId, setEditingId] = useState(-1);
  118. const [editBuf, setEditBuf] = useState({});
  119. const [bulkOpen, setBulkOpen] = useState(false);
  120. const [bulk, setBulk] = useState({ startDate: '', count: 8, mg: 8.0, intervalDays: 7 });
  121. const [selectMode, setSelectMode] = useState(false);
  122. const [selected, setSelected] = useState(new Set());
  123.  
  124. const startEdit = (dose) => { setEditingId(dose.id); setEditBuf({ date: dose.date, mg: dose.mg }); };
  125. const cancelEdit = () => { setEditingId(-1); setEditBuf({}); };
  126. const commitEdit = (id) => {
  127. const mg = parseFloat(editBuf.mg);
  128. if (!editBuf.date || isNaN(mg) || mg <= 0) return;
  129. onUpdate(id, { date: editBuf.date, mg });
  130. setEditingId(-1);
  131. };
  132.  
  133. const toggleSelect = (id) => {
  134. setSelected(prev => {
  135. const next = new Set(prev);
  136. next.has(id) ? next.delete(id) : next.add(id);
  137. return next;
  138. });
  139. };
  140.  
  141. const toggleSelectAll = () => {
  142. if (selected.size === schedule.length) {
  143. setSelected(new Set());
  144. } else {
  145. setSelected(new Set(schedule.map(d => d.id)));
  146. }
  147. };
  148.  
  149. const exitSelectMode = () => { setSelectMode(false); setSelected(new Set()); };
  150.  
  151. const deleteSelected = () => {
  152. if (selected.size === 0) return;
  153. if (window.confirm(`Delete ${selected.size} dose${selected.size > 1 ? "s" : ""}?`)) {
  154. onDeleteMany([...selected]);
  155. exitSelectMode();
  156. }
  157. };
  158.  
  159. const lastDose = doses[doses.length - 1];
  160. const allSelected = selected.size > 0 && selected.size === schedule.length;
  161. const someSelected = selected.size > 0 && selected.size < schedule.length;
  162.  
  163. return (
  164. <div>
  165. {/* Legend + select toggle */}
  166. <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:12, flexWrap:"wrap", gap:8 }}>
  167. <div style={{ display:"flex", gap:14, fontSize:10, opacity:.55, flexWrap:"wrap" }}>
  168. {Object.entries(PHASE_COLORS).map(([k, v]) => (
  169. <span key={k} style={{ display:"flex", alignItems:"center", gap:5 }}>
  170. <span style={{ width:7, height:7, borderRadius:"50%", background:v, display:"inline-block" }} />
  171. {k}{k==="Titration"?" (≤4mg)":k==="Mid"?" (6mg)":" (8mg)"}
  172. </span>
  173. ))}
  174. </div>
  175. <button onClick={() => { setSelectMode(m => !m); setSelected(new Set()); setEditingId(-1); }}
  176. style={{ background: selectMode?"#374151":"#1f2937", border:"1px solid #374151", color: selectMode?"#e2e8f0":"#9ca3af",
  177. borderRadius:6, padding:"5px 12px", fontSize:10, cursor:"pointer", fontFamily:"'DM Mono',monospace",
  178. letterSpacing:"0.08em", textTransform:"uppercase" }}>
  179. {selectMode ? "Cancel select" : "Select"}
  180. </button>
  181. </div>
  182.  
  183. {/* Selection toolbar */}
  184. {selectMode && (
  185. <div style={{ display:"flex", alignItems:"center", gap:10, marginBottom:8, padding:"8px 12px",
  186. background:"rgba(239,68,68,0.08)", border:"1px solid rgba(239,68,68,0.2)", borderRadius:8 }}>
  187. <button onClick={toggleSelectAll} style={{ background:"none", border:"1px solid #4b5563", color:"#9ca3af",
  188. borderRadius:4, padding:"3px 10px", fontSize:10, cursor:"pointer", fontFamily:"'DM Mono',monospace" }}>
  189. {allSelected ? "Deselect all" : "Select all"}
  190. </button>
  191. <div style={{ fontSize:11, opacity:.5, flex:1 }}>
  192. {selected.size === 0 ? "Tap rows to select" : `${selected.size} selected`}
  193. </div>
  194. <button onClick={deleteSelected} disabled={selected.size === 0}
  195. style={{ background: selected.size > 0 ? "#7f1d1d" : "transparent",
  196. border:`1px solid ${selected.size > 0 ? "#ef4444" : "#374151"}`,
  197. color: selected.size > 0 ? "#fca5a5" : "#4b5563",
  198. borderRadius:6, padding:"5px 14px", fontSize:11, cursor: selected.size > 0 ? "pointer" : "default",
  199. fontFamily:"'DM Mono',monospace", fontWeight:500 }}>
  200. 🗑 Delete {selected.size > 0 ? selected.size : ""}
  201. </button>
  202. </div>
  203. )}
  204.  
  205. {/* Header row */}
  206. <div style={{ display:"grid", gridTemplateColumns: selectMode ? "28px 115px 58px 1fr" : "20px 115px 58px 1fr 28px 28px 28px",
  207. gap:10, padding:"4px 10px", fontSize:9, opacity:.35, letterSpacing:"0.12em", textTransform:"uppercase" }}>
  208. <div/>
  209. <div>Date</div><div>Dose</div><div>Vials</div>
  210. {!selectMode && <><div/><div/><div/></>}
  211. </div>
  212.  
  213. <div style={{ display:"flex", flexDirection:"column", gap:1 }}>
  214. {schedule.map((dose) => {
  215. const isEditing = !selectMode && editingId === dose.id;
  216. const done = dose.done;
  217. const color = PHASE_COLORS[getPhase(dose.mg)];
  218. const isToday = dose.date === today;
  219. const isBridge = dose.pulls.length > 1;
  220. const isSelected = selected.has(dose.id);
  221.  
  222. return (
  223. <div key={dose.id}
  224. className={selectMode ? "" : "row-hover"}
  225. onClick={selectMode ? () => toggleSelect(dose.id) : undefined}
  226. style={{
  227. display:"grid",
  228. gridTemplateColumns: selectMode ? "28px 115px 58px 1fr" : "20px 115px 58px 1fr 28px 28px 28px",
  229. alignItems:"center", gap:10, padding:"8px 10px", borderRadius:6, cursor: selectMode ? "pointer" : "default",
  230. background: isSelected ? "rgba(239,68,68,0.1)" : isToday ? "rgba(30,64,175,0.12)" : "transparent",
  231. border: isSelected ? "1px solid rgba(239,68,68,0.3)" : isToday ? "1px solid rgba(59,130,246,0.2)" : isBridge ? "1px solid rgba(251,191,36,0.12)" : "1px solid transparent",
  232. opacity: !selectMode && done ? .4 : 1,
  233. transition: "background 0.1s, border 0.1s",
  234. }}>
  235.  
  236. {/* checkbox or dot */}
  237. {selectMode
  238. ? <div style={{ width:16, height:16, borderRadius:3, margin:"0 auto",
  239. border: isSelected ? "none" : "1.5px solid #4b5563",
  240. background: isSelected ? "#ef4444" : "transparent",
  241. display:"flex", alignItems:"center", justifyContent:"center",
  242. fontSize:10, color:"#fff", flexShrink:0 }}>
  243. {isSelected ? "✓" : ""}
  244. </div>
  245. : <div style={{ width:7, height:7, borderRadius:"50%", background: done?"#374151":color, margin:"0 auto", boxShadow: done?"none":`0 0 5px ${color}55` }} />
  246. }
  247.  
  248. {/* date */}
  249. {isEditing
  250. ? <input type="date" style={{ ...inputStyle, fontSize:11 }} value={editBuf.date} onChange={e => setEditBuf(b => ({...b, date:e.target.value}))} />
  251. : <div style={{ fontSize:12, opacity:.8 }}>{dose.date}{isToday && <span style={{ marginLeft:5, fontSize:9, color:"#60a5fa" }}>TODAY</span>}</div>
  252. }
  253.  
  254. {/* mg */}
  255. {isEditing
  256. ? <input type="number" step="0.5" min="0.5" max="16" style={{ ...inputStyle }} value={editBuf.mg} onChange={e => setEditBuf(b => ({...b, mg:e.target.value}))} />
  257. : <div style={{ fontSize:13, fontWeight:500, color: done?"#4b5563":color }}>{dose.mg}mg</div>
  258. }
  259.  
  260. {/* pulls */}
  261. <div style={{ fontSize:10, opacity:.45, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>
  262. {dose.pulls.length ? dose.pulls.map(p=>`${p.vialId}:${p.amt}`).join(" + ") : <span style={{color:"#ef4444"}}>no supply</span>}
  263. </div>
  264.  
  265. {/* action buttons — hidden in select mode */}
  266. {!selectMode && (isEditing ? (
  267. <>
  268. <button className="icon-btn" style={{color:"#4ade80"}} onClick={() => commitEdit(dose.id)} title="Save">✓</button>
  269. <button className="icon-btn" style={{color:"#f87171"}} onClick={cancelEdit} title="Cancel">✕</button>
  270. <div/>
  271. </>
  272. ) : (
  273. <>
  274. <button className="icon-btn" style={{color: done?"#4ade80":"#374151", fontSize:13}} onClick={() => onUpdate(dose.id, { done: !done })} title={done?"Mark undone":"Mark done"}>
  275. {done ? "✓" : "○"}
  276. </button>
  277. <button className="icon-btn" onClick={() => startEdit(dose)} title="Edit">✎</button>
  278. <button className="icon-btn" style={{color:"#6b7280"}} onClick={() => { if(window.confirm("Delete this dose?")) onDelete(dose.id); }} title="Delete">🗑</button>
  279. </>
  280. ))}
  281. </div>
  282. );
  283. })}
  284. </div>
  285.  
  286. {/* Add controls */}
  287. <div style={{ marginTop:16, display:"flex", gap:8, flexWrap:"wrap", alignItems:"center" }}>
  288. <button onClick={() => onAdd(lastDose)} style={{
  289. background:"#1f2937", border:"1px solid #374151", color:"#9ca3af",
  290. borderRadius:6, padding:"7px 14px", fontSize:11, cursor:"pointer",
  291. fontFamily:"'DM Mono',monospace", letterSpacing:"0.08em",
  292. }}>+ Add one dose</button>
  293.  
  294. <button onClick={() => setBulkOpen(o => !o)} style={{
  295. background:"#1f2937", border:"1px solid #374151", color:"#9ca3af",
  296. borderRadius:6, padding:"7px 14px", fontSize:11, cursor:"pointer",
  297. fontFamily:"'DM Mono',monospace", letterSpacing:"0.08em",
  298. }}>+ Add bulk doses</button>
  299. </div>
  300.  
  301. {bulkOpen && (() => {
  302. const effectiveStart = bulk.startDate || (lastDose ? addDays(lastDose.date, parseInt(bulk.intervalDays)||7) : today);
  303. return (
  304. <div style={{ marginTop:10, background:"#111827", border:"1px solid #1f2937", borderRadius:8, padding:"14px 16px", display:"flex", gap:14, flexWrap:"wrap", alignItems:"flex-end" }}>
  305. {/* Start date */}
  306. <div style={{ display:"flex", flexDirection:"column", gap:4 }}>
  307. <label style={{ fontSize:9, opacity:.45, letterSpacing:"0.12em", textTransform:"uppercase" }}>Start date</label>
  308. <input type="date" value={bulk.startDate}
  309. onChange={e => setBulk(b => ({...b, startDate: e.target.value}))}
  310. style={{ ...inputStyle, width:130, borderBottom:"1px solid #4b5563", padding:"4px 6px", borderRadius:4 }}
  311. />
  312. <div style={{ fontSize:9, opacity:.3 }}>{bulk.startDate ? "" : `default: ${effectiveStart}`}</div>
  313. </div>
  314. {/* Numeric fields */}
  315. {[
  316. { label:"# of doses", key:"count", type:"number", min:1, max:200, step:1 },
  317. { label:"mg each", key:"mg", type:"number", min:0.5, max:16, step:0.5 },
  318. { label:"days apart", key:"intervalDays", type:"number", min:1, max:30, step:1 },
  319. ].map(f => (
  320. <div key={f.key} style={{ display:"flex", flexDirection:"column", gap:4 }}>
  321. <label style={{ fontSize:9, opacity:.45, letterSpacing:"0.12em", textTransform:"uppercase" }}>{f.label}</label>
  322. <input type={f.type} min={f.min} max={f.max} step={f.step}
  323. value={bulk[f.key]}
  324. onChange={e => setBulk(b => ({...b, [f.key]: e.target.value}))}
  325. style={{ ...inputStyle, width:80, borderBottom:"1px solid #4b5563", padding:"4px 6px", borderRadius:4 }}
  326. />
  327. </div>
  328. ))}
  329. <button onClick={() => { onAddBulk(effectiveStart, parseInt(bulk.count), parseFloat(bulk.mg), parseInt(bulk.intervalDays)); setBulkOpen(false); setBulk(b => ({...b, startDate:""})); }}
  330. style={{ background:"#1e40af", border:"none", color:"#e2e8f0", borderRadius:6, padding:"7px 16px", fontSize:11, cursor:"pointer", fontFamily:"'DM Mono',monospace" }}>
  331. Add doses
  332. </button>
  333. <button onClick={() => setBulkOpen(false)} style={{ background:"transparent", border:"1px solid #374151", color:"#6b7280", borderRadius:6, padding:"7px 12px", fontSize:11, cursor:"pointer", fontFamily:"'DM Mono',monospace" }}>
  334. Cancel
  335. </button>
  336. </div>
  337. );
  338. })()}
  339.  
  340. <div style={{ marginTop:8, fontSize:10, opacity:.2, textAlign:"right" }}>
  341. ⚠ yellow border = draws from 2 vials
  342. </div>
  343. </div>
  344. );
  345. }
  346.  
  347. function VialsView({ doses, vials, setVials }) {
  348. const schedule = useMemo(() => assignVials(doses, vials), [doses, vials]);
  349. const [editingId, setEditingId] = useState(-1);
  350. const [editBuf, setEditBuf] = useState({});
  351. const [addMode, setAddMode] = useState(null); // null | "single" | "bulk"
  352. const [newVial, setNewVial] = useState({ id:"", size:"15" });
  353. const [bulkVial, setBulkVial] = useState({ prefix:"V", startNum:"", count:"5", size:"15", insertAt:"end" });
  354. const [selectMode, setSelectMode] = useState(false);
  355. const [selected, setSelected] = useState(new Set());
  356. const [dragId, setDragId] = useState(null);
  357. const [dragOverId, setDragOverId] = useState(null);
  358.  
  359. const vialUsed = useMemo(() => {
  360. const map = {};
  361. vials.forEach(v => { map[v.id] = 0; });
  362. schedule.forEach(d => { if (d.done) d.pulls.forEach(p => { map[p.vialId] = (map[p.vialId]||0) + p.amt; }); });
  363. return map;
  364. }, [schedule, vials]);
  365.  
  366. const startEdit = (v) => { setEditingId(v.id); setEditBuf({ id: v.id, size: v.size }); };
  367. const commitEdit = () => {
  368. const size = parseFloat(editBuf.size);
  369. if (isNaN(size) || size <= 0) return;
  370. setVials(vs => vs.map(v => v.id === editingId ? { ...v, id: editBuf.id.trim()||v.id, size } : v));
  371. setEditingId(-1);
  372. };
  373.  
  374. const addSingleVial = () => {
  375. const id = newVial.id.trim();
  376. const size = parseFloat(newVial.size);
  377. if (!id || isNaN(size) || size <= 0 || vials.some(v=>v.id===id)) return;
  378. setVials(vs => [...vs, { id, size }]);
  379. setNewVial({ id:"", size:"15" });
  380. setAddMode(null);
  381. };
  382.  
  383. const addBulkVials = () => {
  384. const prefix = bulkVial.prefix.trim() || "V";
  385. const startNum = parseInt(bulkVial.startNum) || (vials.length + 1);
  386. const count = Math.max(1, parseInt(bulkVial.count) || 1);
  387. const size = parseFloat(bulkVial.size);
  388. if (isNaN(size) || size <= 0) return;
  389. const newVials = [];
  390. for (let i = 0; i < count; i++) {
  391. const id = `${prefix}${startNum + i}`;
  392. if (!vials.some(v => v.id === id)) newVials.push({ id, size });
  393. }
  394. if (newVials.length === 0) return;
  395. if (bulkVial.insertAt === "start") {
  396. setVials(vs => [...newVials, ...vs]);
  397. } else if (bulkVial.insertAt === "end") {
  398. setVials(vs => [...vs, ...newVials]);
  399. } else {
  400. // insert after specific vial id
  401. setVials(vs => {
  402. const idx = vs.findIndex(v => v.id === bulkVial.insertAt);
  403. if (idx === -1) return [...vs, ...newVials];
  404. return [...vs.slice(0, idx+1), ...newVials, ...vs.slice(idx+1)];
  405. });
  406. }
  407. setAddMode(null);
  408. };
  409.  
  410. const removeVial = (id) => {
  411. if (window.confirm(`Remove vial ${id}? This will shift vial assignments.`)) {
  412. setVials(vs => vs.filter(v => v.id !== id));
  413. }
  414. };
  415.  
  416. const toggleSelect = (id) => setSelected(prev => { const n=new Set(prev); n.has(id)?n.delete(id):n.add(id); return n; });
  417. const toggleSelectAll = () => setSelected(prev => prev.size===vials.length ? new Set() : new Set(vials.map(v=>v.id)));
  418. const exitSelect = () => { setSelectMode(false); setSelected(new Set()); };
  419. const deleteSelected = () => {
  420. if (!selected.size) return;
  421. if (window.confirm(`Remove ${selected.size} vial${selected.size>1?"s":""}? This will shift assignments.`)) {
  422. setVials(vs => vs.filter(v => !selected.has(v.id)));
  423. exitSelect();
  424. }
  425. };
  426.  
  427. // drag-to-reorder
  428. const onDragStart = (id) => setDragId(id);
  429. const onDragOver = (e, id) => { e.preventDefault(); setDragOverId(id); };
  430. const onDrop = (e, targetId) => {
  431. e.preventDefault();
  432. if (!dragId || dragId === targetId) { setDragId(null); setDragOverId(null); return; }
  433. setVials(vs => {
  434. const arr = [...vs];
  435. const from = arr.findIndex(v=>v.id===dragId);
  436. const to = arr.findIndex(v=>v.id===targetId);
  437. arr.splice(to, 0, arr.splice(from, 1)[0]);
  438. return arr;
  439. });
  440. setDragId(null); setDragOverId(null);
  441. };
  442. const onDragEnd = () => { setDragId(null); setDragOverId(null); };
  443.  
  444. return (
  445. <div>
  446. {/* Header row */}
  447. <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:10 }}>
  448. <div style={{ fontSize:10, opacity:.35, letterSpacing:"0.14em", textTransform:"uppercase" }}>
  449. Vial Inventory — completed draws · drag to reorder
  450. </div>
  451. <button onClick={() => { setSelectMode(m=>!m); setSelected(new Set()); setEditingId(-1); }}
  452. style={{ background:selectMode?"#374151":"#1f2937", border:"1px solid #374151",
  453. color:selectMode?"#e2e8f0":"#9ca3af", borderRadius:6, padding:"5px 12px",
  454. fontSize:10, cursor:"pointer", fontFamily:"'DM Mono',monospace", letterSpacing:"0.08em", textTransform:"uppercase" }}>
  455. {selectMode ? "Cancel" : "Select"}
  456. </button>
  457. </div>
  458.  
  459. {/* Selection toolbar */}
  460. {selectMode && (
  461. <div style={{ display:"flex", alignItems:"center", gap:10, marginBottom:8, padding:"8px 12px",
  462. background:"rgba(239,68,68,0.08)", border:"1px solid rgba(239,68,68,0.2)", borderRadius:8 }}>
  463. <button onClick={toggleSelectAll} style={{ background:"none", border:"1px solid #4b5563", color:"#9ca3af",
  464. borderRadius:4, padding:"3px 10px", fontSize:10, cursor:"pointer", fontFamily:"'DM Mono',monospace" }}>
  465. {selected.size===vials.length ? "Deselect all" : "Select all"}
  466. </button>
  467. <div style={{ fontSize:11, opacity:.5, flex:1 }}>{selected.size===0 ? "Tap rows to select" : `${selected.size} selected`}</div>
  468. <button onClick={deleteSelected} disabled={!selected.size}
  469. style={{ background:selected.size?"#7f1d1d":"transparent", border:`1px solid ${selected.size?"#ef4444":"#374151"}`,
  470. color:selected.size?"#fca5a5":"#4b5563", borderRadius:6, padding:"5px 14px",
  471. fontSize:11, cursor:selected.size?"pointer":"default", fontFamily:"'DM Mono',monospace", fontWeight:500 }}>
  472. 🗑 Delete {selected.size||""}
  473. </button>
  474. </div>
  475. )}
  476.  
  477. {/* Vial list */}
  478. <div style={{ display:"flex", flexDirection:"column", gap:4 }}>
  479. {vials.map(v => {
  480. const used = Math.round((vialUsed[v.id]||0)*100)/100;
  481. const pct = Math.min((used/v.size)*100,100);
  482. const isEditing = !selectMode && editingId === v.id;
  483. const isSelected = selected.has(v.id);
  484. const isDragOver = dragOverId === v.id;
  485. return (
  486. <div key={v.id}
  487. draggable={!selectMode && editingId===-1}
  488. onDragStart={() => onDragStart(v.id)}
  489. onDragOver={e => onDragOver(e, v.id)}
  490. onDrop={e => onDrop(e, v.id)}
  491. onDragEnd={onDragEnd}
  492. onClick={selectMode ? () => toggleSelect(v.id) : undefined}
  493. style={{ background: isSelected?"rgba(239,68,68,0.1)":isDragOver?"rgba(59,130,246,0.1)":"#111827",
  494. border: isSelected?"1px solid rgba(239,68,68,0.3)":isDragOver?"1px solid rgba(59,130,246,0.4)":"1px solid #1f2937",
  495. borderRadius:8, padding:"10px 14px", opacity:(!selectMode&&used>=v.size)?.5:1,
  496. cursor:selectMode?"pointer":editingId===-1?"grab":"default", transition:"background .1s, border .1s" }}>
  497. <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:6 }}>
  498. <div style={{ display:"flex", gap:10, alignItems:"center", flex:1 }}>
  499. {/* drag handle */}
  500. {!selectMode && <span style={{opacity:.2, fontSize:12, cursor:"grab", userSelect:"none"}}>⠿</span>}
  501. {/* checkbox */}
  502. {selectMode && (
  503. <div style={{ width:16, height:16, borderRadius:3, border:isSelected?"none":"1.5px solid #4b5563",
  504. background:isSelected?"#ef4444":"transparent", display:"flex", alignItems:"center",
  505. justifyContent:"center", fontSize:10, color:"#fff", flexShrink:0 }}>
  506. {isSelected?"✓":""}
  507. </div>
  508. )}
  509. {isEditing ? (
  510. <>
  511. <input value={editBuf.id} onChange={e=>setEditBuf(b=>({...b,id:e.target.value}))}
  512. style={{...inputStyle,width:60,fontFamily:"'Syne',sans-serif",fontWeight:700,fontSize:13}} />
  513. <input type="number" min="1" step="0.5" value={editBuf.size} onChange={e=>setEditBuf(b=>({...b,size:e.target.value}))}
  514. style={{...inputStyle,width:55}} placeholder="mg" />
  515. <span style={{fontSize:10,opacity:.4}}>mg</span>
  516. </>
  517. ) : (
  518. <>
  519. <span style={{ fontFamily:"'Syne',sans-serif", fontWeight:700, fontSize:13 }}>{v.id}</span>
  520. <span style={{ fontSize:10, opacity:.4 }}>{v.size}mg vial</span>
  521. </>
  522. )}
  523. </div>
  524. <div style={{ display:"flex", alignItems:"center", gap:8 }}>
  525. <div style={{ fontSize:11, textAlign:"right" }}>
  526. <span style={{ color:used>=v.size?"#4b5563":"#4ade80" }}>{Math.round((v.size-used)*100)/100}mg left</span>
  527. <span style={{ opacity:.3, margin:"0 5px" }}>·</span>
  528. <span style={{ opacity:.4 }}>{used}mg used</span>
  529. </div>
  530. {!selectMode && (isEditing ? (
  531. <>
  532. <button className="icon-btn" style={{color:"#4ade80"}} onClick={commitEdit}>✓</button>
  533. <button className="icon-btn" style={{color:"#f87171"}} onClick={()=>setEditingId(-1)}>✕</button>
  534. </>
  535. ) : (
  536. <>
  537. <button className="icon-btn" onClick={e=>{e.stopPropagation();startEdit(v);}}>✎</button>
  538. <button className="icon-btn" style={{color:"#6b7280"}} onClick={e=>{e.stopPropagation();removeVial(v.id);}}>🗑</button>
  539. </>
  540. ))}
  541. </div>
  542. </div>
  543. <div style={{ height:3, background:"#1f2937", borderRadius:2, overflow:"hidden" }}>
  544. <div className="vial-bar" style={{ height:"100%", width:`${pct}%`,
  545. background:used>=v.size?"#374151":"linear-gradient(90deg,#3b82f6,#60a5fa)", borderRadius:2 }} />
  546. </div>
  547. </div>
  548. );
  549. })}
  550. </div>
  551.  
  552. {/* Add controls */}
  553. <div style={{ marginTop:12, display:"flex", gap:8, flexWrap:"wrap" }}>
  554. <button onClick={()=>setAddMode(addMode==="single"?null:"single")}
  555. style={{ background:addMode==="single"?"#374151":"#1f2937", border:"1px solid #374151", color:"#9ca3af",
  556. borderRadius:6, padding:"7px 14px", fontSize:11, cursor:"pointer", fontFamily:"'DM Mono',monospace" }}>
  557. + Add vial
  558. </button>
  559. <button onClick={()=>setAddMode(addMode==="bulk"?null:"bulk")}
  560. style={{ background:addMode==="bulk"?"#374151":"#1f2937", border:"1px solid #374151", color:"#9ca3af",
  561. borderRadius:6, padding:"7px 14px", fontSize:11, cursor:"pointer", fontFamily:"'DM Mono',monospace" }}>
  562. + Add bulk vials
  563. </button>
  564. </div>
  565.  
  566. {/* Single add panel */}
  567. {addMode==="single" && (
  568. <div style={{ marginTop:8, background:"#111827", border:"1px solid #1f2937", borderRadius:8, padding:"14px", display:"flex", gap:12, flexWrap:"wrap", alignItems:"flex-end" }}>
  569. {[{label:"Vial ID",key:"id",placeholder:"V22",w:90},{label:"Size (mg)",key:"size",placeholder:"15",w:80}].map(f=>(
  570. <div key={f.key} style={{display:"flex",flexDirection:"column",gap:4}}>
  571. <label style={{fontSize:9,opacity:.45,letterSpacing:"0.12em",textTransform:"uppercase"}}>{f.label}</label>
  572. <input value={newVial[f.key]} placeholder={f.placeholder} onChange={e=>setNewVial(n=>({...n,[f.key]:e.target.value}))}
  573. style={{...inputStyle,width:f.w,borderBottom:"1px solid #4b5563",padding:"4px 6px",borderRadius:4}} />
  574. </div>
  575. ))}
  576. <button onClick={addSingleVial} style={{background:"#1e40af",border:"none",color:"#e2e8f0",borderRadius:6,padding:"7px 14px",fontSize:11,cursor:"pointer",fontFamily:"'DM Mono',monospace"}}>Add</button>
  577. <button onClick={()=>setAddMode(null)} style={{background:"transparent",border:"1px solid #374151",color:"#6b7280",borderRadius:6,padding:"7px 12px",fontSize:11,cursor:"pointer",fontFamily:"'DM Mono',monospace"}}>Cancel</button>
  578. </div>
  579. )}
  580.  
  581. {/* Bulk add panel */}
  582. {addMode==="bulk" && (
  583. <div style={{ marginTop:8, background:"#111827", border:"1px solid #1f2937", borderRadius:8, padding:"14px", display:"flex", gap:12, flexWrap:"wrap", alignItems:"flex-end" }}>
  584. {[
  585. {label:"ID prefix", key:"prefix", placeholder:"V", w:60},
  586. {label:"Start #", key:"startNum", placeholder:`${vials.length+1}`, w:70, type:"number"},
  587. {label:"Count", key:"count", placeholder:"5", w:60, type:"number"},
  588. {label:"mg each", key:"size", placeholder:"15", w:70, type:"number"},
  589. ].map(f=>(
  590. <div key={f.key} style={{display:"flex",flexDirection:"column",gap:4}}>
  591. <label style={{fontSize:9,opacity:.45,letterSpacing:"0.12em",textTransform:"uppercase"}}>{f.label}</label>
  592. <input type={f.type||"text"} value={bulkVial[f.key]} placeholder={f.placeholder}
  593. onChange={e=>setBulkVial(b=>({...b,[f.key]:e.target.value}))}
  594. style={{...inputStyle,width:f.w,borderBottom:"1px solid #4b5563",padding:"4px 6px",borderRadius:4}} />
  595. </div>
  596. ))}
  597. {/* Insert position */}
  598. <div style={{display:"flex",flexDirection:"column",gap:4}}>
  599. <label style={{fontSize:9,opacity:.45,letterSpacing:"0.12em",textTransform:"uppercase"}}>Insert after</label>
  600. <select value={bulkVial.insertAt} onChange={e=>setBulkVial(b=>({...b,insertAt:e.target.value}))}
  601. style={{...inputStyle,width:100,borderBottom:"1px solid #4b5563",padding:"4px 6px",borderRadius:4,cursor:"pointer"}}>
  602. <option value="start">Beginning</option>
  603. {vials.map(v=><option key={v.id} value={v.id}>After {v.id}</option>)}
  604. <option value="end">End</option>
  605. </select>
  606. </div>
  607. {/* Preview */}
  608. <div style={{fontSize:10,opacity:.4,alignSelf:"center"}}>
  609. → {bulkVial.prefix||"V"}{parseInt(bulkVial.startNum)||vials.length+1} … {bulkVial.prefix||"V"}{(parseInt(bulkVial.startNum)||vials.length+1)+(parseInt(bulkVial.count)||1)-1}
  610. </div>
  611. <button onClick={addBulkVials} style={{background:"#1e40af",border:"none",color:"#e2e8f0",borderRadius:6,padding:"7px 16px",fontSize:11,cursor:"pointer",fontFamily:"'DM Mono',monospace"}}>Add vials</button>
  612. <button onClick={()=>setAddMode(null)} style={{background:"transparent",border:"1px solid #374151",color:"#6b7280",borderRadius:6,padding:"7px 12px",fontSize:11,cursor:"pointer",fontFamily:"'DM Mono',monospace"}}>Cancel</button>
  613. </div>
  614. )}
  615.  
  616. <div style={{ marginTop:8, fontSize:10, opacity:.2, textAlign:"right" }}>Drag to reorder · order determines draw sequence</div>
  617. </div>
  618. );
  619. }
  620.  
  621. function SupplyView({ doses, vials, onUpdate }) {
  622. const today = new Date().toISOString().slice(0, 10);
  623. const schedule = useMemo(() => assignVials(doses, vials), [doses, vials]);
  624.  
  625. const plannedVialSummary = useMemo(() => {
  626. const map = {};
  627. vials.forEach(v => { map[v.id] = { size:v.size, planned:0, done:0, first:null, last:null }; });
  628. schedule.forEach(d => {
  629. d.pulls.forEach(p => {
  630. if (!map[p.vialId]) return;
  631. map[p.vialId].planned += p.amt;
  632. if (d.done) map[p.vialId].done += p.amt;
  633. if (!map[p.vialId].first) map[p.vialId].first = d.date;
  634. map[p.vialId].last = d.date;
  635. });
  636. });
  637. return vials.map(v => ({
  638. ...v,
  639. planned: Math.round((map[v.id]?.planned||0)*100)/100,
  640. done: Math.round((map[v.id]?.done||0)*100)/100,
  641. leftover: Math.round((v.size-(map[v.id]?.planned||0))*100)/100,
  642. first: map[v.id]?.first,
  643. last: map[v.id]?.last,
  644. })).filter(v => v.planned > 0);
  645. }, [schedule, vials]);
  646.  
  647. const bridgeDoses = useMemo(() => schedule.filter(d => d.pulls.length > 1), [schedule]);
  648. const activeVial = plannedVialSummary.find(v => v.done < v.planned);
  649.  
  650. return (
  651. <div>
  652. <div style={{ marginBottom:20 }}>
  653. <div style={{ fontSize:10, opacity:.4, letterSpacing:"0.18em", textTransform:"uppercase", marginBottom:10 }}>
  654. ⚠ Multi-Vial Doses — bring both vials on these dates
  655. </div>
  656. {bridgeDoses.length === 0 && <div style={{fontSize:12,opacity:.4}}>None — all doses draw from a single vial.</div>}
  657. <div style={{ display:"flex", flexDirection:"column", gap:3 }}>
  658. {bridgeDoses.map((dose, bi) => {
  659. const isPast = dose.date < today;
  660. return (
  661. <div key={bi} style={{
  662. display:"grid", gridTemplateColumns:"100px 52px 1fr 28px",
  663. gap:10, padding:"7px 12px", borderRadius:6,
  664. background: dose.done?"transparent": isPast?"rgba(251,191,36,0.03)":"rgba(251,191,36,0.07)",
  665. border: dose.done?"1px solid #1f2937":"1px solid rgba(251,191,36,0.2)",
  666. opacity: dose.done?.35:1,
  667. }}>
  668. <div style={{fontSize:12,opacity:.8}}>{dose.date}</div>
  669. <div style={{fontSize:12,color:"#facc15",fontWeight:500}}>{dose.mg}mg</div>
  670. <div style={{fontSize:11,opacity:.6}}>
  671. {dose.pulls.map((p,pi)=>(
  672. <span key={pi}>
  673. {pi>0&&<span style={{opacity:.35,margin:"0 5px"}}>+</span>}
  674. <span style={{color:"#60a5fa"}}>{p.vialId}</span>
  675. <span style={{opacity:.5}}> {p.amt}mg</span>
  676. </span>
  677. ))}
  678. </div>
  679. <button className="icon-btn" style={{color:dose.done?"#4ade80":"#374151",fontSize:12}} onClick={()=>onUpdate(dose.id,{done:!dose.done})}>
  680. {dose.done?"✓":"○"}
  681. </button>
  682. </div>
  683. );
  684. })}
  685. </div>
  686. </div>
  687.  
  688. <div style={{ fontSize:10, opacity:.4, letterSpacing:"0.18em", textTransform:"uppercase", marginBottom:10 }}>Full Vial Plan</div>
  689. <div style={{ display:"flex", flexDirection:"column", gap:5 }}>
  690. {plannedVialSummary.map(v => {
  691. const isActive = activeVial && v.id === activeVial.id;
  692. const isDone = v.done >= v.planned;
  693. const donePct = Math.min((v.done/v.size)*100,100);
  694. const plannedPct = Math.min((v.planned/v.size)*100,100);
  695. return (
  696. <div key={v.id} style={{
  697. background: isActive?"rgba(30,64,175,0.1)":"#111827",
  698. border: isActive?"1px solid rgba(59,130,246,0.3)":"1px solid #1f2937",
  699. borderRadius:8, padding:"10px 14px", opacity:isDone?.45:1,
  700. }}>
  701. <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:6 }}>
  702. <div style={{ display:"flex", gap:10, alignItems:"center" }}>
  703. <span style={{fontFamily:"'Syne',sans-serif",fontWeight:700,fontSize:13}}>{v.id}</span>
  704. <span style={{fontSize:10,opacity:.35}}>{v.size}mg</span>
  705. {isActive && <span style={{fontSize:9,color:"#60a5fa",letterSpacing:"0.1em",textTransform:"uppercase"}}>ACTIVE</span>}
  706. {isDone && <span style={{fontSize:9,color:"#6b7280",letterSpacing:"0.1em",textTransform:"uppercase"}}>DONE</span>}
  707. </div>
  708. <div style={{fontSize:10,opacity:.5}}>{v.first} → {v.last}</div>
  709. </div>
  710. <div style={{height:4,background:"#1f2937",borderRadius:2,overflow:"hidden",position:"relative"}}>
  711. <div style={{position:"absolute",left:0,top:0,height:"100%",width:`${plannedPct}%`,background:isDone?"#374151":"rgba(59,130,246,0.22)",borderRadius:2}}/>
  712. <div className="vial-bar" style={{position:"absolute",left:0,top:0,height:"100%",width:`${donePct}%`,background:isDone?"#374151":"linear-gradient(90deg,#3b82f6,#60a5fa)",borderRadius:2}}/>
  713. </div>
  714. <div style={{display:"flex",justifyContent:"space-between",marginTop:4,fontSize:10,opacity:.4}}>
  715. <span>{v.done}mg drawn</span>
  716. <span>{v.planned}mg planned · {v.leftover>=0.01?`${v.leftover}mg leftover`:"fully used"}</span>
  717. </div>
  718. </div>
  719. );
  720. })}
  721. </div>
  722. <div style={{marginTop:8,fontSize:10,opacity:.2,textAlign:"right"}}>Bright = drawn · Faint = planned · Arrow = date range</div>
  723. </div>
  724. );
  725. }
  726.  
  727. // ── main App ──────────────────────────────────────────────────────────────────
  728.  
  729. const STORAGE_KEY_DOSES = "reta_doses_v1";
  730. const STORAGE_KEY_VIALS = "reta_vials_v1";
  731.  
  732. function sanitizeDoses(doses) {
  733. let maxId = Math.max(0, ...doses.filter(d => typeof d.id === 'number' && isFinite(d.id)).map(d => d.id));
  734. return doses.map(d => (typeof d.id !== 'number' || !isFinite(d.id)) ? { ...d, id: ++maxId } : d);
  735. }
  736.  
  737. function loadDoses() {
  738. try {
  739. const raw = localStorage.getItem(STORAGE_KEY_DOSES);
  740. if (raw) return sanitizeDoses(JSON.parse(raw));
  741. } catch(e) {}
  742. return buildInitialDoses();
  743. }
  744.  
  745. function loadVials() {
  746. try {
  747. const raw = localStorage.getItem(STORAGE_KEY_VIALS);
  748. if (raw) return JSON.parse(raw);
  749. } catch(e) {}
  750. return buildInitialVials();
  751. }
  752.  
  753. function saveDoses(doses) {
  754. try { localStorage.setItem(STORAGE_KEY_DOSES, JSON.stringify(doses)); } catch(e) {}
  755. }
  756.  
  757. function saveVials(vials) {
  758. try { localStorage.setItem(STORAGE_KEY_VIALS, JSON.stringify(vials)); } catch(e) {}
  759. }
  760.  
  761. function sortedByDate(doses) {
  762. return [...doses].sort((a, b) => a.date < b.date ? -1 : a.date > b.date ? 1 : 0);
  763. }
  764.  
  765. function App() {
  766. const [doses, setDoses] = useState(() => loadDoses());
  767. const [vials, setVials] = useState(() => loadVials());
  768. const [view, setView] = useState("schedule");
  769. const nextIdRef = useRef((() => {
  770. const saved = loadDoses();
  771. return saved.length ? Math.max(...saved.map(d => d.id)) + 1 : 200;
  772. })());
  773.  
  774. const today = new Date().toISOString().slice(0, 10);
  775.  
  776. // persist whenever doses or vials change
  777. const setDosesAndSave = useCallback((updater) => {
  778. setDoses(prev => {
  779. const next = sortedByDate(typeof updater === "function" ? updater(prev) : updater);
  780. saveDoses(next);
  781. return next;
  782. });
  783. }, []);
  784.  
  785. const setVialsAndSave = useCallback((updater) => {
  786. setVials(prev => {
  787. const next = typeof updater === "function" ? updater(prev) : updater;
  788. saveVials(next);
  789. return next;
  790. });
  791. }, []);
  792.  
  793. const updateDose = useCallback((id, patch) => {
  794. setDosesAndSave(ds => ds.map(d => d.id === id ? { ...d, ...patch } : d));
  795. }, [setDosesAndSave]);
  796.  
  797. const deleteDose = useCallback((id) => {
  798. setDosesAndSave(ds => ds.filter(d => d.id !== id));
  799. }, [setDosesAndSave]);
  800.  
  801. const deleteManyDoses = useCallback((ids) => {
  802. const idSet = new Set(ids);
  803. setDosesAndSave(ds => ds.filter(d => !idSet.has(d.id)));
  804. }, [setDosesAndSave]);
  805.  
  806. const addOneDose = useCallback((afterDose) => {
  807. const newDate = afterDose ? addDays(afterDose.date, 7) : today;
  808. const newMg = afterDose ? afterDose.mg : 8.0;
  809. setDosesAndSave(ds => [...ds, { id: nextIdRef.current++, date: newDate, mg: newMg, done: false }]);
  810. }, [today, setDosesAndSave]);
  811.  
  812. const addBulkDoses = useCallback((startDate, count, mg, intervalDays) => {
  813. let d = startDate || today;
  814. const newDoses = [];
  815. for (let i = 0; i < count; i++) {
  816. newDoses.push({ id: nextIdRef.current++, date: d, mg, done: false });
  817. d = addDays(d, intervalDays);
  818. }
  819. setDosesAndSave(ds => [...ds, ...newDoses]);
  820. }, [today, setDosesAndSave]);
  821.  
  822. // stats
  823. const schedule = useMemo(() => assignVials(doses, vials), [doses, vials]);
  824. const totalMg = doses.reduce((s, d) => s + d.mg, 0);
  825. const doneMg = doses.reduce((s, d) => s + (d.done ? d.mg : 0), 0);
  826. const completedCount = doses.filter(d => d.done).length;
  827. const nextDose = schedule.find(d => !d.done);
  828. const totalSupply = vials.reduce((s, v) => s + v.size, 0);
  829. const remainingSupply = totalSupply - doneMg;
  830.  
  831. return (
  832. <div style={{ minHeight:"100vh", background:"#0a0e1a", color:"#e2e8f0", fontFamily:"'DM Mono','Courier New',monospace", padding:"24px 16px" }}>
  833. <div style={{ maxWidth:700, margin:"0 auto" }}>
  834.  
  835. {/* Header */}
  836. <div style={{ marginBottom:6, fontSize:10, opacity:.3, letterSpacing:"0.2em", textTransform:"uppercase" }}>Retatrutide Protocol</div>
  837. <div style={{ fontFamily:"'Syne',sans-serif", fontSize:26, fontWeight:800, letterSpacing:"-0.02em", marginBottom:18 }}>Dosing Tracker</div>
  838.  
  839. {/* Stats */}
  840. <div style={{ display:"flex", gap:12, marginBottom:20, flexWrap:"wrap" }}>
  841. <StatCard label="Total Doses" value={doses.length} />
  842. <StatCard label="Completed" value={completedCount} />
  843. <StatCard label="Supply Used" value={`${+doneMg.toFixed(1)}mg`} />
  844. <StatCard label="Supply Left" value={`${+remainingSupply.toFixed(1)}mg`} />
  845. </div>
  846.  
  847. {/* Next dose banner */}
  848. {nextDose && (
  849. <div style={{ background:"linear-gradient(135deg,#1a2744,#0f1e3d)", border:"1px solid #2d4a8a", borderRadius:8, padding:"11px 16px", marginBottom:20, display:"flex", alignItems:"center", gap:12 }}>
  850. <div style={{fontSize:18}}>💉</div>
  851. <div>
  852. <div style={{fontSize:10,opacity:.5,textTransform:"uppercase",letterSpacing:"0.14em",marginBottom:3}}>Next Dose</div>
  853. <div style={{fontSize:13,fontWeight:500,display:"flex",flexWrap:"wrap",gap:4,alignItems:"center"}}>
  854. <span style={{color:"#60a5fa"}}>{nextDose.date}</span>
  855. <span style={{opacity:.3}}>·</span>
  856. <span style={{color:PHASE_COLORS[getPhase(nextDose.mg)]}}>{nextDose.mg}mg</span>
  857. <span style={{opacity:.3}}>·</span>
  858. <span style={{opacity:.55,fontSize:11}}>
  859. {nextDose.pulls.length ? nextDose.pulls.map(p=>`${p.vialId}: ${p.amt}mg`).join(" + ") : "⚠ no supply"}
  860. </span>
  861. </div>
  862. </div>
  863. </div>
  864. )}
  865.  
  866. {/* Tabs */}
  867. <div style={{ display:"inline-flex", gap:3, marginBottom:18, background:"#111827", borderRadius:8, padding:4 }}>
  868. {["schedule","vials","supply"].map(t => (
  869. <button key={t} className="tab-btn" onClick={()=>setView(t)} style={{
  870. padding:"6px 18px", borderRadius:6, fontSize:11, letterSpacing:"0.1em",
  871. textTransform:"uppercase", fontWeight:500,
  872. background: view===t?"#1e40af":"transparent",
  873. color: view===t?"#e2e8f0":"#6b7280",
  874. }}>{t}</button>
  875. ))}
  876. </div>
  877.  
  878. {view==="schedule" && <ScheduleView doses={doses} vials={vials} onUpdate={updateDose} onDelete={deleteDose} onDeleteMany={deleteManyDoses} onAdd={addOneDose} onAddBulk={addBulkDoses} />}
  879. {view==="vials" && <VialsView doses={doses} vials={vials} setVials={setVialsAndSave} />}
  880. {view==="supply" && <SupplyView doses={doses} vials={vials} onUpdate={updateDose} />}
  881.  
  882. <div style={{marginTop:32,fontSize:9,opacity:.15,textAlign:"center",letterSpacing:"0.12em",textTransform:"uppercase",display:"flex",justifyContent:"center",gap:16,alignItems:"center"}}>
  883. <span>For personal tracking use only · Data saved in browser</span>
  884. <span style={{opacity:.5}}>·</span>
  885. <span style={{cursor:"pointer",textDecoration:"underline"}} onClick={()=>{ if(window.confirm("Reset all data to defaults? This cannot be undone.")) { localStorage.removeItem(STORAGE_KEY_DOSES); localStorage.removeItem(STORAGE_KEY_VIALS); window.location.reload(); } }}>Reset to defaults</span>
  886. </div>
  887. </div>
  888. </div>
  889. );
  890. }
  891.  
  892. ReactDOM.createRoot(document.getElementById("root")).render(<App />);
  893. </script>
  894. </body>
  895. </html>
Advertisement
Comments
  • ag098636
    2 hours (edited)
    # HTML 0.13 KB | 0 0
    1. Create a new blank .html file locally, paste the contents into the new file and it should work. Comment with any bugs or recommendations.
Add Comment
Please, Sign In to add comment