feat(web): full desktop layout instead of a stretched phone view

- focus screens: question and context on the left, answers on the right in
  one wide card; consent text and the 0–10 scale in one wide column
- today: dashboard with tasks, this week's doses and quick actions side by
  side; meds / doctor / profile in two-column grids; welcome gets «Как это
  работает» steps
- fix: 0–10 scale no longer overflows narrow containers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 08:25:17 +03:00
co-authored by Claude Opus 5.5
parent f233c1e5ca
commit 40c242bb1a
10 changed files with 558 additions and 192 deletions
+165 -103
View File
@@ -1,12 +1,13 @@
import { useNavigate } from "react-router-dom";
import type { TodayDose } from "../api/types";
import type { TodayDose, TodayPrescription } from "../api/types";
import { AppShell, Notice } from "../components/AppShell";
import { Icon } from "../components/Icon";
import { ru } from "../content/ru";
import { useDialog } from "../dialog/DialogContext";
import { classify } from "../dialog/step";
import { useSession } from "../session/SessionContext";
import { WeekDots } from "./Meds";
import { humanDate, useToday } from "./useToday";
function VisitCard({ date, onOpen }: { date: string; onOpen: () => void }) {
@@ -24,6 +25,26 @@ function VisitCard({ date, onOpen }: { date: string; onOpen: () => void }) {
);
}
function WeekCard({ rx, onOpen }: { rx: TodayPrescription; onOpen: () => void }) {
const taken = rx.week.filter((w) => w.status === "taken").length;
const counted = rx.week.filter((w) => w.status !== "none").length;
return (
<button type="button" className="card weekcard" onClick={onOpen}>
<span className="card__head">
<span className="tile-icon tile-icon--teal">
<Icon name="pill" />
</span>
<span className="row-card__text">
<span className="row-card__sub">{ru.today.week}</span>
<span className="row-card__title">{ru.today.weekTaken(taken, counted)}</span>
</span>
<Icon name="chevronRight" />
</span>
<WeekDots week={rx.week} />
</button>
);
}
// C1 / C2 · Сегодня
export function Today() {
const t = ru.today;
@@ -43,20 +64,27 @@ export function Today() {
const takenDoses = plan?.doses.filter((d) => d.status === "taken") ?? [];
const allDone = plan !== null && pendingDoses.length === 0 && plan.checkin_done && !hasOpenStep;
const nextDose = plan?.prescriptions.flatMap((p) => p.times).sort()[0];
const noDoctor = session?.mode === "anonymous" || (plan !== null && !plan.has_doctor);
const now = plan?.now ?? "00:00";
const openDose = (d: TodayDose) => navigate(`/dose/${d.id}`);
const visit = plan?.next_visit ? <VisitCard date={plan.next_visit} onOpen={() => navigate("/doctor")} /> : null;
const hasDone = takenDoses.length > 0 || plan?.checkin_done;
return (
<AppShell aside={visit}>
<div className="stack">
<header className="today-head">
<div className="dash">
<header className="today-head dash__head">
<div>
<h1 className="h1">{t.title}</h1>
{plan && <p className="muted">{humanDate(plan.today)}</p>}
</div>
<button type="button" className="icon-btn icon-btn--bell" onClick={() => navigate("/messages")} aria-label={ru.nav.messages}>
<button
type="button"
className="icon-btn icon-btn--bell"
onClick={() => navigate("/messages")}
aria-label={ru.nav.messages}
>
<Icon name="bell" />
{dialog.unread > 0 && <span className="dot" />}
</button>
@@ -64,113 +92,147 @@ export function Today() {
{failed && <Notice tone="danger">{ru.common.networkError}</Notice>}
{hasOpenStep && (
<button type="button" className="task task--hero" onClick={() => navigate("/flow")}>
<span className="task__icon">
<Icon name="message" />
</span>
<span className="task__text">
<span className="task__title">{t.newMessage}</span>
<span className="task__sub">{dialog.pending[0]?.text.split("\n")[0].slice(0, 80)}</span>
</span>
<span className="chip chip--teal-solid">{t.open}</span>
</button>
)}
<section className="dash__main" aria-label={t.title}>
{hasOpenStep && (
<button type="button" className="task task--hero" onClick={() => navigate("/flow")}>
<span className="task__icon">
<Icon name="message" />
</span>
<span className="task__text">
<span className="task__title">{t.newMessage}</span>
<span className="task__sub">{dialog.pending[0]?.text.split("\n")[0].slice(0, 90)}</span>
</span>
<span className="chip chip--teal-solid">{t.open}</span>
</button>
)}
{allDone ? (
<section className="alldone">
<span className="done__badge" aria-hidden="true">
<Icon name="check" size={36} />
</span>
<h2 className="h2 center">{t.allDoneTitle}</h2>
<p className="muted center">{nextDose ? t.allDoneNext(`завтра в ${nextDose}`) : t.allDoneNoMeds}</p>
{plan && plan.prescriptions.length > 0 && (
<button type="button" className="btn btn--primary" onClick={() => navigate("/meds")}>
{t.viewMeds}
<Icon name="chevronRight" size={20} />
</button>
)}
</section>
) : (
<div className="tasks">
{pendingDoses.map((d) => (
{allDone ? (
<div className="alldone">
<span className="done__badge" aria-hidden="true">
<Icon name="check" size={36} />
</span>
<h2 className="h2 center">{t.allDoneTitle}</h2>
<p className="muted center">{nextDose ? t.allDoneNext(`завтра в ${nextDose}`) : t.allDoneNoMeds}</p>
{plan && plan.prescriptions.length > 0 && (
<button type="button" className="btn btn--primary btn--auto" onClick={() => navigate("/meds")}>
{t.viewMeds}
<Icon name="chevronRight" size={20} />
</button>
)}
</div>
) : (
<div className="tasks">
{pendingDoses.map((d) => (
<button
key={d.id}
type="button"
className={d.time <= now && !hasOpenStep ? "task task--hero" : "task"}
onClick={() => openDose(d)}
>
<span className="task__icon task__icon--teal">
<Icon name="pill" />
</span>
<span className="task__text">
<span className="task__title">{t.takeMed}</span>
<span className="task__sub">
{d.time} · {d.drug}, {d.dose}
</span>
</span>
{d.time <= now && <span className="chip chip--teal-solid">{t.now}</span>}
<Icon name="chevronRight" />
</button>
))}
{plan && !plan.checkin_done && (
<button
type="button"
className="task"
onClick={() => go("menu:checkin", t.checkin)}
disabled={dialog.busy}
>
<span className="task__icon task__icon--teal">
<Icon name="smile" />
</span>
<span className="task__text">
<span className="task__title">{t.checkin}</span>
<span className="task__sub">{t.checkinHint}</span>
</span>
<Icon name="chevronRight" />
</button>
)}
</div>
)}
</section>
<section className="dash__side">
{plan?.next_visit && (
<div className="only-phone">
<VisitCard date={plan.next_visit} onOpen={() => navigate("/doctor")} />
</div>
)}
{!allDone && hasDone && (
<div className="card">
<h2 className="h3">{t.done}</h2>
{takenDoses.map((d) => (
<div key={d.id} className="done-row">
<span className="badge-icon badge-icon--teal">
<Icon name="check" size={16} />
</span>
<span>
<span className="done-row__title">{t.medTaken}</span>
<span className="done-row__sub">{t.medTakenAt(d.time)}</span>
</span>
</div>
))}
{plan?.checkin_done && (
<div className="done-row">
<span className="badge-icon badge-icon--teal">
<Icon name="check" size={16} />
</span>
<span className="done-row__title">{t.checkinDone}</span>
</div>
)}
</div>
)}
{plan?.prescriptions[0] && <WeekCard rx={plan.prescriptions[0]} onOpen={() => navigate("/meds")} />}
{noDoctor ? (
<Notice>{t.noDoctor}</Notice>
) : (
<div className="quick">
<button
key={d.id}
type="button"
className={d.time <= now && !hasOpenStep ? "task task--hero" : "task"}
onClick={() => openDose(d)}
className="quick__btn"
onClick={() => go("menu:med_question", t.medQuestion)}
disabled={dialog.busy}
>
<span className="task__icon task__icon--teal">
<Icon name="pill" />
</span>
<span className="task__text">
<span className="task__title">{t.takeMed}</span>
<span className="task__sub">{d.time}</span>
</span>
{d.time <= now && <span className="chip chip--teal-solid">{t.now}</span>}
<Icon name="chevronRight" />
</button>
))}
{plan && !plan.checkin_done && (
<button type="button" className="task" onClick={() => go("menu:checkin", t.checkin)} disabled={dialog.busy}>
<span className="task__icon task__icon--teal">
<Icon name="smile" />
</span>
<span className="task__text">
<span className="task__title">{t.checkin}</span>
<span className="task__sub">{t.checkinHint}</span>
</span>
<Icon name="chevronRight" />
</button>
)}
</div>
)}
{!allDone && (takenDoses.length > 0 || plan?.checkin_done) && (
<section className="stack stack--sm">
<h2 className="h3">{t.done}</h2>
{takenDoses.map((d) => (
<div key={d.id} className="done-row">
<span className="badge-icon badge-icon--teal">
<Icon name="check" size={16} />
<span className="tile-icon tile-icon--blue">
<Icon name="question" />
</span>
<span>
<span className="done-row__title">{t.medTaken}</span>
<span className="done-row__sub">{t.medTakenAt(d.time)}</span>
<span className="quick__title">{t.medQuestion}</span>
<span className="quick__sub">{t.medQuestionHint}</span>
</span>
</div>
))}
{plan?.checkin_done && (
<div className="done-row">
<span className="badge-icon badge-icon--teal">
<Icon name="check" size={16} />
</button>
<button
type="button"
className="quick__btn"
onClick={() => go("menu:contact_doctor", t.messageDoctor)}
disabled={dialog.busy}
>
<span className="tile-icon tile-icon--blue">
<Icon name="message" />
</span>
<span className="done-row__title">{t.checkinDone}</span>
</div>
)}
</section>
)}
{plan?.next_visit && (
<div className="only-phone">
<VisitCard date={plan.next_visit} onOpen={() => navigate("/doctor")} />
</div>
)}
{session?.mode === "anonymous" || (plan && !plan.has_doctor) ? (
<Notice>{t.noDoctor}</Notice>
) : (
<div className="quick">
<button type="button" className="quick__btn" onClick={() => go("menu:med_question", t.medQuestion)} disabled={dialog.busy}>
<Icon name="question" />
{t.medQuestion}
</button>
<button type="button" className="quick__btn" onClick={() => go("menu:contact_doctor", t.messageDoctor)} disabled={dialog.busy}>
<Icon name="message" />
{t.messageDoctor}
</button>
</div>
)}
<span>
<span className="quick__title">{t.messageDoctor}</span>
<span className="quick__sub">{t.messageDoctorHint}</span>
</span>
</button>
</div>
)}
</section>
</div>
</AppShell>
);