A second channel to the same dialog engine as the Telegram bot: consent, onboarding, scales and check-ins come from app/dialog; the bot is unchanged. - API: /auth/emias (doctor's one-time connect code via EmiasMock), /auth/emias/demo (synthetic personas), /auth/anonymous (random token, nothing stored in the identity DB), /crisis/resources from crisis.yaml - web/: React + TS + Vite, mobile-first, safe areas, hash routing, tokens and texts isolated for a Capacitor mobile build Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
4.8 KiB
TypeScript
146 lines
4.8 KiB
TypeScript
import { useNavigate } from "react-router-dom";
|
|
|
|
import { AppShell } from "../components/AppShell";
|
|
import { Icon } from "../components/Icon";
|
|
import { ru } from "../content/ru";
|
|
|
|
const SCALE = [1, 2, 3, 4, 5] as const;
|
|
const WEEK = [38, 52, 46, 60, 58, 72, 80]; // illustrative trend, %
|
|
|
|
function Facts({ items }: { items: readonly { ok: boolean; text: string }[] }) {
|
|
return (
|
|
<ul className="facts">
|
|
{items.map((f) => (
|
|
<li key={f.text} className={f.ok ? "fact" : "fact fact--no"}>
|
|
<Icon name={f.ok ? "check" : "minus"} size={18} />
|
|
<span>{f.text}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
// Illustration of a day with the service (decorative: the same facts are stated in text).
|
|
function DayPreview() {
|
|
const d = ru.welcome.day;
|
|
return (
|
|
<figure className="day" aria-hidden="true">
|
|
<figcaption className="day__caption">{d.caption}</figcaption>
|
|
<ol className="day__list">
|
|
<li className="day__item day__item--meds">
|
|
<span className="day__time">{d.meds.time}</span>
|
|
<div className="day__card">
|
|
<span className="day__track">
|
|
<Icon name="pill" size={16} />
|
|
{d.meds.track}
|
|
</span>
|
|
<div className="day__row">
|
|
<span className="day__text">{d.meds.text}</span>
|
|
<span className="day__done">
|
|
<Icon name="check" size={16} />
|
|
{d.meds.done}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<li className="day__item day__item--mood">
|
|
<span className="day__time">{d.mood.time}</span>
|
|
<div className="day__card">
|
|
<span className="day__track">
|
|
<Icon name="pulse" size={16} />
|
|
{d.mood.track}
|
|
</span>
|
|
<span className="day__text">{d.mood.text}</span>
|
|
<div className="scale">
|
|
{SCALE.map((n) => (
|
|
<span key={n} className={`scale__dot scale__dot--${n}${n === 4 ? " is-picked" : ""}`}>
|
|
{n}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<div className="scale__ends">
|
|
<span>{d.mood.low}</span>
|
|
<span>{d.mood.high}</span>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
<li className="day__item day__item--doctor">
|
|
<span className="day__time">{d.doctor.time}</span>
|
|
<div className="day__card">
|
|
<span className="day__track">
|
|
<Icon name="doctor" size={16} />
|
|
{d.doctor.track}
|
|
</span>
|
|
<div className="day__row">
|
|
<span className="day__text">{d.doctor.text}</span>
|
|
<span className="spark">
|
|
{WEEK.map((v, i) => (
|
|
<i key={i} style={{ height: `${v}%` }} />
|
|
))}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ol>
|
|
</figure>
|
|
);
|
|
}
|
|
|
|
export function Welcome() {
|
|
const navigate = useNavigate();
|
|
const t = ru.welcome;
|
|
|
|
return (
|
|
<AppShell>
|
|
<main className="welcome" id="main">
|
|
<div className="welcome__inner">
|
|
<section className="welcome__intro" aria-labelledby="welcome-title">
|
|
<h1 id="welcome-title" className="display">
|
|
{t.title}
|
|
</h1>
|
|
<p className="lead">{t.lead}</p>
|
|
</section>
|
|
|
|
<section className="signin" aria-labelledby="choice-title">
|
|
<h2 id="choice-title" className="signin__title">
|
|
{t.choose}
|
|
</h2>
|
|
|
|
<article className="option option--emias">
|
|
<div className="option__head">
|
|
<span className="option__icon">
|
|
<Icon name="doctor" size={22} />
|
|
</span>
|
|
<h3 className="option__title">{t.emias.title}</h3>
|
|
<span className="badge">{t.emias.badge}</span>
|
|
</div>
|
|
<Facts items={t.emias.facts} />
|
|
<button type="button" className="btn btn--primary btn--block" onClick={() => navigate("/emias")}>
|
|
{t.emias.action}
|
|
</button>
|
|
</article>
|
|
|
|
<article className="option option--anon">
|
|
<div className="option__head">
|
|
<span className="option__icon">
|
|
<Icon name="lock" size={22} />
|
|
</span>
|
|
<h3 className="option__title">{t.anonymous.title}</h3>
|
|
</div>
|
|
<Facts items={t.anonymous.facts} />
|
|
<button type="button" className="btn btn--secondary btn--block" onClick={() => navigate("/anonymous")}>
|
|
{t.anonymous.action}
|
|
</button>
|
|
</article>
|
|
</section>
|
|
|
|
<div className="welcome__aside">
|
|
<DayPreview />
|
|
<p className="disclaimer">{t.disclaimer}</p>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</AppShell>
|
|
);
|
|
}
|