Add Medassist demo dashboard UI
This commit is contained in:
+277
-7
@@ -1,8 +1,28 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
const healthSchema = z.object({ status: z.string() });
|
||||
|
||||
type Medication = {
|
||||
id: string;
|
||||
name: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
timeOfDay: string;
|
||||
startDate: string;
|
||||
notes?: string;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
type DoseEvent = {
|
||||
id: string;
|
||||
medId: string;
|
||||
medName: string;
|
||||
time: Date;
|
||||
notes?: string;
|
||||
};
|
||||
|
||||
// Simple backend health check
|
||||
function useHealth() {
|
||||
const [status, setStatus] = useState<string>("loading");
|
||||
useEffect(() => {
|
||||
@@ -18,15 +38,265 @@ function useHealth() {
|
||||
return status;
|
||||
}
|
||||
|
||||
const starterMeds: Medication[] = [
|
||||
{
|
||||
id: "med-1",
|
||||
name: "Lisinopril",
|
||||
dosage: "10 mg",
|
||||
frequency: "Daily",
|
||||
timeOfDay: "08:00",
|
||||
startDate: "2025-11-28",
|
||||
notes: "Blood pressure",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: "med-2",
|
||||
name: "Metformin",
|
||||
dosage: "500 mg",
|
||||
frequency: "2x daily",
|
||||
timeOfDay: "08:00",
|
||||
startDate: "2025-12-01",
|
||||
notes: "Before meals",
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: "med-3",
|
||||
name: "Vitamin D",
|
||||
dosage: "2000 IU",
|
||||
frequency: "Weekly",
|
||||
timeOfDay: "09:00",
|
||||
startDate: "2025-12-05",
|
||||
notes: "Sunday dose",
|
||||
active: false,
|
||||
},
|
||||
];
|
||||
|
||||
const frequencies = ["Daily", "2x daily", "Weekly", "As needed"] as const;
|
||||
|
||||
export default function App() {
|
||||
const status = useHealth();
|
||||
const [meds, setMeds] = useState<Medication[]>(starterMeds);
|
||||
const [form, setForm] = useState({
|
||||
name: "",
|
||||
dosage: "",
|
||||
frequency: "Daily",
|
||||
timeOfDay: "08:00",
|
||||
notes: "",
|
||||
});
|
||||
|
||||
const activeCount = meds.filter((m) => m.active).length;
|
||||
|
||||
const schedule: DoseEvent[] = useMemo(() => {
|
||||
const daysAhead = 3;
|
||||
const events: DoseEvent[] = [];
|
||||
const now = new Date();
|
||||
for (let d = 0; d <= daysAhead; d++) {
|
||||
const base = new Date(now);
|
||||
base.setDate(now.getDate() + d);
|
||||
meds
|
||||
.filter((m) => m.active)
|
||||
.forEach((m) => {
|
||||
if (m.frequency === "As needed") return;
|
||||
const [h, min] = m.timeOfDay.split(":").map(Number);
|
||||
const when = new Date(base);
|
||||
when.setHours(h ?? 8, min ?? 0, 0, 0);
|
||||
events.push({
|
||||
id: `${m.id}-${when.toISOString()}`,
|
||||
medId: m.id,
|
||||
medName: m.name,
|
||||
time: when,
|
||||
notes: m.notes,
|
||||
});
|
||||
if (m.frequency === "2x daily") {
|
||||
const evening = new Date(when);
|
||||
evening.setHours(20, 0, 0, 0);
|
||||
events.push({
|
||||
id: `${m.id}-${evening.toISOString()}`,
|
||||
medId: m.id,
|
||||
medName: m.name,
|
||||
time: evening,
|
||||
notes: m.notes,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return events.sort((a, b) => a.time.getTime() - b.time.getTime());
|
||||
}, [meds]);
|
||||
|
||||
const handleAdd = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!form.name.trim() || !form.dosage.trim()) return;
|
||||
const newMed: Medication = {
|
||||
id: crypto.randomUUID(),
|
||||
name: form.name.trim(),
|
||||
dosage: form.dosage.trim(),
|
||||
frequency: form.frequency,
|
||||
timeOfDay: form.timeOfDay,
|
||||
startDate: new Date().toISOString().slice(0, 10),
|
||||
notes: form.notes.trim() || undefined,
|
||||
active: true,
|
||||
};
|
||||
setMeds((prev) => [newMed, ...prev]);
|
||||
setForm({ name: "", dosage: "", frequency: "Daily", timeOfDay: "08:00", notes: "" });
|
||||
};
|
||||
|
||||
const toggleActive = (id: string) => {
|
||||
setMeds((prev) => prev.map((m) => (m.id === id ? { ...m, active: !m.active } : m)));
|
||||
};
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="card">
|
||||
<h1>Medassist (Rebuild)</h1>
|
||||
<p>Backend health: {status}</p>
|
||||
<p>Frontend scaffold ready. Auth & CRUD folgen.</p>
|
||||
</div>
|
||||
<main className="page">
|
||||
<header className="hero">
|
||||
<div>
|
||||
<p className="eyebrow">Medassist · Rebuild Preview</p>
|
||||
<h1>Deine Medikationszentrale</h1>
|
||||
<p className="sub">Schneller Überblick über Medikamente, Einnahmeplan und Erinnerungen.</p>
|
||||
<div className="badges">
|
||||
<span className="pill success">Backend: {status}</span>
|
||||
<span className="pill">Aktive Medikamente: {activeCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="stats">
|
||||
<div className="stat">
|
||||
<p className="label">Heute geplant</p>
|
||||
<p className="value">{schedule.filter((e) => isToday(e.time)).length}</p>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<p className="label">Diese Woche</p>
|
||||
<p className="value">{schedule.length}</p>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<p className="label">Ohne Login</p>
|
||||
<p className="value">Demo</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="grid">
|
||||
<article className="card meds">
|
||||
<div className="card-head">
|
||||
<h2>Medikamentenliste</h2>
|
||||
<span className="pill">{meds.length} gesamt</span>
|
||||
</div>
|
||||
<div className="med-list">
|
||||
{meds.map((med) => (
|
||||
<div key={med.id} className="med-row">
|
||||
<div>
|
||||
<div className="med-name">{med.name}</div>
|
||||
<div className="muted">{med.dosage} · {med.frequency} · Start {med.startDate}</div>
|
||||
{med.notes && <div className="tag">{med.notes}</div>}
|
||||
</div>
|
||||
<div className="med-actions">
|
||||
<span className={`pill ${med.active ? "success" : "neutral"}`}>
|
||||
{med.active ? "aktiv" : "pausiert"}
|
||||
</span>
|
||||
<button className="ghost" onClick={() => toggleActive(med.id)}>
|
||||
{med.active ? "Pausieren" : "Aktivieren"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="card form">
|
||||
<div className="card-head">
|
||||
<h2>Neues Medikament</h2>
|
||||
<span className="pill">ohne Login</span>
|
||||
</div>
|
||||
<form className="form-grid" onSubmit={handleAdd}>
|
||||
<label>
|
||||
Name
|
||||
<input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} placeholder="z.B. Ibuprofen" />
|
||||
</label>
|
||||
<label>
|
||||
Dosierung
|
||||
<input value={form.dosage} onChange={(e) => setForm({ ...form, dosage: e.target.value })} placeholder="z.B. 400 mg" />
|
||||
</label>
|
||||
<label>
|
||||
Einnahmezeit
|
||||
<input type="time" value={form.timeOfDay} onChange={(e) => setForm({ ...form, timeOfDay: e.target.value })} />
|
||||
</label>
|
||||
<label>
|
||||
Frequenz
|
||||
<select value={form.frequency} onChange={(e) => setForm({ ...form, frequency: e.target.value })}>
|
||||
{frequencies.map((f) => (
|
||||
<option key={f} value={f}>
|
||||
{f}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="full">
|
||||
Notizen
|
||||
<input value={form.notes} onChange={(e) => setForm({ ...form, notes: e.target.value })} placeholder="Erinnerung oder Kontext" />
|
||||
</label>
|
||||
<div className="full align-end">
|
||||
<button type="submit">Hinzufügen</button>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="grid">
|
||||
<article className="card">
|
||||
<div className="card-head">
|
||||
<h2>Nächste Einnahmen</h2>
|
||||
<span className="pill">3 Tage Vorschau</span>
|
||||
</div>
|
||||
<div className="timeline">
|
||||
{schedule.slice(0, 8).map((event) => (
|
||||
<div key={event.id} className="time-row">
|
||||
<div className="time-chip">{formatTime(event.time)}</div>
|
||||
<div>
|
||||
<div className="med-name">{event.medName}</div>
|
||||
<div className="muted">{formatDate(event.time)}</div>
|
||||
{event.notes && <div className="tag subtle">{event.notes}</div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article className="card">
|
||||
<div className="card-head">
|
||||
<h2>Highlights</h2>
|
||||
<span className="pill neutral">Demo-Daten</span>
|
||||
</div>
|
||||
<div className="highlights">
|
||||
<div>
|
||||
<p className="label">Aktive Pläne</p>
|
||||
<p className="value">{activeCount}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="label">Starttermine</p>
|
||||
<p className="value">{meds.filter((m) => m.startDate).length}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="label">Backend Health</p>
|
||||
<p className="value">{status}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="muted small">Login und Synchronisation kommen später – dies ist ein sichtbarer Preview.</p>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function isToday(date: Date) {
|
||||
const now = new Date();
|
||||
return (
|
||||
date.getFullYear() === now.getFullYear() &&
|
||||
date.getMonth() === now.getMonth() &&
|
||||
date.getDate() === now.getDate()
|
||||
);
|
||||
}
|
||||
|
||||
function formatTime(date: Date) {
|
||||
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
function formatDate(date: Date) {
|
||||
return date.toLocaleDateString([], { weekday: "short", day: "2-digit", month: "short" });
|
||||
}
|
||||
|
||||
+86
-16
@@ -1,39 +1,109 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap");
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
background: #0f1115;
|
||||
font-family: "Space Grotesk", "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
background: radial-gradient(circle at 20% 20%, #1a2440, #0b1220 40%), #0b1220;
|
||||
color: #e5e7eb;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 960px;
|
||||
.page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
padding: 2.5rem 1.5rem 3rem;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: linear-gradient(135deg, rgba(67, 106, 255, 0.12), rgba(115, 195, 255, 0.1));
|
||||
border: 1px solid rgba(73, 117, 255, 0.25);
|
||||
border-radius: 20px;
|
||||
padding: 1.6rem;
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 1.5rem;
|
||||
box-shadow: 0 18px 45px rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hero h1 { margin: 0.2rem 0 0.4rem; font-size: 2rem; }
|
||||
.sub { color: #b7c2e5; margin: 0; }
|
||||
.eyebrow { letter-spacing: 0.08em; text-transform: uppercase; color: #7ca7ff; font-size: 0.8rem; margin: 0; }
|
||||
|
||||
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; align-items: center; }
|
||||
.stat { background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 14px; padding: 0.9rem; }
|
||||
.stat .label { margin: 0; color: #b7c2e5; font-size: 0.9rem; }
|
||||
.stat .value { margin: 0.25rem 0 0; font-size: 1.4rem; font-weight: 700; }
|
||||
|
||||
.grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); margin-bottom: 1rem; }
|
||||
|
||||
.card {
|
||||
background: #1b1f2a;
|
||||
border: 1px solid #252b3b;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 12px 30px rgba(0,0,0,0.25);
|
||||
background: #111827;
|
||||
border: 1px solid #1f2a3d;
|
||||
border-radius: 14px;
|
||||
padding: 1.25rem;
|
||||
box-shadow: 0 14px 36px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.card-head { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; margin-bottom: 1rem; }
|
||||
.card h2 { margin: 0; font-size: 1.2rem; }
|
||||
|
||||
.pill { border: 1px solid #2f86f6; color: #dceaff; background: rgba(47, 134, 246, 0.1); padding: 0.35rem 0.7rem; border-radius: 999px; font-size: 0.85rem; }
|
||||
.pill.success { border-color: #39d98a; background: rgba(57, 217, 138, 0.12); color: #c6f4dc; }
|
||||
.pill.neutral { border-color: #4b5565; background: rgba(255, 255, 255, 0.04); color: #cbd5f5; }
|
||||
|
||||
.badges { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.6rem; }
|
||||
|
||||
.meds .med-list { display: flex; flex-direction: column; gap: 0.9rem; }
|
||||
.med-row { display: flex; justify-content: space-between; gap: 1rem; border: 1px solid #1f2a3d; padding: 0.9rem; border-radius: 12px; background: #0d1424; }
|
||||
.med-name { font-weight: 600; }
|
||||
.muted { color: #a3adc2; font-size: 0.95rem; }
|
||||
.small { font-size: 0.9rem; }
|
||||
|
||||
.tag { display: inline-block; background: rgba(255, 255, 255, 0.06); border-radius: 10px; padding: 0.2rem 0.55rem; margin-top: 0.35rem; color: #dce3f5; font-size: 0.85rem; }
|
||||
.tag.subtle { background: rgba(255, 255, 255, 0.04); color: #c1cbe0; }
|
||||
|
||||
.med-actions { display: flex; align-items: center; gap: 0.5rem; }
|
||||
|
||||
button {
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 8px;
|
||||
padding: 0.65rem 1rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2f86f6;
|
||||
background: #2f86f6;
|
||||
background: linear-gradient(135deg, #2f86f6, #3fa9f5);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: transform 120ms ease, box-shadow 120ms ease;
|
||||
}
|
||||
button:hover { transform: translateY(-1px); box-shadow: 0 10px 25px rgba(47, 134, 246, 0.35); }
|
||||
button.ghost { background: transparent; border-color: #3a475f; color: #d0d8ec; box-shadow: none; }
|
||||
button.ghost:hover { background: rgba(255, 255, 255, 0.05); transform: none; }
|
||||
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 0.65rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #30384a;
|
||||
background: #111521;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #1f2a3d;
|
||||
background: #0d1322;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 0.75rem 1rem; }
|
||||
.form-grid label { display: flex; flex-direction: column; gap: 0.35rem; color: #cfd8f1; font-weight: 500; }
|
||||
.form-grid .full { grid-column: 1 / -1; }
|
||||
.align-end { display: flex; justify-content: flex-end; }
|
||||
|
||||
.timeline { display: flex; flex-direction: column; gap: 0.85rem; }
|
||||
.time-row { display: flex; gap: 0.9rem; align-items: center; border-bottom: 1px solid #1f2a3d; padding-bottom: 0.75rem; }
|
||||
.time-row:last-child { border-bottom: none; padding-bottom: 0; }
|
||||
.time-chip { min-width: 80px; text-align: center; border: 1px solid #2f86f6; border-radius: 10px; padding: 0.45rem 0.6rem; background: rgba(47, 134, 246, 0.1); color: #dceaff; font-weight: 600; }
|
||||
|
||||
.highlights { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.75rem; margin-bottom: 0.75rem; }
|
||||
|
||||
.card p { margin: 0; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero { grid-template-columns: 1fr; }
|
||||
.stats { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user