217 lines
6.8 KiB
TypeScript
217 lines
6.8 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { Link, NavLink } from "react-router-dom";
|
|
|
|
import { ru } from "../content/ru";
|
|
import { useDialog } from "../dialog/DialogContext";
|
|
import { useSession } from "../session/SessionContext";
|
|
import { HotlineCard, useHelp } from "./HelpSheet";
|
|
import { Icon, LogoMark, type IconName } from "./Icon";
|
|
|
|
/*
|
|
* Shared patient workspace, following the approved doctor dashboard.
|
|
* phone: content → «Экстренная помощь» block → bottom tab bar (signed in)
|
|
* desktop: white sidebar | workspace header | content + emergency panel
|
|
* bare: one centred column on every width (questionnaires)
|
|
*/
|
|
|
|
const TABS: { to: string; label: string; icon: IconName }[] = [
|
|
{ to: "/today", label: ru.nav.today, icon: "home" },
|
|
{ to: "/meds", label: ru.nav.meds, icon: "pill" },
|
|
{ to: "/doctor", label: ru.nav.doctor, icon: "doctor" },
|
|
{ to: "/profile", label: ru.nav.profile, icon: "user" },
|
|
];
|
|
|
|
/** Inline emergency block shown at the bottom of every phone screen (hidden on desktop: the rail has it). */
|
|
export function EmergencyBlock() {
|
|
const { resources, open } = useHelp();
|
|
return (
|
|
<section className="sosblock" aria-label={ru.help.block}>
|
|
<button type="button" className="sosblock__head" onClick={open}>
|
|
<span className="sosblock__icon">
|
|
<Icon name="phone" size={18} />
|
|
</span>
|
|
<span>{ru.help.block}</span>
|
|
<Icon name="chevronRight" size={20} />
|
|
</button>
|
|
<div className="sosblock__lines">
|
|
{resources.slice(0, 2).map((r) => (
|
|
<HotlineCard key={r.phone} r={r} size="sm" />
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function Sidebar({ signedIn }: { signedIn: boolean }) {
|
|
const { open } = useHelp();
|
|
const { unread } = useDialog();
|
|
return (
|
|
<nav className="sidebar" aria-label="Разделы">
|
|
<Link to={signedIn ? "/today" : "/"} className="sidebar__brand">
|
|
<LogoMark />
|
|
<span>
|
|
<span className="sidebar__name">{ru.product.name}</span>
|
|
<span className="sidebar__tag">{ru.product.tagline}</span>
|
|
</span>
|
|
</Link>
|
|
{signedIn ? (
|
|
<ul className="sidebar__list">
|
|
{TABS.map((t) => (
|
|
<li key={t.to}>
|
|
<NavLink to={t.to} className="sidebar__link">
|
|
<Icon name={t.icon} />
|
|
{t.label}
|
|
</NavLink>
|
|
</li>
|
|
))}
|
|
<li>
|
|
<NavLink to="/messages" className="sidebar__link">
|
|
<Icon name="message" />
|
|
{ru.nav.messages}
|
|
{unread > 0 && <span className="dot" aria-label="новое" />}
|
|
</NavLink>
|
|
</li>
|
|
</ul>
|
|
) : (
|
|
<ul className="sidebar__list">
|
|
<li>
|
|
<NavLink to="/" end className="sidebar__link">
|
|
<Icon name="home" />
|
|
{ru.nav.signIn}
|
|
</NavLink>
|
|
</li>
|
|
<li>
|
|
<button type="button" className="sidebar__link" onClick={open}>
|
|
<Icon name="question" />
|
|
{ru.nav.help}
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
)}
|
|
<div className="sidebar__note">
|
|
<strong>{ru.product.supportTitle}</strong>
|
|
<span>{ru.product.supportText}</span>
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
function BottomNav() {
|
|
return (
|
|
<nav className="tabbar" aria-label="Разделы">
|
|
{TABS.map((t) => (
|
|
<NavLink key={t.to} to={t.to} className="tabbar__item">
|
|
<Icon name={t.icon} />
|
|
<span>{t.label}</span>
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
function Rail({ aside }: { aside?: ReactNode }) {
|
|
const { resources, open } = useHelp();
|
|
return (
|
|
<aside className="rail">
|
|
{aside}
|
|
<section className="railsos" aria-label={ru.help.block}>
|
|
<button type="button" className="railsos__head" onClick={open}>
|
|
<Icon name="phone" size={18} />
|
|
<span>{ru.help.block}</span>
|
|
</button>
|
|
{resources.map((r) => (
|
|
<HotlineCard key={r.phone} r={r} size="sm" />
|
|
))}
|
|
</section>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
function WorkspaceHeader({ signedIn, bare = false }: { signedIn: boolean; bare?: boolean }) {
|
|
const { session } = useSession();
|
|
return (
|
|
<header className={`workspace${bare ? " workspace--bare" : ""}`}>
|
|
<div className="workspace__brand">
|
|
<LogoMark size={30} />
|
|
<span>{bare ? ru.product.name : signedIn ? ru.product.workspace : ru.nav.signIn}</span>
|
|
</div>
|
|
{signedIn && !bare ? (
|
|
<Link className="workspace__profile" to="/profile">
|
|
<span className="avatar"><Icon name="user" size={18} /></span>
|
|
<span>{session?.displayName || ru.nav.profile}</span>
|
|
</Link>
|
|
) : <span className="workspace__tag">{ru.product.tagline}</span>}
|
|
</header>
|
|
);
|
|
}
|
|
|
|
export function AppShell({
|
|
children,
|
|
signedIn = true,
|
|
aside,
|
|
focus = false,
|
|
sos = true,
|
|
bare = false,
|
|
}: {
|
|
children: ReactNode;
|
|
signedIn?: boolean;
|
|
aside?: ReactNode;
|
|
/** Focus screens (one question per screen) hide the tab bar on phones, like in the mocks. */
|
|
focus?: boolean;
|
|
/** The crisis screen shows the hotlines itself, so it drops the bottom block. */
|
|
sos?: boolean;
|
|
/** Questionnaires: no sidebar, rail or tab bar — one centred column, so nothing distracts from the answers. */
|
|
bare?: boolean;
|
|
}) {
|
|
if (bare)
|
|
return (
|
|
<div className="app app--bare">
|
|
<div className="app__main">
|
|
<WorkspaceHeader signedIn={signedIn} bare />
|
|
<main className="screen" id="main">
|
|
{children}
|
|
{sos && <EmergencyBlock />}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
return (
|
|
<div className={`app${signedIn && !focus ? " app--tabs" : ""}`}>
|
|
<Sidebar signedIn={signedIn} />
|
|
<div className="app__main">
|
|
<WorkspaceHeader signedIn={signedIn} />
|
|
<main className="screen" id="main">
|
|
{children}
|
|
{sos && <EmergencyBlock />}
|
|
</main>
|
|
<Rail aside={aside} />
|
|
</div>
|
|
{signedIn && !focus && <BottomNav />}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function Notice({ tone = "info", children }: { tone?: "info" | "warn" | "danger" | "ok"; children: ReactNode }) {
|
|
const icon: IconName = tone === "info" ? "info" : tone === "ok" ? "check" : tone === "danger" ? "x" : "alert";
|
|
return (
|
|
<div className={`notice notice--${tone}`} role="note">
|
|
<Icon name={icon} />
|
|
<div>{children}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function TopBar({ onBack, title, children }: { onBack?: () => void; title?: string; children?: ReactNode }) {
|
|
return (
|
|
<div className="topbar">
|
|
{onBack && (
|
|
<button type="button" className="icon-btn" onClick={onBack} aria-label={ru.common.back}>
|
|
<Icon name="arrowLeft" />
|
|
</button>
|
|
)}
|
|
{title && <span className="topbar__title">{title}</span>}
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|