mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
fix things (#1293)
This commit is contained in:
@@ -7,6 +7,14 @@ import { isSamePathname } from "../_lib/client-route";
|
||||
import type { AuthMode } from "../_lib/den-flow";
|
||||
import { useDenFlow } from "../_providers/den-flow-provider";
|
||||
|
||||
type PanelContent = {
|
||||
title: string;
|
||||
copy: string;
|
||||
submitLabel: string;
|
||||
togglePrompt?: string;
|
||||
toggleActionLabel?: string;
|
||||
};
|
||||
|
||||
function getDesktopGrant(url: string | null) {
|
||||
if (!url) return null;
|
||||
try {
|
||||
@@ -52,7 +60,7 @@ function SocialButton({
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-center gap-3 rounded-xl border border-gray-200 bg-white px-4 py-3 text-[14px] font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className="den-button-secondary den-social-button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
@@ -62,8 +70,6 @@ function SocialButton({
|
||||
}
|
||||
|
||||
export function AuthPanel({
|
||||
panelTitle,
|
||||
panelCopy,
|
||||
prefilledEmail,
|
||||
prefillKey,
|
||||
initialMode = "sign-up",
|
||||
@@ -71,9 +77,10 @@ export function AuthPanel({
|
||||
hideSocialAuth = false,
|
||||
hideEmailField = false,
|
||||
eyebrow = "Account",
|
||||
signUpContent,
|
||||
signInContent,
|
||||
verificationContent,
|
||||
}: {
|
||||
panelTitle?: string;
|
||||
panelCopy?: string;
|
||||
prefilledEmail?: string;
|
||||
prefillKey?: string;
|
||||
initialMode?: AuthMode;
|
||||
@@ -81,6 +88,9 @@ export function AuthPanel({
|
||||
hideSocialAuth?: boolean;
|
||||
hideEmailField?: boolean;
|
||||
eyebrow?: string;
|
||||
signUpContent?: Partial<PanelContent>;
|
||||
signInContent?: Partial<PanelContent>;
|
||||
verificationContent?: Partial<PanelContent>;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -111,17 +121,37 @@ export function AuthPanel({
|
||||
resolveUserLandingRoute,
|
||||
} = useDenFlow();
|
||||
|
||||
const resolvedSignUpContent: PanelContent = {
|
||||
title: "Get started.",
|
||||
copy: "Free to try. Team plans from $50/mo.",
|
||||
submitLabel: "Create account",
|
||||
togglePrompt: "Have an account?",
|
||||
toggleActionLabel: "Sign in",
|
||||
...signUpContent,
|
||||
};
|
||||
|
||||
const resolvedSignInContent: PanelContent = {
|
||||
title: "Welcome back.",
|
||||
copy: "Sign in to open your team workspace.",
|
||||
submitLabel: "Sign in",
|
||||
togglePrompt: "Need an account?",
|
||||
toggleActionLabel: "Create one",
|
||||
...signInContent,
|
||||
};
|
||||
|
||||
const resolvedVerificationContent: PanelContent = {
|
||||
title: "Verify your email.",
|
||||
copy: "Enter the six-digit code from your inbox.",
|
||||
submitLabel: "Verify email",
|
||||
...verificationContent,
|
||||
};
|
||||
|
||||
const desktopGrant = getDesktopGrant(desktopRedirectUrl);
|
||||
const resolvedPanelTitle = verificationRequired
|
||||
? "Verify your email."
|
||||
: panelTitle ?? (authMode === "sign-up" ? "Create your Cloud account." : "Sign in to Cloud.");
|
||||
const resolvedPanelCopy = verificationRequired
|
||||
? "Enter the six-digit code from your inbox to finish setup."
|
||||
: panelCopy ?? (
|
||||
authMode === "sign-up"
|
||||
? "Start with email, GitHub, or Google."
|
||||
: "Welcome back. Keep your team setup in sync across Cloud and desktop."
|
||||
);
|
||||
const activeContent = verificationRequired
|
||||
? resolvedVerificationContent
|
||||
: authMode === "sign-in"
|
||||
? resolvedSignInContent
|
||||
: resolvedSignUpContent;
|
||||
const showLockedEmailSummary = Boolean(prefilledEmail && lockEmail && hideEmailField);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -147,31 +177,31 @@ export function AuthPanel({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-[28px] border border-gray-100 bg-white p-6 shadow-[0_10px_30px_-24px_rgba(15,23,42,0.22)] md:p-7">
|
||||
<div className="grid gap-2">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400">
|
||||
{eyebrow}
|
||||
</p>
|
||||
<h2 className="text-[28px] font-semibold tracking-[-0.04em] text-gray-900">{resolvedPanelTitle}</h2>
|
||||
<p className="text-[14px] leading-relaxed text-gray-500">{resolvedPanelCopy}</p>
|
||||
<div className="den-frame grid gap-5 p-6 md:p-7">
|
||||
<div className="grid gap-3">
|
||||
<p className="den-eyebrow">{eyebrow}</p>
|
||||
<div className="grid gap-2">
|
||||
<h2 className="den-title-lg">{activeContent.title}</h2>
|
||||
<p className="den-copy">{activeContent.copy}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{desktopAuthRequested ? (
|
||||
<div className="mt-5 rounded-2xl border border-sky-100 bg-sky-50 p-4 text-[13px] text-sky-900">
|
||||
Finish auth here and we'll send you back into the OpenWork desktop app.
|
||||
<div className="den-notice is-info grid gap-3 text-[13px]">
|
||||
<p className="m-0">Finish sign-in here, then jump back into the OpenWork desktop app.</p>
|
||||
{desktopRedirectUrl ? (
|
||||
<div className="mt-4 grid gap-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="grid gap-3">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-sky-200 bg-white px-4 py-2 text-xs font-medium text-sky-900 transition-colors hover:bg-sky-100"
|
||||
className="den-button-secondary w-full sm:w-auto"
|
||||
onClick={() => window.location.assign(desktopRedirectUrl)}
|
||||
>
|
||||
Open OpenWork
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-sky-200 bg-white px-4 py-2 text-xs font-medium text-sky-900 transition-colors hover:bg-sky-100"
|
||||
className="den-button-secondary w-full sm:w-auto"
|
||||
onClick={() => void copyDesktopValue("link", desktopRedirectUrl)}
|
||||
>
|
||||
{copiedDesktopField === "link" ? "Copied link" : "Copy sign-in link"}
|
||||
@@ -179,14 +209,14 @@ export function AuthPanel({
|
||||
{desktopGrant ? (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full border border-sky-200 bg-white px-4 py-2 text-xs font-medium text-sky-900 transition-colors hover:bg-sky-100"
|
||||
className="den-button-secondary w-full sm:w-auto"
|
||||
onClick={() => void copyDesktopValue("code", desktopGrant)}
|
||||
>
|
||||
{copiedDesktopField === "code" ? "Copied code" : "Copy one-time code"}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-xs leading-5 text-sky-800/80">
|
||||
<p className="m-0 text-xs leading-5 text-sky-800/80">
|
||||
If OpenWork does not open automatically, copy the sign-in link or one-time code and paste it into the OpenWork desktop app.
|
||||
</p>
|
||||
</div>
|
||||
@@ -195,7 +225,7 @@ export function AuthPanel({
|
||||
) : null}
|
||||
|
||||
<form
|
||||
className="mt-5 grid gap-3"
|
||||
className="grid gap-4"
|
||||
onSubmit={async (event) => {
|
||||
const next = verificationRequired
|
||||
? await submitVerificationCode(event)
|
||||
@@ -228,33 +258,24 @@ export function AuthPanel({
|
||||
<span>Continue with Google</span>
|
||||
</SocialButton>
|
||||
|
||||
<div
|
||||
className="flex items-center gap-3 text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="h-px flex-1 bg-gray-200" />
|
||||
<div className="den-divider" aria-hidden="true">
|
||||
<span>or</span>
|
||||
<span className="h-px flex-1 bg-gray-200" />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{showLockedEmailSummary ? (
|
||||
<div className="rounded-2xl border border-gray-100 bg-gray-50 px-4 py-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">
|
||||
Invited email
|
||||
</p>
|
||||
<p className="mt-1 text-[14px] font-medium text-gray-900">{prefilledEmail}</p>
|
||||
<div className="den-frame-inset grid gap-1 rounded-[1.5rem] px-4 py-3">
|
||||
<p className="den-label">Invited email</p>
|
||||
<p className="m-0 text-sm font-medium text-[var(--dls-text-primary)]">{prefilledEmail}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!hideEmailField ? (
|
||||
<label className="grid gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">
|
||||
Email
|
||||
</span>
|
||||
<span className="den-label">Email</span>
|
||||
<input
|
||||
className="w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-[14px] text-gray-900 outline-none transition focus:border-gray-300 focus:ring-4 focus:ring-gray-900/5 disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500"
|
||||
className="den-input disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-500"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
@@ -268,11 +289,9 @@ export function AuthPanel({
|
||||
|
||||
{!verificationRequired ? (
|
||||
<label className="grid gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">
|
||||
Password
|
||||
</span>
|
||||
<span className="den-label">Password</span>
|
||||
<input
|
||||
className="w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-[14px] text-gray-900 outline-none transition focus:border-gray-300 focus:ring-4 focus:ring-gray-900/5"
|
||||
className="den-input"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
@@ -282,11 +301,9 @@ export function AuthPanel({
|
||||
</label>
|
||||
) : (
|
||||
<label className="grid gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">
|
||||
Verification code
|
||||
</span>
|
||||
<span className="den-label">Verification code</span>
|
||||
<input
|
||||
className="w-full rounded-xl border border-gray-200 bg-white px-4 py-3 text-center text-[18px] font-semibold tracking-[0.35em] text-gray-900 outline-none transition focus:border-gray-300 focus:ring-4 focus:ring-gray-900/5"
|
||||
className="den-input text-center text-[18px] font-semibold tracking-[0.35em]"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
@@ -302,16 +319,10 @@ export function AuthPanel({
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="flex w-full items-center justify-center gap-2 rounded-full bg-gray-900 px-5 py-3 text-[14px] font-medium text-white transition-colors hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className="den-button-primary w-full"
|
||||
disabled={authBusy || desktopRedirectBusy}
|
||||
>
|
||||
{authBusy || desktopRedirectBusy
|
||||
? "Working..."
|
||||
: verificationRequired
|
||||
? "Verify email"
|
||||
: authMode === "sign-in"
|
||||
? "Sign in to Cloud"
|
||||
: "Create Cloud account"}
|
||||
{authBusy || desktopRedirectBusy ? "Working..." : activeContent.submitLabel}
|
||||
{!authBusy && !desktopRedirectBusy ? <ArrowRight className="h-4 w-4" /> : null}
|
||||
</button>
|
||||
|
||||
@@ -319,7 +330,7 @@ export function AuthPanel({
|
||||
<div className="flex flex-col gap-3 sm:flex-row">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full rounded-full border border-gray-200 bg-white px-4 py-3 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className="den-button-secondary w-full"
|
||||
onClick={() => void resendVerificationCode()}
|
||||
disabled={authBusy || desktopRedirectBusy}
|
||||
>
|
||||
@@ -327,7 +338,7 @@ export function AuthPanel({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="w-full rounded-full border border-gray-200 bg-white px-4 py-3 text-[13px] font-medium text-gray-700 transition-colors hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
className="den-button-secondary w-full"
|
||||
onClick={() => cancelVerification()}
|
||||
disabled={authBusy || desktopRedirectBusy}
|
||||
>
|
||||
@@ -338,21 +349,27 @@ export function AuthPanel({
|
||||
</form>
|
||||
|
||||
{!verificationRequired ? (
|
||||
<div className="mt-4 flex items-center justify-between gap-3 border-t border-gray-200 pt-4 text-sm text-gray-500">
|
||||
<p>{authMode === "sign-in" ? "Need an account?" : "Already have an account?"}</p>
|
||||
<div className="flex items-center justify-between gap-3 border-t border-[var(--dls-border)] pt-4 text-sm text-[var(--dls-text-secondary)]">
|
||||
<p className="m-0">
|
||||
{authMode === "sign-in"
|
||||
? resolvedSignInContent.togglePrompt
|
||||
: resolvedSignUpContent.togglePrompt}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
className="font-medium text-gray-900 transition hover:opacity-70"
|
||||
className="font-medium text-[var(--dls-text-primary)] transition hover:opacity-70"
|
||||
onClick={() => setAuthMode(authMode === "sign-in" ? "sign-up" : "sign-in")}
|
||||
>
|
||||
{authMode === "sign-in" ? "Create account" : "Switch to sign in"}
|
||||
{authMode === "sign-in"
|
||||
? resolvedSignInContent.toggleActionLabel
|
||||
: resolvedSignUpContent.toggleActionLabel}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showAuthFeedback ? (
|
||||
<div
|
||||
className="mt-4 grid gap-1 rounded-2xl border border-gray-100 bg-gray-50 px-4 py-3 text-center text-[13px] text-gray-500"
|
||||
className="den-frame-inset grid gap-1 rounded-[1.5rem] px-4 py-3 text-center text-[13px] text-[var(--dls-text-secondary)]"
|
||||
aria-live="polite"
|
||||
>
|
||||
<p>{authInfo}</p>
|
||||
|
||||
@@ -10,25 +10,23 @@ import { AuthPanel } from "./auth-panel";
|
||||
|
||||
function FeatureCard({ title, body }: { title: string; body: string }) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-100 bg-white p-5">
|
||||
<p className="mb-2 text-[14px] font-medium text-gray-900">{title}</p>
|
||||
<p className="text-[13px] leading-[1.6] text-gray-500">{body}</p>
|
||||
<div className="den-stat-card grid gap-2">
|
||||
<p className="m-0 text-[14px] font-medium text-[var(--dls-text-primary)]">{title}</p>
|
||||
<p className="m-0 text-[13px] leading-[1.6] text-[var(--dls-text-secondary)]">{body}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingPanel({ title, body }: { title: string; body: string }) {
|
||||
return (
|
||||
<div className="rounded-[28px] border border-gray-100 bg-white p-6 shadow-[0_10px_30px_-24px_rgba(15,23,42,0.22)] md:p-7">
|
||||
<div className="den-frame grid gap-3 p-6 md:p-7">
|
||||
<div className="grid gap-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400">
|
||||
OpenWork Cloud
|
||||
</p>
|
||||
<h2 className="text-[28px] font-semibold tracking-[-0.04em] text-gray-900">{title}</h2>
|
||||
<p className="text-[14px] leading-relaxed text-gray-500">{body}</p>
|
||||
<p className="den-eyebrow">OpenWork Cloud</p>
|
||||
<h2 className="den-title-lg">{title}</h2>
|
||||
<p className="den-copy">{body}</p>
|
||||
</div>
|
||||
<div className="mt-6 h-2 overflow-hidden rounded-full bg-gray-100">
|
||||
<div className="h-full w-1/3 animate-pulse rounded-full bg-gray-900/80" />
|
||||
<div className="h-2 overflow-hidden rounded-full bg-[var(--dls-hover)]">
|
||||
<div className="h-full w-1/3 animate-pulse rounded-full bg-[var(--dls-accent)]" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -70,7 +68,7 @@ export function AuthScreen() {
|
||||
<section className="den-page flex w-full items-center py-4 lg:min-h-[calc(100vh-2.5rem)]">
|
||||
<div className="grid w-full gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(360px,440px)]">
|
||||
<div className="order-2 flex flex-col gap-6 lg:order-1">
|
||||
<div className="relative min-h-[300px] overflow-hidden rounded-[32px] border border-gray-100 px-7 py-8 md:px-10 md:py-10">
|
||||
<div className="den-frame relative min-h-[300px] overflow-hidden px-7 py-8 md:px-10 md:py-10">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Dithering
|
||||
speed={0}
|
||||
@@ -104,13 +102,13 @@ export function AuthScreen() {
|
||||
|
||||
<div className="grid gap-4">
|
||||
<span className="inline-flex w-fit rounded-full border border-white/20 bg-white/15 px-3 py-1 text-[10px] font-medium uppercase tracking-[0.18em] text-white backdrop-blur-md">
|
||||
Shared setups
|
||||
OpenWork Cloud
|
||||
</span>
|
||||
<h1 className="max-w-[12ch] text-[2.25rem] font-semibold leading-[0.95] tracking-[-0.06em] text-white md:text-[3rem]">
|
||||
Share your OpenWork setup with your team.
|
||||
One setup, every seat.
|
||||
</h1>
|
||||
<p className="max-w-[34rem] text-[15px] leading-7 text-white/80">
|
||||
Provision shared setups, invite your org, and keep background workspaces available across Cloud and desktop.
|
||||
Configure once. Your whole team gets the same tools, agents, and providers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,16 +116,16 @@ export function AuthScreen() {
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<FeatureCard
|
||||
title="Team sharing"
|
||||
body="Package skills, MCPs, plugins, and config once so the whole org can use the same setup."
|
||||
title="Shared config"
|
||||
body="Set it up once, then push it to the org."
|
||||
/>
|
||||
<FeatureCard
|
||||
title="Cloud Hosted Agents"
|
||||
body="Keep selected workflows running in the cloud without asking each teammate to run them locally."
|
||||
title="Cloud agents"
|
||||
body="Workflows that keep running while your team is away."
|
||||
/>
|
||||
<FeatureCard
|
||||
title="Custom LLM Providers"
|
||||
body="Whether you want to use LiteLLM, Azure, or any other provider, you can use OpenWork to provision your team."
|
||||
title="Your models"
|
||||
body="Bring your own provider when the team is ready."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,14 +17,16 @@ import { AuthPanel } from "./auth-panel";
|
||||
|
||||
function LoadingCard({ title, body }: { title: string; body: string }) {
|
||||
return (
|
||||
<section className="mx-auto grid w-full max-w-[40rem] gap-4 rounded-[32px] border border-gray-100 bg-white p-6 shadow-[0_10px_30px_-24px_rgba(15,23,42,0.22)] md:p-8">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400">OpenWork Cloud</p>
|
||||
<div className="grid gap-2">
|
||||
<h1 className="text-[2rem] font-semibold tracking-[-0.05em] text-gray-900">{title}</h1>
|
||||
<p className="text-[14px] leading-relaxed text-gray-500">{body}</p>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-gray-100">
|
||||
<div className="h-full w-1/3 animate-pulse rounded-full bg-gray-900/80" />
|
||||
<section className="den-page py-4 lg:py-6">
|
||||
<div className="den-frame grid max-w-[44rem] gap-4 p-6 md:p-7">
|
||||
<p className="den-eyebrow">OpenWork Cloud</p>
|
||||
<div className="grid gap-2">
|
||||
<h1 className="den-title-lg">{title}</h1>
|
||||
<p className="den-copy">{body}</p>
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-[var(--dls-hover)]">
|
||||
<div className="h-full w-1/3 animate-pulse rounded-full bg-[var(--dls-accent)]" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -33,13 +35,13 @@ function LoadingCard({ title, body }: { title: string; body: string }) {
|
||||
function statusMessage(preview: DenInvitationPreview | null) {
|
||||
switch (preview?.invitation.status) {
|
||||
case "accepted":
|
||||
return "This invitation has already been accepted.";
|
||||
return "This invite has already been used.";
|
||||
case "canceled":
|
||||
return "This invitation has been canceled.";
|
||||
return "This invite was canceled.";
|
||||
case "expired":
|
||||
return "This invitation has expired.";
|
||||
return "This invite expired.";
|
||||
default:
|
||||
return "This invitation is no longer available.";
|
||||
return "This invite is no longer available.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +57,7 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) {
|
||||
const invitedEmailMatches = preview && user
|
||||
? preview.invitation.email.trim().toLowerCase() === user.email.trim().toLowerCase()
|
||||
: false;
|
||||
const roleLabel = preview ? formatRoleLabel(preview.invitation.role) : "";
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -87,7 +90,7 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) {
|
||||
}
|
||||
|
||||
setPreview(null);
|
||||
setPreviewError(getErrorMessage(payload, response.status === 404 ? "This invitation is no longer available." : `Could not load the invitation (${response.status}).`));
|
||||
setPreviewError(getErrorMessage(payload, response.status === 404 ? "This invite is no longer available." : `Could not load the invite (${response.status}).`));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,7 +105,7 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) {
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
setPreview(null);
|
||||
setPreviewError(error instanceof Error ? error.message : "Could not load the invitation.");
|
||||
setPreviewError(error instanceof Error ? error.message : "Could not load the invite.");
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
@@ -138,7 +141,7 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) {
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
setJoinError(getErrorMessage(payload, response.status === 404 ? "This invitation could not be accepted." : `Could not join the organization (${response.status}).`));
|
||||
setJoinError(getErrorMessage(payload, response.status === 404 ? "This invite could not be accepted." : `Could not join the organization (${response.status}).`));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -165,21 +168,23 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) {
|
||||
}
|
||||
|
||||
if (!sessionHydrated || previewBusy) {
|
||||
return <LoadingCard title="Loading invitation." body="Checking the invitation details and your account state..." />;
|
||||
return <LoadingCard title="Loading invite." body="Checking the invite details and your account state..." />;
|
||||
}
|
||||
|
||||
if (!preview) {
|
||||
return (
|
||||
<section className="mx-auto grid w-full max-w-[40rem] gap-6 rounded-[32px] border border-gray-100 bg-white p-6 shadow-[0_10px_30px_-24px_rgba(15,23,42,0.22)] md:p-8">
|
||||
<div className="grid gap-2">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400">OpenWork Cloud</p>
|
||||
<h1 className="text-[2rem] font-semibold tracking-[-0.05em] text-gray-900">Invitation unavailable.</h1>
|
||||
<p className="text-[14px] leading-relaxed text-gray-500">{previewError ?? "This invitation could not be loaded."}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link href="/" className="inline-flex items-center rounded-full bg-gray-900 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-800">
|
||||
Back to OpenWork Cloud
|
||||
</Link>
|
||||
<section className="den-page py-4 lg:py-6">
|
||||
<div className="den-frame grid max-w-[44rem] gap-6 p-6 md:p-8">
|
||||
<div className="grid gap-2">
|
||||
<p className="den-eyebrow">OpenWork Cloud</p>
|
||||
<h1 className="den-title-lg">This invite can't be opened.</h1>
|
||||
<p className="den-copy">{previewError ?? "This invite could not be loaded."}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link href="/" className="den-button-primary w-full sm:w-auto">
|
||||
Back to OpenWork Cloud
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -187,38 +192,55 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) {
|
||||
|
||||
if (preview.invitation.status === "pending" && !user) {
|
||||
return (
|
||||
<section className="mx-auto grid w-full max-w-[64rem] gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(360px,440px)]">
|
||||
<div className="grid gap-6 rounded-[32px] border border-gray-100 bg-white p-6 shadow-[0_10px_30px_-24px_rgba(15,23,42,0.22)] md:p-8">
|
||||
<section className="den-page grid gap-6 py-4 lg:grid-cols-[minmax(0,1fr)_minmax(360px,440px)] lg:py-6">
|
||||
<div className="den-frame grid gap-6 p-6 md:p-8">
|
||||
<div className="grid gap-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400">OpenWork Cloud</p>
|
||||
<p className="den-eyebrow">OpenWork Cloud</p>
|
||||
<div className="grid gap-2">
|
||||
<p className="text-[15px] font-medium text-gray-500">You've been invited to</p>
|
||||
<h1 className="text-[2.5rem] font-semibold tracking-[-0.06em] text-gray-900">{preview.organization.name}</h1>
|
||||
<p className="den-copy">You've been invited to</p>
|
||||
<h1 className="den-title-xl max-w-[12ch]">{preview.organization.name}</h1>
|
||||
</div>
|
||||
<p className="text-[14px] leading-relaxed text-gray-500">Role: {formatRoleLabel(preview.invitation.role)}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 rounded-3xl border border-gray-100 bg-gray-50 p-5">
|
||||
<div className="grid gap-1">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-gray-400">Invited email</p>
|
||||
<p className="text-[16px] font-medium text-gray-900">{preview.invitation.email}</p>
|
||||
</div>
|
||||
<p className="text-[14px] leading-relaxed text-gray-600">
|
||||
Set a password for this invited email to create your OpenWork Cloud account, or switch to sign in if you already use it.
|
||||
<div className="den-meta-row">
|
||||
<span className="den-kicker">Role · {roleLabel}</span>
|
||||
</div>
|
||||
|
||||
<div className="den-frame-inset grid gap-3 rounded-[1.5rem] p-5">
|
||||
<p className="m-0 text-base font-medium text-[var(--dls-text-primary)]">
|
||||
Your team is already set up and waiting.
|
||||
</p>
|
||||
<p className="den-copy">Member access is ready as soon as you join.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AuthPanel
|
||||
eyebrow="Invite"
|
||||
panelTitle={`Join ${preview.organization.name}.`}
|
||||
panelCopy="We prefilled your invited email so you can finish account setup without going through the generic signup flow first."
|
||||
prefilledEmail={preview.invitation.email}
|
||||
prefillKey={preview.invitation.id}
|
||||
initialMode="sign-up"
|
||||
lockEmail
|
||||
hideSocialAuth
|
||||
hideEmailField
|
||||
signUpContent={{
|
||||
title: `Join ${preview.organization.name}.`,
|
||||
copy: "Pick a password and you're in.",
|
||||
submitLabel: `Join ${preview.organization.name}`,
|
||||
togglePrompt: "Already on Cloud?",
|
||||
toggleActionLabel: "Sign in",
|
||||
}}
|
||||
signInContent={{
|
||||
title: `Join ${preview.organization.name}.`,
|
||||
copy: `Sign in as ${preview.invitation.email} to accept this invite.`,
|
||||
submitLabel: "Sign in to join",
|
||||
togglePrompt: "Need a new account?",
|
||||
toggleActionLabel: "Create one",
|
||||
}}
|
||||
verificationContent={{
|
||||
title: "Check your inbox.",
|
||||
copy: `Enter the six-digit code sent to ${preview.invitation.email}.`,
|
||||
submitLabel: "Verify and join",
|
||||
}}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
@@ -227,68 +249,75 @@ export function JoinOrgScreen({ invitationId }: { invitationId: string }) {
|
||||
const showAcceptAction = preview.invitation.status === "pending" && Boolean(user) && invitedEmailMatches;
|
||||
|
||||
return (
|
||||
<section className="mx-auto grid w-full max-w-[40rem] gap-6 rounded-[32px] border border-gray-100 bg-white p-6 shadow-[0_10px_30px_-24px_rgba(15,23,42,0.22)] md:p-8">
|
||||
<div className="grid gap-3">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-gray-400">OpenWork Cloud</p>
|
||||
<div className="grid gap-2">
|
||||
<p className="text-[15px] font-medium text-gray-500">You've been invited to</p>
|
||||
<h1 className="text-[2.5rem] font-semibold tracking-[-0.06em] text-gray-900">{preview.organization.name}</h1>
|
||||
<section className="den-page py-4 lg:py-6">
|
||||
<div className="den-frame grid max-w-[44rem] gap-6 p-6 md:p-8">
|
||||
<div className="grid gap-3">
|
||||
<p className="den-eyebrow">OpenWork Cloud</p>
|
||||
<div className="grid gap-2">
|
||||
<p className="den-copy">You've been invited to</p>
|
||||
<h1 className="den-title-xl max-w-[12ch]">{preview.organization.name}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[14px] leading-relaxed text-gray-500">Role: {formatRoleLabel(preview.invitation.role)}</p>
|
||||
|
||||
<div className="den-meta-row">
|
||||
<span className="den-kicker">Role · {roleLabel}</span>
|
||||
{user ? <span>{user.email}</span> : null}
|
||||
</div>
|
||||
|
||||
{user ? (
|
||||
<div className="den-frame-inset grid gap-1 rounded-[1.5rem] px-4 py-3">
|
||||
<p className="den-label">Signed in as</p>
|
||||
<p className="m-0 text-sm font-medium text-[var(--dls-text-primary)]">{user.email}</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{preview.invitation.status !== "pending" ? (
|
||||
<div className="grid gap-4">
|
||||
<p className="den-copy">{statusMessage(preview)}</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link
|
||||
href={user && invitedEmailMatches ? getOrgDashboardRoute(preview.organization.slug) : "/"}
|
||||
className="den-button-primary w-full sm:w-auto"
|
||||
>
|
||||
{user && invitedEmailMatches ? "Open team" : "Back to OpenWork Cloud"}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : !invitedEmailMatches ? (
|
||||
<div className="grid gap-4">
|
||||
<p className="den-copy">
|
||||
This invite is for <span className="font-medium text-[var(--dls-text-primary)]">{preview.invitation.email}</span>. Switch accounts to continue.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="den-button-primary w-full sm:w-auto"
|
||||
onClick={() => void handleSwitchAccount()}
|
||||
disabled={joinBusy}
|
||||
>
|
||||
Use a different account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
<p className="den-copy">You're one click away from the team workspace.</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="den-button-primary w-full sm:w-auto"
|
||||
onClick={() => void handleAcceptInvitation()}
|
||||
disabled={!showAcceptAction || joinBusy}
|
||||
>
|
||||
{joinBusy ? "Joining..." : `Join ${preview.organization.name}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{joinError ? <div className="den-notice is-error">{joinError}</div> : null}
|
||||
{previewError ? <div className="den-notice is-error">{previewError}</div> : null}
|
||||
</div>
|
||||
|
||||
{user ? (
|
||||
<div className="rounded-2xl border border-gray-100 bg-gray-50 px-4 py-3 text-[13px] text-gray-700">
|
||||
Signed in as <span className="font-medium text-gray-900">{user.email}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{preview.invitation.status !== "pending" ? (
|
||||
<div className="grid gap-4">
|
||||
<p className="text-[15px] leading-relaxed text-gray-600">{statusMessage(preview)}</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link
|
||||
href={user && invitedEmailMatches ? getOrgDashboardRoute(preview.organization.slug) : "/"}
|
||||
className="inline-flex items-center rounded-full bg-gray-900 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-800"
|
||||
>
|
||||
{user && invitedEmailMatches ? "Open organization" : "Back to OpenWork Cloud"}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : !invitedEmailMatches ? (
|
||||
<div className="grid gap-4">
|
||||
<p className="text-[15px] leading-relaxed text-gray-600">
|
||||
This invite was sent to <span className="font-medium text-gray-900">{preview.invitation.email}</span>. Sign in with that email to join the organization.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center rounded-full bg-gray-900 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
onClick={() => void handleSwitchAccount()}
|
||||
disabled={joinBusy}
|
||||
>
|
||||
Use a different account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4">
|
||||
<p className="text-[15px] leading-relaxed text-gray-600">Click to join</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center rounded-full bg-gray-900 px-5 py-2.5 text-sm font-medium text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
onClick={() => void handleAcceptInvitation()}
|
||||
disabled={!showAcceptAction || joinBusy}
|
||||
>
|
||||
{joinBusy ? "Joining..." : "Join org"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{joinError ? <p className="text-[13px] text-rose-600">{joinError}</p> : null}
|
||||
{previewError ? <p className="text-[13px] text-rose-600">{previewError}</p> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -333,6 +333,72 @@ body {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.den-social-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.den-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: var(--dls-text-secondary);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.den-divider::before,
|
||||
.den-divider::after {
|
||||
content: "";
|
||||
flex: 1 1 auto;
|
||||
height: 1px;
|
||||
background: var(--dls-border);
|
||||
}
|
||||
|
||||
.den-meta-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
color: var(--dls-text-secondary);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.den-bullet-list {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.den-bullet-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
color: var(--dls-text-secondary);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.den-bullet-dot {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
margin-top: 0.55rem;
|
||||
border-radius: 9999px;
|
||||
flex: 0 0 auto;
|
||||
background: #cbd5e1;
|
||||
}
|
||||
|
||||
.den-link-inline {
|
||||
color: var(--dls-text-primary);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.den-link-inline:hover {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.ow-shell {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
|
||||
Reference in New Issue
Block a user