UI - NODEDC LAUNCHER: invite screen по auth design-code

This commit is contained in:
DCCONSTRUCTIONS
2026-05-05 16:26:19 +03:00
parent 821a4009f0
commit 745f0f928d
2 changed files with 174 additions and 47 deletions
+58 -47
View File
@@ -587,6 +587,10 @@ export function LauncherApp() {
state={inviteFlow ?? { status: "loading" }}
currentEmail={authSession.user.email}
onAccept={() => void handleAcceptInvite()}
onSwitchAccount={() => {
const returnTo = `${window.location.pathname}${window.location.search}${window.location.hash}`;
window.location.replace(`/auth/logout?global=1&returnTo=${encodeURIComponent(returnTo)}`);
}}
onGoHome={() => {
window.history.replaceState(null, "", "/");
window.location.replace("/");
@@ -740,23 +744,16 @@ function InviteFlowScreen({
state,
currentEmail,
onAccept,
onSwitchAccount,
onGoHome,
}: {
state: InviteFlowState;
currentEmail: string;
onAccept: () => void;
onSwitchAccount: () => void;
onGoHome: () => void;
}) {
const payload = "payload" in state ? state.payload : undefined;
const title =
state.status === "accepted"
? "Доступ подключён"
: state.status === "error"
? "Инвайт недоступен"
: "Приглашение в NODE.DC";
const description = payload
? `Клиент: ${payload.client.name}. Роль: ${membershipRoleLabel(payload.invite.role)}.`
: "Проверяем приглашение и платформенную сессию.";
const emailMismatch = payload && payload.invite.email.toLowerCase() !== currentEmail.toLowerCase();
const inviteStatus = payload?.invite.status;
const isAccepting = state.status === "accepting";
@@ -767,52 +764,46 @@ function InviteFlowScreen({
inviteStatus !== "expired" &&
inviteStatus !== "revoked"
);
const isTerminalInvite = inviteStatus === "accepted" || inviteStatus === "expired" || inviteStatus === "revoked";
const details = payload
? [
`Рабочая область: ${payload.client.name}`,
`Роль: ${membershipRoleLabel(payload.invite.role)}`,
`Почта инвайта: ${payload.invite.email}`,
]
: ["Проверяем приглашение и платформенную сессию"];
const statusMessage = resolveInviteStatusMessage(state, Boolean(emailMismatch), inviteStatus);
return (
<div className="launcher-app">
<main
style={{
display: "grid",
minHeight: "100vh",
placeItems: "center",
padding: "2rem",
}}
>
<section
style={{
display: "grid",
width: "min(34rem, 100%)",
gap: "1rem",
padding: "2rem",
borderRadius: "1.75rem",
background: "linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.035))",
textAlign: "center",
}}
>
<img src="/nodedc-logo.svg" alt="NODE.DC" style={{ justifySelf: "center", width: "11rem" }} />
<p className="eyebrow" style={{ margin: 0 }}>
Invite flow
</p>
<h1 style={{ margin: 0 }}>{title}</h1>
<p style={{ margin: 0, color: "var(--text-secondary)", lineHeight: 1.5 }}>{description}</p>
{payload ? (
<p style={{ margin: 0, color: "var(--text-secondary)", lineHeight: 1.5 }}>
Инвайт: <strong>{payload.invite.email}</strong>. Текущий вход: <strong>{currentEmail}</strong>.
</p>
) : null}
<div className="launcher-app nodedc-auth-page">
<NodeDcAuthBrandHeader />
<main className="nodedc-auth-page__main">
<section className="nodedc-auth-card nodedc-invite-card" aria-live="polite">
<div className="nodedc-auth-card__copy">
<h1>Работайте во всех измерениях.</h1>
<p>Приглашение в NODE.DC.</p>
</div>
<div className="nodedc-invite-card__details">
{details.map((detail) => (
<span key={detail}>{detail}</span>
))}
</div>
{statusMessage ? <p className="nodedc-auth-card__status">{statusMessage}</p> : null}
{state.status === "error" ? <p className="nodedc-auth-card__status">{state.message}</p> : null}
{emailMismatch ? (
<p style={{ margin: 0, color: "var(--warning)", lineHeight: 1.45 }}>
Нужно войти под почтой, на которую выписан инвайт.
</p>
) : null}
{state.status === "error" ? <p style={{ margin: 0, color: "var(--warning)", lineHeight: 1.45 }}>{state.message}</p> : null}
{state.status === "accepted" ? (
<button className="button button--primary" type="button" onClick={onSwitchAccount}>
Сменить аккаунт
</button>
) : state.status === "error" || state.status === "accepted" || isTerminalInvite ? (
<button className="button button--primary" type="button" onClick={onGoHome}>
Перейти в витрину
</button>
) : (
<button className="button button--primary" type="button" disabled={!canAccept || isAccepting} onClick={onAccept}>
{isAccepting ? "Подключаем доступ" : "Принять приглашение"}
{state.status === "loading" ? "Проверяем" : isAccepting ? "Подключаем доступ" : "Принять приглашение"}
</button>
)}
</section>
@@ -821,6 +812,26 @@ function InviteFlowScreen({
);
}
function NodeDcAuthBrandHeader() {
return (
<header className="nodedc-auth-brand-shell">
<a href="/" className="nodedc-expanded-brand-link" aria-label="NODE.DC">
<img src="/nodedc-logo.svg" alt="NODE DC" className="nodedc-expanded-brand-logo" />
</a>
</header>
);
}
function resolveInviteStatusMessage(state: InviteFlowState, emailMismatch: boolean, inviteStatus?: Invite["status"]) {
if (state.status === "loading") return "Проверяем приглашение.";
if (state.status === "accepting") return "Подключаем доступ к рабочей области.";
if (state.status === "accepted" || inviteStatus === "accepted") return "Доступ уже подключён.";
if (inviteStatus === "expired") return "Срок действия приглашения истёк.";
if (inviteStatus === "revoked") return "Приглашение отозвано.";
if (emailMismatch) return "Нужно войти под почтой, на которую выписан инвайт.";
return null;
}
function AuthStateScreen({
title,
description,