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" }}
|
state={inviteFlow ?? { status: "loading" }}
|
||||||
currentEmail={authSession.user.email}
|
currentEmail={authSession.user.email}
|
||||||
onAccept={() => void handleAcceptInvite()}
|
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={() => {
|
onGoHome={() => {
|
||||||
window.history.replaceState(null, "", "/");
|
window.history.replaceState(null, "", "/");
|
||||||
window.location.replace("/");
|
window.location.replace("/");
|
||||||
|
|
@ -740,23 +744,16 @@ function InviteFlowScreen({
|
||||||
state,
|
state,
|
||||||
currentEmail,
|
currentEmail,
|
||||||
onAccept,
|
onAccept,
|
||||||
|
onSwitchAccount,
|
||||||
onGoHome,
|
onGoHome,
|
||||||
}: {
|
}: {
|
||||||
state: InviteFlowState;
|
state: InviteFlowState;
|
||||||
currentEmail: string;
|
currentEmail: string;
|
||||||
onAccept: () => void;
|
onAccept: () => void;
|
||||||
|
onSwitchAccount: () => void;
|
||||||
onGoHome: () => void;
|
onGoHome: () => void;
|
||||||
}) {
|
}) {
|
||||||
const payload = "payload" in state ? state.payload : undefined;
|
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 emailMismatch = payload && payload.invite.email.toLowerCase() !== currentEmail.toLowerCase();
|
||||||
const inviteStatus = payload?.invite.status;
|
const inviteStatus = payload?.invite.status;
|
||||||
const isAccepting = state.status === "accepting";
|
const isAccepting = state.status === "accepting";
|
||||||
|
|
@ -767,52 +764,46 @@ function InviteFlowScreen({
|
||||||
inviteStatus !== "expired" &&
|
inviteStatus !== "expired" &&
|
||||||
inviteStatus !== "revoked"
|
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 (
|
return (
|
||||||
<div className="launcher-app">
|
<div className="launcher-app nodedc-auth-page">
|
||||||
<main
|
<NodeDcAuthBrandHeader />
|
||||||
style={{
|
<main className="nodedc-auth-page__main">
|
||||||
display: "grid",
|
<section className="nodedc-auth-card nodedc-invite-card" aria-live="polite">
|
||||||
minHeight: "100vh",
|
<div className="nodedc-auth-card__copy">
|
||||||
placeItems: "center",
|
<h1>Работайте во всех измерениях.</h1>
|
||||||
padding: "2rem",
|
<p>Приглашение в NODE.DC.</p>
|
||||||
}}
|
</div>
|
||||||
>
|
|
||||||
<section
|
<div className="nodedc-invite-card__details">
|
||||||
style={{
|
{details.map((detail) => (
|
||||||
display: "grid",
|
<span key={detail}>{detail}</span>
|
||||||
width: "min(34rem, 100%)",
|
))}
|
||||||
gap: "1rem",
|
</div>
|
||||||
padding: "2rem",
|
|
||||||
borderRadius: "1.75rem",
|
{statusMessage ? <p className="nodedc-auth-card__status">{statusMessage}</p> : null}
|
||||||
background: "linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.035))",
|
{state.status === "error" ? <p className="nodedc-auth-card__status">{state.message}</p> : null}
|
||||||
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}
|
|
||||||
{emailMismatch ? (
|
{emailMismatch ? (
|
||||||
<p style={{ margin: 0, color: "var(--warning)", lineHeight: 1.45 }}>
|
<button className="button button--primary" type="button" onClick={onSwitchAccount}>
|
||||||
Нужно войти под почтой, на которую выписан инвайт.
|
Сменить аккаунт
|
||||||
</p>
|
</button>
|
||||||
) : null}
|
) : state.status === "error" || state.status === "accepted" || isTerminalInvite ? (
|
||||||
{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={onGoHome}>
|
<button className="button button--primary" type="button" onClick={onGoHome}>
|
||||||
Перейти в витрину
|
Перейти в витрину
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button className="button button--primary" type="button" disabled={!canAccept || isAccepting} onClick={onAccept}>
|
<button className="button button--primary" type="button" disabled={!canAccept || isAccepting} onClick={onAccept}>
|
||||||
{isAccepting ? "Подключаем доступ" : "Принять приглашение"}
|
{state.status === "loading" ? "Проверяем" : isAccepting ? "Подключаем доступ" : "Принять приглашение"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</section>
|
</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({
|
function AuthStateScreen({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,122 @@ code {
|
||||||
background: #050506;
|
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 {
|
.eyebrow {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue