Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
- <title>Retatrutide Dosing Tracker</title>
- <script src="https://cdn.tailwindcss.com"></script>
- <link rel="preconnect" href="https://fonts.googleapis.com" />
- <link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Syne:wght@700;800&display=swap" rel="stylesheet" />
- <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.5/babel.min.js"></script>
- <style>
- body { background:#0a0e1a; color:#e2e8f0; font-family:'DM Mono','Courier New',monospace; margin:0; }
- ::-webkit-scrollbar{width:4px} ::-webkit-scrollbar-track{background:#0a0e1a} ::-webkit-scrollbar-thumb{background:#2d3748;border-radius:2px}
- .vial-bar{transition:width .6s ease}
- input[type=date],input[type=number],input[type=text]{color-scheme:dark}
- input:focus{outline:none}
- .row-hover:hover{background:rgba(255,255,255,0.025)!important}
- .icon-btn{cursor:pointer;border:none;background:none;color:#6b7280;transition:color .15s,transform .15s;padding:2px 4px;font-size:14px;line-height:1}
- .icon-btn:hover{color:#e2e8f0;transform:scale(1.15)}
- .tab-btn{transition:all .15s;cursor:pointer;border:none;font-family:'DM Mono',monospace}
- </style>
- </head>
- <body>
- <div id="root"></div>
- <script type="text/babel">
- const { useState, useMemo, useCallback, useRef } = React;
- // ── helpers ──────────────────────────────────────────────────────────────────
- function addDays(dateStr, n) {
- const d = new Date(dateStr + "T12:00:00");
- d.setDate(d.getDate() + n);
- return d.toISOString().slice(0, 10);
- }
- function assignVials(doses, vials) {
- const supply = vials.map(v => ({ ...v, remaining: v.size }));
- let vi = 0;
- return doses.map(dose => {
- let needed = dose.mg;
- const pulls = [];
- while (needed > 0.001 && vi < supply.length) {
- const vial = supply[vi];
- const take = Math.min(needed, vial.remaining);
- if (take > 0.001) {
- pulls.push({ vialId: vial.id, amt: Math.round(take * 100) / 100 });
- vial.remaining = Math.round((vial.remaining - take) * 100) / 100;
- needed = Math.round((needed - take) * 100) / 100;
- }
- if (vial.remaining < 0.001) vi++;
- }
- return { ...dose, pulls };
- });
- }
- function getPhase(mg) {
- if (mg <= 4) return "Titration";
- if (mg <= 6) return "Mid";
- return "Maintenance";
- }
- const PHASE_COLORS = { Titration: "#4ade80", Mid: "#facc15", Maintenance: "#f97316" };
- function buildInitialDoses() {
- const doses = [
- { id: 1, date: "2026-05-20", mg: 1.5, done: true },
- { id: 2, date: "2026-05-26", mg: 2.0, done: true },
- { id: 3, date: "2026-06-01", mg: 4.0, done: true },
- ];
- let nextId = 4;
- let d = "2026-06-08";
- for (let i = 0; i < 3; i++) { doses.push({ id: nextId++, date: d, mg: 4.0, done: false }); d = addDays(d, 7); }
- for (let i = 0; i < 4; i++) { doses.push({ id: nextId++, date: d, mg: 6.0, done: false }); d = addDays(d, 7); }
- const usedSoFar = doses.reduce((s, x) => s + x.mg, 0);
- let remaining = 312 - usedSoFar;
- while (remaining >= 8) { doses.push({ id: nextId++, date: d, mg: 8.0, done: false }); remaining -= 8; d = addDays(d, 7); }
- return doses;
- }
- function buildInitialVials() {
- return [
- { id: "V1", size: 12 },
- ...Array.from({ length: 20 }, (_, i) => ({ id: `V${i + 2}`, size: 15 })),
- ];
- }
- // ── inline input styles ───────────────────────────────────────────────────────
- const inputStyle = {
- background: "transparent",
- border: "none",
- borderBottom: "1px solid #374151",
- color: "#e2e8f0",
- fontFamily: "'DM Mono', monospace",
- fontSize: 12,
- padding: "1px 2px",
- width: "100%",
- borderRadius: 0,
- };
- // ── sub-components ────────────────────────────────────────────────────────────
- function StatCard({ label, value }) {
- return (
- <div style={{ background:"#111827", border:"1px solid #1f2937", borderRadius:8, padding:"10px 16px", flex:"1 1 90px", minWidth:80 }}>
- <div style={{ fontSize:10, opacity:.4, letterSpacing:"0.14em", textTransform:"uppercase", marginBottom:4 }}>{label}</div>
- <div style={{ fontSize:20, fontFamily:"'Syne',sans-serif", fontWeight:700 }}>{value}</div>
- </div>
- );
- }
- function ScheduleView({ doses, vials, onUpdate, onDelete, onDeleteMany, onAdd, onAddBulk }) {
- const today = new Date().toISOString().slice(0, 10);
- const schedule = useMemo(() => assignVials(doses, vials), [doses, vials]);
- const [editingId, setEditingId] = useState(-1);
- const [editBuf, setEditBuf] = useState({});
- const [bulkOpen, setBulkOpen] = useState(false);
- const [bulk, setBulk] = useState({ startDate: '', count: 8, mg: 8.0, intervalDays: 7 });
- const [selectMode, setSelectMode] = useState(false);
- const [selected, setSelected] = useState(new Set());
- const startEdit = (dose) => { setEditingId(dose.id); setEditBuf({ date: dose.date, mg: dose.mg }); };
- const cancelEdit = () => { setEditingId(-1); setEditBuf({}); };
- const commitEdit = (id) => {
- const mg = parseFloat(editBuf.mg);
- if (!editBuf.date || isNaN(mg) || mg <= 0) return;
- onUpdate(id, { date: editBuf.date, mg });
- setEditingId(-1);
- };
- const toggleSelect = (id) => {
- setSelected(prev => {
- const next = new Set(prev);
- next.has(id) ? next.delete(id) : next.add(id);
- return next;
- });
- };
- const toggleSelectAll = () => {
- if (selected.size === schedule.length) {
- setSelected(new Set());
- } else {
- setSelected(new Set(schedule.map(d => d.id)));
- }
- };
- const exitSelectMode = () => { setSelectMode(false); setSelected(new Set()); };
- const deleteSelected = () => {
- if (selected.size === 0) return;
- if (window.confirm(`Delete ${selected.size} dose${selected.size > 1 ? "s" : ""}?`)) {
- onDeleteMany([...selected]);
- exitSelectMode();
- }
- };
- const lastDose = doses[doses.length - 1];
- const allSelected = selected.size > 0 && selected.size === schedule.length;
- const someSelected = selected.size > 0 && selected.size < schedule.length;
- return (
- <div>
- {/* Legend + select toggle */}
- <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:12, flexWrap:"wrap", gap:8 }}>
- <div style={{ display:"flex", gap:14, fontSize:10, opacity:.55, flexWrap:"wrap" }}>
- {Object.entries(PHASE_COLORS).map(([k, v]) => (
- <span key={k} style={{ display:"flex", alignItems:"center", gap:5 }}>
- <span style={{ width:7, height:7, borderRadius:"50%", background:v, display:"inline-block" }} />
- {k}{k==="Titration"?" (≤4mg)":k==="Mid"?" (6mg)":" (8mg)"}
- </span>
- ))}
- </div>
- <button onClick={() => { setSelectMode(m => !m); setSelected(new Set()); setEditingId(-1); }}
- style={{ background: selectMode?"#374151":"#1f2937", border:"1px solid #374151", color: selectMode?"#e2e8f0":"#9ca3af",
- borderRadius:6, padding:"5px 12px", fontSize:10, cursor:"pointer", fontFamily:"'DM Mono',monospace",
- letterSpacing:"0.08em", textTransform:"uppercase" }}>
- {selectMode ? "Cancel select" : "Select"}
- </button>
- </div>
- {/* Selection toolbar */}
- {selectMode && (
- <div style={{ display:"flex", alignItems:"center", gap:10, marginBottom:8, padding:"8px 12px",
- background:"rgba(239,68,68,0.08)", border:"1px solid rgba(239,68,68,0.2)", borderRadius:8 }}>
- <button onClick={toggleSelectAll} style={{ background:"none", border:"1px solid #4b5563", color:"#9ca3af",
- borderRadius:4, padding:"3px 10px", fontSize:10, cursor:"pointer", fontFamily:"'DM Mono',monospace" }}>
- {allSelected ? "Deselect all" : "Select all"}
- </button>
- <div style={{ fontSize:11, opacity:.5, flex:1 }}>
- {selected.size === 0 ? "Tap rows to select" : `${selected.size} selected`}
- </div>
- <button onClick={deleteSelected} disabled={selected.size === 0}
- style={{ background: selected.size > 0 ? "#7f1d1d" : "transparent",
- border:`1px solid ${selected.size > 0 ? "#ef4444" : "#374151"}`,
- color: selected.size > 0 ? "#fca5a5" : "#4b5563",
- borderRadius:6, padding:"5px 14px", fontSize:11, cursor: selected.size > 0 ? "pointer" : "default",
- fontFamily:"'DM Mono',monospace", fontWeight:500 }}>
- 🗑 Delete {selected.size > 0 ? selected.size : ""}
- </button>
- </div>
- )}
- {/* Header row */}
- <div style={{ display:"grid", gridTemplateColumns: selectMode ? "28px 115px 58px 1fr" : "20px 115px 58px 1fr 28px 28px 28px",
- gap:10, padding:"4px 10px", fontSize:9, opacity:.35, letterSpacing:"0.12em", textTransform:"uppercase" }}>
- <div/>
- <div>Date</div><div>Dose</div><div>Vials</div>
- {!selectMode && <><div/><div/><div/></>}
- </div>
- <div style={{ display:"flex", flexDirection:"column", gap:1 }}>
- {schedule.map((dose) => {
- const isEditing = !selectMode && editingId === dose.id;
- const done = dose.done;
- const color = PHASE_COLORS[getPhase(dose.mg)];
- const isToday = dose.date === today;
- const isBridge = dose.pulls.length > 1;
- const isSelected = selected.has(dose.id);
- return (
- <div key={dose.id}
- className={selectMode ? "" : "row-hover"}
- onClick={selectMode ? () => toggleSelect(dose.id) : undefined}
- style={{
- display:"grid",
- gridTemplateColumns: selectMode ? "28px 115px 58px 1fr" : "20px 115px 58px 1fr 28px 28px 28px",
- alignItems:"center", gap:10, padding:"8px 10px", borderRadius:6, cursor: selectMode ? "pointer" : "default",
- background: isSelected ? "rgba(239,68,68,0.1)" : isToday ? "rgba(30,64,175,0.12)" : "transparent",
- 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",
- opacity: !selectMode && done ? .4 : 1,
- transition: "background 0.1s, border 0.1s",
- }}>
- {/* checkbox or dot */}
- {selectMode
- ? <div style={{ width:16, height:16, borderRadius:3, margin:"0 auto",
- border: isSelected ? "none" : "1.5px solid #4b5563",
- background: isSelected ? "#ef4444" : "transparent",
- display:"flex", alignItems:"center", justifyContent:"center",
- fontSize:10, color:"#fff", flexShrink:0 }}>
- {isSelected ? "✓" : ""}
- </div>
- : <div style={{ width:7, height:7, borderRadius:"50%", background: done?"#374151":color, margin:"0 auto", boxShadow: done?"none":`0 0 5px ${color}55` }} />
- }
- {/* date */}
- {isEditing
- ? <input type="date" style={{ ...inputStyle, fontSize:11 }} value={editBuf.date} onChange={e => setEditBuf(b => ({...b, date:e.target.value}))} />
- : <div style={{ fontSize:12, opacity:.8 }}>{dose.date}{isToday && <span style={{ marginLeft:5, fontSize:9, color:"#60a5fa" }}>TODAY</span>}</div>
- }
- {/* mg */}
- {isEditing
- ? <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}))} />
- : <div style={{ fontSize:13, fontWeight:500, color: done?"#4b5563":color }}>{dose.mg}mg</div>
- }
- {/* pulls */}
- <div style={{ fontSize:10, opacity:.45, overflow:"hidden", textOverflow:"ellipsis", whiteSpace:"nowrap" }}>
- {dose.pulls.length ? dose.pulls.map(p=>`${p.vialId}:${p.amt}`).join(" + ") : <span style={{color:"#ef4444"}}>no supply</span>}
- </div>
- {/* action buttons — hidden in select mode */}
- {!selectMode && (isEditing ? (
- <>
- <button className="icon-btn" style={{color:"#4ade80"}} onClick={() => commitEdit(dose.id)} title="Save">✓</button>
- <button className="icon-btn" style={{color:"#f87171"}} onClick={cancelEdit} title="Cancel">✕</button>
- <div/>
- </>
- ) : (
- <>
- <button className="icon-btn" style={{color: done?"#4ade80":"#374151", fontSize:13}} onClick={() => onUpdate(dose.id, { done: !done })} title={done?"Mark undone":"Mark done"}>
- {done ? "✓" : "○"}
- </button>
- <button className="icon-btn" onClick={() => startEdit(dose)} title="Edit">✎</button>
- <button className="icon-btn" style={{color:"#6b7280"}} onClick={() => { if(window.confirm("Delete this dose?")) onDelete(dose.id); }} title="Delete">🗑</button>
- </>
- ))}
- </div>
- );
- })}
- </div>
- {/* Add controls */}
- <div style={{ marginTop:16, display:"flex", gap:8, flexWrap:"wrap", alignItems:"center" }}>
- <button onClick={() => onAdd(lastDose)} style={{
- background:"#1f2937", border:"1px solid #374151", color:"#9ca3af",
- borderRadius:6, padding:"7px 14px", fontSize:11, cursor:"pointer",
- fontFamily:"'DM Mono',monospace", letterSpacing:"0.08em",
- }}>+ Add one dose</button>
- <button onClick={() => setBulkOpen(o => !o)} style={{
- background:"#1f2937", border:"1px solid #374151", color:"#9ca3af",
- borderRadius:6, padding:"7px 14px", fontSize:11, cursor:"pointer",
- fontFamily:"'DM Mono',monospace", letterSpacing:"0.08em",
- }}>+ Add bulk doses</button>
- </div>
- {bulkOpen && (() => {
- const effectiveStart = bulk.startDate || (lastDose ? addDays(lastDose.date, parseInt(bulk.intervalDays)||7) : today);
- return (
- <div style={{ marginTop:10, background:"#111827", border:"1px solid #1f2937", borderRadius:8, padding:"14px 16px", display:"flex", gap:14, flexWrap:"wrap", alignItems:"flex-end" }}>
- {/* Start date */}
- <div style={{ display:"flex", flexDirection:"column", gap:4 }}>
- <label style={{ fontSize:9, opacity:.45, letterSpacing:"0.12em", textTransform:"uppercase" }}>Start date</label>
- <input type="date" value={bulk.startDate}
- onChange={e => setBulk(b => ({...b, startDate: e.target.value}))}
- style={{ ...inputStyle, width:130, borderBottom:"1px solid #4b5563", padding:"4px 6px", borderRadius:4 }}
- />
- <div style={{ fontSize:9, opacity:.3 }}>{bulk.startDate ? "" : `default: ${effectiveStart}`}</div>
- </div>
- {/* Numeric fields */}
- {[
- { label:"# of doses", key:"count", type:"number", min:1, max:200, step:1 },
- { label:"mg each", key:"mg", type:"number", min:0.5, max:16, step:0.5 },
- { label:"days apart", key:"intervalDays", type:"number", min:1, max:30, step:1 },
- ].map(f => (
- <div key={f.key} style={{ display:"flex", flexDirection:"column", gap:4 }}>
- <label style={{ fontSize:9, opacity:.45, letterSpacing:"0.12em", textTransform:"uppercase" }}>{f.label}</label>
- <input type={f.type} min={f.min} max={f.max} step={f.step}
- value={bulk[f.key]}
- onChange={e => setBulk(b => ({...b, [f.key]: e.target.value}))}
- style={{ ...inputStyle, width:80, borderBottom:"1px solid #4b5563", padding:"4px 6px", borderRadius:4 }}
- />
- </div>
- ))}
- <button onClick={() => { onAddBulk(effectiveStart, parseInt(bulk.count), parseFloat(bulk.mg), parseInt(bulk.intervalDays)); setBulkOpen(false); setBulk(b => ({...b, startDate:""})); }}
- style={{ background:"#1e40af", border:"none", color:"#e2e8f0", borderRadius:6, padding:"7px 16px", fontSize:11, cursor:"pointer", fontFamily:"'DM Mono',monospace" }}>
- Add doses
- </button>
- <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" }}>
- Cancel
- </button>
- </div>
- );
- })()}
- <div style={{ marginTop:8, fontSize:10, opacity:.2, textAlign:"right" }}>
- ⚠ yellow border = draws from 2 vials
- </div>
- </div>
- );
- }
- function VialsView({ doses, vials, setVials }) {
- const schedule = useMemo(() => assignVials(doses, vials), [doses, vials]);
- const [editingId, setEditingId] = useState(-1);
- const [editBuf, setEditBuf] = useState({});
- const [addMode, setAddMode] = useState(null); // null | "single" | "bulk"
- const [newVial, setNewVial] = useState({ id:"", size:"15" });
- const [bulkVial, setBulkVial] = useState({ prefix:"V", startNum:"", count:"5", size:"15", insertAt:"end" });
- const [selectMode, setSelectMode] = useState(false);
- const [selected, setSelected] = useState(new Set());
- const [dragId, setDragId] = useState(null);
- const [dragOverId, setDragOverId] = useState(null);
- const vialUsed = useMemo(() => {
- const map = {};
- vials.forEach(v => { map[v.id] = 0; });
- schedule.forEach(d => { if (d.done) d.pulls.forEach(p => { map[p.vialId] = (map[p.vialId]||0) + p.amt; }); });
- return map;
- }, [schedule, vials]);
- const startEdit = (v) => { setEditingId(v.id); setEditBuf({ id: v.id, size: v.size }); };
- const commitEdit = () => {
- const size = parseFloat(editBuf.size);
- if (isNaN(size) || size <= 0) return;
- setVials(vs => vs.map(v => v.id === editingId ? { ...v, id: editBuf.id.trim()||v.id, size } : v));
- setEditingId(-1);
- };
- const addSingleVial = () => {
- const id = newVial.id.trim();
- const size = parseFloat(newVial.size);
- if (!id || isNaN(size) || size <= 0 || vials.some(v=>v.id===id)) return;
- setVials(vs => [...vs, { id, size }]);
- setNewVial({ id:"", size:"15" });
- setAddMode(null);
- };
- const addBulkVials = () => {
- const prefix = bulkVial.prefix.trim() || "V";
- const startNum = parseInt(bulkVial.startNum) || (vials.length + 1);
- const count = Math.max(1, parseInt(bulkVial.count) || 1);
- const size = parseFloat(bulkVial.size);
- if (isNaN(size) || size <= 0) return;
- const newVials = [];
- for (let i = 0; i < count; i++) {
- const id = `${prefix}${startNum + i}`;
- if (!vials.some(v => v.id === id)) newVials.push({ id, size });
- }
- if (newVials.length === 0) return;
- if (bulkVial.insertAt === "start") {
- setVials(vs => [...newVials, ...vs]);
- } else if (bulkVial.insertAt === "end") {
- setVials(vs => [...vs, ...newVials]);
- } else {
- // insert after specific vial id
- setVials(vs => {
- const idx = vs.findIndex(v => v.id === bulkVial.insertAt);
- if (idx === -1) return [...vs, ...newVials];
- return [...vs.slice(0, idx+1), ...newVials, ...vs.slice(idx+1)];
- });
- }
- setAddMode(null);
- };
- const removeVial = (id) => {
- if (window.confirm(`Remove vial ${id}? This will shift vial assignments.`)) {
- setVials(vs => vs.filter(v => v.id !== id));
- }
- };
- const toggleSelect = (id) => setSelected(prev => { const n=new Set(prev); n.has(id)?n.delete(id):n.add(id); return n; });
- const toggleSelectAll = () => setSelected(prev => prev.size===vials.length ? new Set() : new Set(vials.map(v=>v.id)));
- const exitSelect = () => { setSelectMode(false); setSelected(new Set()); };
- const deleteSelected = () => {
- if (!selected.size) return;
- if (window.confirm(`Remove ${selected.size} vial${selected.size>1?"s":""}? This will shift assignments.`)) {
- setVials(vs => vs.filter(v => !selected.has(v.id)));
- exitSelect();
- }
- };
- // drag-to-reorder
- const onDragStart = (id) => setDragId(id);
- const onDragOver = (e, id) => { e.preventDefault(); setDragOverId(id); };
- const onDrop = (e, targetId) => {
- e.preventDefault();
- if (!dragId || dragId === targetId) { setDragId(null); setDragOverId(null); return; }
- setVials(vs => {
- const arr = [...vs];
- const from = arr.findIndex(v=>v.id===dragId);
- const to = arr.findIndex(v=>v.id===targetId);
- arr.splice(to, 0, arr.splice(from, 1)[0]);
- return arr;
- });
- setDragId(null); setDragOverId(null);
- };
- const onDragEnd = () => { setDragId(null); setDragOverId(null); };
- return (
- <div>
- {/* Header row */}
- <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:10 }}>
- <div style={{ fontSize:10, opacity:.35, letterSpacing:"0.14em", textTransform:"uppercase" }}>
- Vial Inventory — completed draws · drag to reorder
- </div>
- <button onClick={() => { setSelectMode(m=>!m); setSelected(new Set()); setEditingId(-1); }}
- style={{ background:selectMode?"#374151":"#1f2937", border:"1px solid #374151",
- color:selectMode?"#e2e8f0":"#9ca3af", borderRadius:6, padding:"5px 12px",
- fontSize:10, cursor:"pointer", fontFamily:"'DM Mono',monospace", letterSpacing:"0.08em", textTransform:"uppercase" }}>
- {selectMode ? "Cancel" : "Select"}
- </button>
- </div>
- {/* Selection toolbar */}
- {selectMode && (
- <div style={{ display:"flex", alignItems:"center", gap:10, marginBottom:8, padding:"8px 12px",
- background:"rgba(239,68,68,0.08)", border:"1px solid rgba(239,68,68,0.2)", borderRadius:8 }}>
- <button onClick={toggleSelectAll} style={{ background:"none", border:"1px solid #4b5563", color:"#9ca3af",
- borderRadius:4, padding:"3px 10px", fontSize:10, cursor:"pointer", fontFamily:"'DM Mono',monospace" }}>
- {selected.size===vials.length ? "Deselect all" : "Select all"}
- </button>
- <div style={{ fontSize:11, opacity:.5, flex:1 }}>{selected.size===0 ? "Tap rows to select" : `${selected.size} selected`}</div>
- <button onClick={deleteSelected} disabled={!selected.size}
- style={{ background:selected.size?"#7f1d1d":"transparent", border:`1px solid ${selected.size?"#ef4444":"#374151"}`,
- color:selected.size?"#fca5a5":"#4b5563", borderRadius:6, padding:"5px 14px",
- fontSize:11, cursor:selected.size?"pointer":"default", fontFamily:"'DM Mono',monospace", fontWeight:500 }}>
- 🗑 Delete {selected.size||""}
- </button>
- </div>
- )}
- {/* Vial list */}
- <div style={{ display:"flex", flexDirection:"column", gap:4 }}>
- {vials.map(v => {
- const used = Math.round((vialUsed[v.id]||0)*100)/100;
- const pct = Math.min((used/v.size)*100,100);
- const isEditing = !selectMode && editingId === v.id;
- const isSelected = selected.has(v.id);
- const isDragOver = dragOverId === v.id;
- return (
- <div key={v.id}
- draggable={!selectMode && editingId===-1}
- onDragStart={() => onDragStart(v.id)}
- onDragOver={e => onDragOver(e, v.id)}
- onDrop={e => onDrop(e, v.id)}
- onDragEnd={onDragEnd}
- onClick={selectMode ? () => toggleSelect(v.id) : undefined}
- style={{ background: isSelected?"rgba(239,68,68,0.1)":isDragOver?"rgba(59,130,246,0.1)":"#111827",
- border: isSelected?"1px solid rgba(239,68,68,0.3)":isDragOver?"1px solid rgba(59,130,246,0.4)":"1px solid #1f2937",
- borderRadius:8, padding:"10px 14px", opacity:(!selectMode&&used>=v.size)?.5:1,
- cursor:selectMode?"pointer":editingId===-1?"grab":"default", transition:"background .1s, border .1s" }}>
- <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:6 }}>
- <div style={{ display:"flex", gap:10, alignItems:"center", flex:1 }}>
- {/* drag handle */}
- {!selectMode && <span style={{opacity:.2, fontSize:12, cursor:"grab", userSelect:"none"}}>⠿</span>}
- {/* checkbox */}
- {selectMode && (
- <div style={{ width:16, height:16, borderRadius:3, border:isSelected?"none":"1.5px solid #4b5563",
- background:isSelected?"#ef4444":"transparent", display:"flex", alignItems:"center",
- justifyContent:"center", fontSize:10, color:"#fff", flexShrink:0 }}>
- {isSelected?"✓":""}
- </div>
- )}
- {isEditing ? (
- <>
- <input value={editBuf.id} onChange={e=>setEditBuf(b=>({...b,id:e.target.value}))}
- style={{...inputStyle,width:60,fontFamily:"'Syne',sans-serif",fontWeight:700,fontSize:13}} />
- <input type="number" min="1" step="0.5" value={editBuf.size} onChange={e=>setEditBuf(b=>({...b,size:e.target.value}))}
- style={{...inputStyle,width:55}} placeholder="mg" />
- <span style={{fontSize:10,opacity:.4}}>mg</span>
- </>
- ) : (
- <>
- <span style={{ fontFamily:"'Syne',sans-serif", fontWeight:700, fontSize:13 }}>{v.id}</span>
- <span style={{ fontSize:10, opacity:.4 }}>{v.size}mg vial</span>
- </>
- )}
- </div>
- <div style={{ display:"flex", alignItems:"center", gap:8 }}>
- <div style={{ fontSize:11, textAlign:"right" }}>
- <span style={{ color:used>=v.size?"#4b5563":"#4ade80" }}>{Math.round((v.size-used)*100)/100}mg left</span>
- <span style={{ opacity:.3, margin:"0 5px" }}>·</span>
- <span style={{ opacity:.4 }}>{used}mg used</span>
- </div>
- {!selectMode && (isEditing ? (
- <>
- <button className="icon-btn" style={{color:"#4ade80"}} onClick={commitEdit}>✓</button>
- <button className="icon-btn" style={{color:"#f87171"}} onClick={()=>setEditingId(-1)}>✕</button>
- </>
- ) : (
- <>
- <button className="icon-btn" onClick={e=>{e.stopPropagation();startEdit(v);}}>✎</button>
- <button className="icon-btn" style={{color:"#6b7280"}} onClick={e=>{e.stopPropagation();removeVial(v.id);}}>🗑</button>
- </>
- ))}
- </div>
- </div>
- <div style={{ height:3, background:"#1f2937", borderRadius:2, overflow:"hidden" }}>
- <div className="vial-bar" style={{ height:"100%", width:`${pct}%`,
- background:used>=v.size?"#374151":"linear-gradient(90deg,#3b82f6,#60a5fa)", borderRadius:2 }} />
- </div>
- </div>
- );
- })}
- </div>
- {/* Add controls */}
- <div style={{ marginTop:12, display:"flex", gap:8, flexWrap:"wrap" }}>
- <button onClick={()=>setAddMode(addMode==="single"?null:"single")}
- style={{ background:addMode==="single"?"#374151":"#1f2937", border:"1px solid #374151", color:"#9ca3af",
- borderRadius:6, padding:"7px 14px", fontSize:11, cursor:"pointer", fontFamily:"'DM Mono',monospace" }}>
- + Add vial
- </button>
- <button onClick={()=>setAddMode(addMode==="bulk"?null:"bulk")}
- style={{ background:addMode==="bulk"?"#374151":"#1f2937", border:"1px solid #374151", color:"#9ca3af",
- borderRadius:6, padding:"7px 14px", fontSize:11, cursor:"pointer", fontFamily:"'DM Mono',monospace" }}>
- + Add bulk vials
- </button>
- </div>
- {/* Single add panel */}
- {addMode==="single" && (
- <div style={{ marginTop:8, background:"#111827", border:"1px solid #1f2937", borderRadius:8, padding:"14px", display:"flex", gap:12, flexWrap:"wrap", alignItems:"flex-end" }}>
- {[{label:"Vial ID",key:"id",placeholder:"V22",w:90},{label:"Size (mg)",key:"size",placeholder:"15",w:80}].map(f=>(
- <div key={f.key} style={{display:"flex",flexDirection:"column",gap:4}}>
- <label style={{fontSize:9,opacity:.45,letterSpacing:"0.12em",textTransform:"uppercase"}}>{f.label}</label>
- <input value={newVial[f.key]} placeholder={f.placeholder} onChange={e=>setNewVial(n=>({...n,[f.key]:e.target.value}))}
- style={{...inputStyle,width:f.w,borderBottom:"1px solid #4b5563",padding:"4px 6px",borderRadius:4}} />
- </div>
- ))}
- <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>
- <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>
- </div>
- )}
- {/* Bulk add panel */}
- {addMode==="bulk" && (
- <div style={{ marginTop:8, background:"#111827", border:"1px solid #1f2937", borderRadius:8, padding:"14px", display:"flex", gap:12, flexWrap:"wrap", alignItems:"flex-end" }}>
- {[
- {label:"ID prefix", key:"prefix", placeholder:"V", w:60},
- {label:"Start #", key:"startNum", placeholder:`${vials.length+1}`, w:70, type:"number"},
- {label:"Count", key:"count", placeholder:"5", w:60, type:"number"},
- {label:"mg each", key:"size", placeholder:"15", w:70, type:"number"},
- ].map(f=>(
- <div key={f.key} style={{display:"flex",flexDirection:"column",gap:4}}>
- <label style={{fontSize:9,opacity:.45,letterSpacing:"0.12em",textTransform:"uppercase"}}>{f.label}</label>
- <input type={f.type||"text"} value={bulkVial[f.key]} placeholder={f.placeholder}
- onChange={e=>setBulkVial(b=>({...b,[f.key]:e.target.value}))}
- style={{...inputStyle,width:f.w,borderBottom:"1px solid #4b5563",padding:"4px 6px",borderRadius:4}} />
- </div>
- ))}
- {/* Insert position */}
- <div style={{display:"flex",flexDirection:"column",gap:4}}>
- <label style={{fontSize:9,opacity:.45,letterSpacing:"0.12em",textTransform:"uppercase"}}>Insert after</label>
- <select value={bulkVial.insertAt} onChange={e=>setBulkVial(b=>({...b,insertAt:e.target.value}))}
- style={{...inputStyle,width:100,borderBottom:"1px solid #4b5563",padding:"4px 6px",borderRadius:4,cursor:"pointer"}}>
- <option value="start">Beginning</option>
- {vials.map(v=><option key={v.id} value={v.id}>After {v.id}</option>)}
- <option value="end">End</option>
- </select>
- </div>
- {/* Preview */}
- <div style={{fontSize:10,opacity:.4,alignSelf:"center"}}>
- → {bulkVial.prefix||"V"}{parseInt(bulkVial.startNum)||vials.length+1} … {bulkVial.prefix||"V"}{(parseInt(bulkVial.startNum)||vials.length+1)+(parseInt(bulkVial.count)||1)-1}
- </div>
- <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>
- <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>
- </div>
- )}
- <div style={{ marginTop:8, fontSize:10, opacity:.2, textAlign:"right" }}>Drag to reorder · order determines draw sequence</div>
- </div>
- );
- }
- function SupplyView({ doses, vials, onUpdate }) {
- const today = new Date().toISOString().slice(0, 10);
- const schedule = useMemo(() => assignVials(doses, vials), [doses, vials]);
- const plannedVialSummary = useMemo(() => {
- const map = {};
- vials.forEach(v => { map[v.id] = { size:v.size, planned:0, done:0, first:null, last:null }; });
- schedule.forEach(d => {
- d.pulls.forEach(p => {
- if (!map[p.vialId]) return;
- map[p.vialId].planned += p.amt;
- if (d.done) map[p.vialId].done += p.amt;
- if (!map[p.vialId].first) map[p.vialId].first = d.date;
- map[p.vialId].last = d.date;
- });
- });
- return vials.map(v => ({
- ...v,
- planned: Math.round((map[v.id]?.planned||0)*100)/100,
- done: Math.round((map[v.id]?.done||0)*100)/100,
- leftover: Math.round((v.size-(map[v.id]?.planned||0))*100)/100,
- first: map[v.id]?.first,
- last: map[v.id]?.last,
- })).filter(v => v.planned > 0);
- }, [schedule, vials]);
- const bridgeDoses = useMemo(() => schedule.filter(d => d.pulls.length > 1), [schedule]);
- const activeVial = plannedVialSummary.find(v => v.done < v.planned);
- return (
- <div>
- <div style={{ marginBottom:20 }}>
- <div style={{ fontSize:10, opacity:.4, letterSpacing:"0.18em", textTransform:"uppercase", marginBottom:10 }}>
- ⚠ Multi-Vial Doses — bring both vials on these dates
- </div>
- {bridgeDoses.length === 0 && <div style={{fontSize:12,opacity:.4}}>None — all doses draw from a single vial.</div>}
- <div style={{ display:"flex", flexDirection:"column", gap:3 }}>
- {bridgeDoses.map((dose, bi) => {
- const isPast = dose.date < today;
- return (
- <div key={bi} style={{
- display:"grid", gridTemplateColumns:"100px 52px 1fr 28px",
- gap:10, padding:"7px 12px", borderRadius:6,
- background: dose.done?"transparent": isPast?"rgba(251,191,36,0.03)":"rgba(251,191,36,0.07)",
- border: dose.done?"1px solid #1f2937":"1px solid rgba(251,191,36,0.2)",
- opacity: dose.done?.35:1,
- }}>
- <div style={{fontSize:12,opacity:.8}}>{dose.date}</div>
- <div style={{fontSize:12,color:"#facc15",fontWeight:500}}>{dose.mg}mg</div>
- <div style={{fontSize:11,opacity:.6}}>
- {dose.pulls.map((p,pi)=>(
- <span key={pi}>
- {pi>0&&<span style={{opacity:.35,margin:"0 5px"}}>+</span>}
- <span style={{color:"#60a5fa"}}>{p.vialId}</span>
- <span style={{opacity:.5}}> {p.amt}mg</span>
- </span>
- ))}
- </div>
- <button className="icon-btn" style={{color:dose.done?"#4ade80":"#374151",fontSize:12}} onClick={()=>onUpdate(dose.id,{done:!dose.done})}>
- {dose.done?"✓":"○"}
- </button>
- </div>
- );
- })}
- </div>
- </div>
- <div style={{ fontSize:10, opacity:.4, letterSpacing:"0.18em", textTransform:"uppercase", marginBottom:10 }}>Full Vial Plan</div>
- <div style={{ display:"flex", flexDirection:"column", gap:5 }}>
- {plannedVialSummary.map(v => {
- const isActive = activeVial && v.id === activeVial.id;
- const isDone = v.done >= v.planned;
- const donePct = Math.min((v.done/v.size)*100,100);
- const plannedPct = Math.min((v.planned/v.size)*100,100);
- return (
- <div key={v.id} style={{
- background: isActive?"rgba(30,64,175,0.1)":"#111827",
- border: isActive?"1px solid rgba(59,130,246,0.3)":"1px solid #1f2937",
- borderRadius:8, padding:"10px 14px", opacity:isDone?.45:1,
- }}>
- <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:6 }}>
- <div style={{ display:"flex", gap:10, alignItems:"center" }}>
- <span style={{fontFamily:"'Syne',sans-serif",fontWeight:700,fontSize:13}}>{v.id}</span>
- <span style={{fontSize:10,opacity:.35}}>{v.size}mg</span>
- {isActive && <span style={{fontSize:9,color:"#60a5fa",letterSpacing:"0.1em",textTransform:"uppercase"}}>ACTIVE</span>}
- {isDone && <span style={{fontSize:9,color:"#6b7280",letterSpacing:"0.1em",textTransform:"uppercase"}}>DONE</span>}
- </div>
- <div style={{fontSize:10,opacity:.5}}>{v.first} → {v.last}</div>
- </div>
- <div style={{height:4,background:"#1f2937",borderRadius:2,overflow:"hidden",position:"relative"}}>
- <div style={{position:"absolute",left:0,top:0,height:"100%",width:`${plannedPct}%`,background:isDone?"#374151":"rgba(59,130,246,0.22)",borderRadius:2}}/>
- <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}}/>
- </div>
- <div style={{display:"flex",justifyContent:"space-between",marginTop:4,fontSize:10,opacity:.4}}>
- <span>{v.done}mg drawn</span>
- <span>{v.planned}mg planned · {v.leftover>=0.01?`${v.leftover}mg leftover`:"fully used"}</span>
- </div>
- </div>
- );
- })}
- </div>
- <div style={{marginTop:8,fontSize:10,opacity:.2,textAlign:"right"}}>Bright = drawn · Faint = planned · Arrow = date range</div>
- </div>
- );
- }
- // ── main App ──────────────────────────────────────────────────────────────────
- const STORAGE_KEY_DOSES = "reta_doses_v1";
- const STORAGE_KEY_VIALS = "reta_vials_v1";
- function sanitizeDoses(doses) {
- let maxId = Math.max(0, ...doses.filter(d => typeof d.id === 'number' && isFinite(d.id)).map(d => d.id));
- return doses.map(d => (typeof d.id !== 'number' || !isFinite(d.id)) ? { ...d, id: ++maxId } : d);
- }
- function loadDoses() {
- try {
- const raw = localStorage.getItem(STORAGE_KEY_DOSES);
- if (raw) return sanitizeDoses(JSON.parse(raw));
- } catch(e) {}
- return buildInitialDoses();
- }
- function loadVials() {
- try {
- const raw = localStorage.getItem(STORAGE_KEY_VIALS);
- if (raw) return JSON.parse(raw);
- } catch(e) {}
- return buildInitialVials();
- }
- function saveDoses(doses) {
- try { localStorage.setItem(STORAGE_KEY_DOSES, JSON.stringify(doses)); } catch(e) {}
- }
- function saveVials(vials) {
- try { localStorage.setItem(STORAGE_KEY_VIALS, JSON.stringify(vials)); } catch(e) {}
- }
- function sortedByDate(doses) {
- return [...doses].sort((a, b) => a.date < b.date ? -1 : a.date > b.date ? 1 : 0);
- }
- function App() {
- const [doses, setDoses] = useState(() => loadDoses());
- const [vials, setVials] = useState(() => loadVials());
- const [view, setView] = useState("schedule");
- const nextIdRef = useRef((() => {
- const saved = loadDoses();
- return saved.length ? Math.max(...saved.map(d => d.id)) + 1 : 200;
- })());
- const today = new Date().toISOString().slice(0, 10);
- // persist whenever doses or vials change
- const setDosesAndSave = useCallback((updater) => {
- setDoses(prev => {
- const next = sortedByDate(typeof updater === "function" ? updater(prev) : updater);
- saveDoses(next);
- return next;
- });
- }, []);
- const setVialsAndSave = useCallback((updater) => {
- setVials(prev => {
- const next = typeof updater === "function" ? updater(prev) : updater;
- saveVials(next);
- return next;
- });
- }, []);
- const updateDose = useCallback((id, patch) => {
- setDosesAndSave(ds => ds.map(d => d.id === id ? { ...d, ...patch } : d));
- }, [setDosesAndSave]);
- const deleteDose = useCallback((id) => {
- setDosesAndSave(ds => ds.filter(d => d.id !== id));
- }, [setDosesAndSave]);
- const deleteManyDoses = useCallback((ids) => {
- const idSet = new Set(ids);
- setDosesAndSave(ds => ds.filter(d => !idSet.has(d.id)));
- }, [setDosesAndSave]);
- const addOneDose = useCallback((afterDose) => {
- const newDate = afterDose ? addDays(afterDose.date, 7) : today;
- const newMg = afterDose ? afterDose.mg : 8.0;
- setDosesAndSave(ds => [...ds, { id: nextIdRef.current++, date: newDate, mg: newMg, done: false }]);
- }, [today, setDosesAndSave]);
- const addBulkDoses = useCallback((startDate, count, mg, intervalDays) => {
- let d = startDate || today;
- const newDoses = [];
- for (let i = 0; i < count; i++) {
- newDoses.push({ id: nextIdRef.current++, date: d, mg, done: false });
- d = addDays(d, intervalDays);
- }
- setDosesAndSave(ds => [...ds, ...newDoses]);
- }, [today, setDosesAndSave]);
- // stats
- const schedule = useMemo(() => assignVials(doses, vials), [doses, vials]);
- const totalMg = doses.reduce((s, d) => s + d.mg, 0);
- const doneMg = doses.reduce((s, d) => s + (d.done ? d.mg : 0), 0);
- const completedCount = doses.filter(d => d.done).length;
- const nextDose = schedule.find(d => !d.done);
- const totalSupply = vials.reduce((s, v) => s + v.size, 0);
- const remainingSupply = totalSupply - doneMg;
- return (
- <div style={{ minHeight:"100vh", background:"#0a0e1a", color:"#e2e8f0", fontFamily:"'DM Mono','Courier New',monospace", padding:"24px 16px" }}>
- <div style={{ maxWidth:700, margin:"0 auto" }}>
- {/* Header */}
- <div style={{ marginBottom:6, fontSize:10, opacity:.3, letterSpacing:"0.2em", textTransform:"uppercase" }}>Retatrutide Protocol</div>
- <div style={{ fontFamily:"'Syne',sans-serif", fontSize:26, fontWeight:800, letterSpacing:"-0.02em", marginBottom:18 }}>Dosing Tracker</div>
- {/* Stats */}
- <div style={{ display:"flex", gap:12, marginBottom:20, flexWrap:"wrap" }}>
- <StatCard label="Total Doses" value={doses.length} />
- <StatCard label="Completed" value={completedCount} />
- <StatCard label="Supply Used" value={`${+doneMg.toFixed(1)}mg`} />
- <StatCard label="Supply Left" value={`${+remainingSupply.toFixed(1)}mg`} />
- </div>
- {/* Next dose banner */}
- {nextDose && (
- <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 }}>
- <div style={{fontSize:18}}>💉</div>
- <div>
- <div style={{fontSize:10,opacity:.5,textTransform:"uppercase",letterSpacing:"0.14em",marginBottom:3}}>Next Dose</div>
- <div style={{fontSize:13,fontWeight:500,display:"flex",flexWrap:"wrap",gap:4,alignItems:"center"}}>
- <span style={{color:"#60a5fa"}}>{nextDose.date}</span>
- <span style={{opacity:.3}}>·</span>
- <span style={{color:PHASE_COLORS[getPhase(nextDose.mg)]}}>{nextDose.mg}mg</span>
- <span style={{opacity:.3}}>·</span>
- <span style={{opacity:.55,fontSize:11}}>
- {nextDose.pulls.length ? nextDose.pulls.map(p=>`${p.vialId}: ${p.amt}mg`).join(" + ") : "⚠ no supply"}
- </span>
- </div>
- </div>
- </div>
- )}
- {/* Tabs */}
- <div style={{ display:"inline-flex", gap:3, marginBottom:18, background:"#111827", borderRadius:8, padding:4 }}>
- {["schedule","vials","supply"].map(t => (
- <button key={t} className="tab-btn" onClick={()=>setView(t)} style={{
- padding:"6px 18px", borderRadius:6, fontSize:11, letterSpacing:"0.1em",
- textTransform:"uppercase", fontWeight:500,
- background: view===t?"#1e40af":"transparent",
- color: view===t?"#e2e8f0":"#6b7280",
- }}>{t}</button>
- ))}
- </div>
- {view==="schedule" && <ScheduleView doses={doses} vials={vials} onUpdate={updateDose} onDelete={deleteDose} onDeleteMany={deleteManyDoses} onAdd={addOneDose} onAddBulk={addBulkDoses} />}
- {view==="vials" && <VialsView doses={doses} vials={vials} setVials={setVialsAndSave} />}
- {view==="supply" && <SupplyView doses={doses} vials={vials} onUpdate={updateDose} />}
- <div style={{marginTop:32,fontSize:9,opacity:.15,textAlign:"center",letterSpacing:"0.12em",textTransform:"uppercase",display:"flex",justifyContent:"center",gap:16,alignItems:"center"}}>
- <span>For personal tracking use only · Data saved in browser</span>
- <span style={{opacity:.5}}>·</span>
- <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>
- </div>
- </div>
- </div>
- );
- }
- ReactDOM.createRoot(document.getElementById("root")).render(<App />);
- </script>
- </body>
- </html>
Advertisement
Comments
-
- 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