UI - NODEDC LAUNCHER: invite screen по auth design-code
This commit is contained in:
parent
821a4009f0
commit
745f0f928d
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -125,6 +125,122 @@ code {
|
|||
background: #050506;
|
||||
}
|
||||
|
||||
.nodedc-auth-page {
|
||||
--nodedc-auth-bg: #0e0f10;
|
||||
--nodedc-auth-card-bg: rgba(9, 9, 12, 0.84);
|
||||
--nodedc-auth-primary: rgb(195, 255, 102);
|
||||
--nodedc-auth-primary-hover: rgb(218, 255, 139);
|
||||
--nodedc-auth-on-primary: rgb(11, 17, 23);
|
||||
--nodedc-auth-text-primary: #e4e6e7;
|
||||
--nodedc-auth-text-secondary: #cacdce;
|
||||
--nodedc-auth-text-placeholder: #959a9d;
|
||||
background: var(--nodedc-auth-bg);
|
||||
color: var(--nodedc-auth-text-primary);
|
||||
font-synthesis: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: geometricPrecision;
|
||||
}
|
||||
|
||||
.nodedc-auth-brand-shell {
|
||||
position: absolute;
|
||||
z-index: 80;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: min(100%, calc(100vw - var(--nodedc-shell-frame-gutter-x)));
|
||||
padding: var(--nodedc-shell-padding-top) var(--nodedc-shell-padding-x) var(--nodedc-shell-padding-bottom);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.nodedc-auth-page__main {
|
||||
display: grid;
|
||||
min-height: 100vh;
|
||||
place-items: center;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.nodedc-auth-card {
|
||||
display: grid;
|
||||
width: min(100%, 28rem);
|
||||
gap: 1.05rem;
|
||||
overflow: hidden;
|
||||
padding: 2.2rem;
|
||||
border: 0;
|
||||
border-radius: 1.9rem;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.048) 0%, rgba(255, 255, 255, 0.015) 100%),
|
||||
var(--nodedc-auth-card-bg);
|
||||
box-shadow: none;
|
||||
backdrop-filter: blur(40px);
|
||||
-webkit-backdrop-filter: blur(40px);
|
||||
}
|
||||
|
||||
.nodedc-auth-card__copy {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.nodedc-auth-card__copy h1,
|
||||
.nodedc-auth-card__copy p {
|
||||
max-width: 22rem;
|
||||
margin: 0;
|
||||
letter-spacing: -0.045em;
|
||||
text-transform: none;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.nodedc-auth-card__copy h1 {
|
||||
color: var(--nodedc-auth-text-primary);
|
||||
font-size: 2rem;
|
||||
font-weight: 650;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.nodedc-auth-card__copy p {
|
||||
color: var(--nodedc-auth-text-placeholder);
|
||||
font-size: 1.9rem;
|
||||
font-weight: 650;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.nodedc-invite-card__details {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin-top: 0.45rem;
|
||||
color: var(--nodedc-auth-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.nodedc-auth-card__status {
|
||||
margin: 0;
|
||||
color: var(--nodedc-auth-primary);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.nodedc-auth-card .button {
|
||||
width: 100%;
|
||||
min-height: 3.25rem;
|
||||
margin-top: 0.45rem;
|
||||
border-radius: 1.25rem;
|
||||
background: var(--nodedc-auth-primary);
|
||||
color: var(--nodedc-auth-on-primary);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.nodedc-auth-card .button:hover:not(:disabled),
|
||||
.nodedc-auth-card .button:focus-visible:not(:disabled) {
|
||||
background: var(--nodedc-auth-primary-hover);
|
||||
color: var(--nodedc-auth-on-primary);
|
||||
}
|
||||
|
||||
.nodedc-auth-card .button:disabled {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.72rem;
|
||||
|
|
|
|||
Loading…
Reference in New Issue