Use compliance page logos for auth layout

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-01-30 11:46:46 +04:00
parent aa8bc15233
commit d76ac5b5fa
6 changed files with 255 additions and 87 deletions

View File

@@ -1,17 +1,48 @@
import { useSystemTheme } from "@probo/hooks";
import { Logo } from "@probo/ui";
import type { PropsWithChildren } from "react";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { Outlet } from "react-router";
import { graphql } from "relay-runtime";
export function AuthLayout(props: PropsWithChildren) {
const { children } = props;
import type { AuthLayoutQuery } from "./__generated__/AuthLayoutQuery.graphql";
export const authLayoutQuery = graphql`
query AuthLayoutQuery {
currentTrustCenter @required(action: THROW) {
logoFileUrl
darkLogoFileUrl
}
}
`;
export function AuthLayout(props: { queryRef: PreloadedQuery<AuthLayoutQuery> }) {
const { queryRef } = props;
const { currentTrustCenter: compliancePage } = usePreloadedQuery<AuthLayoutQuery>(authLayoutQuery, queryRef);
const theme = useSystemTheme();
const logoFileUrl = theme === "dark"
? compliancePage.darkLogoFileUrl ?? compliancePage.logoFileUrl
: compliancePage.logoFileUrl;
return (
<div className="grid grid-cols-1 lg:grid-cols-2 min-h-screen text-txt-primary">
<div className="bg-level-0 flex flex-col items-center justify-center">
<div className="w-full max-w-md px-6">{children}</div>
<div className="w-full max-w-md px-6">
<Outlet />
</div>
</div>
<div className="hidden lg:flex bg-dialog font-bold flex-col items-center justify-center p-8 text-txt-primary lg:p-10">
<div className="flex flex-col items-center justify-center gap-4">
<Logo withPicto className="w-[440px]" />
{logoFileUrl
? (
<img
alt=""
src={logoFileUrl}
className="size-110 rounded-2xl"
/>
)
: <Logo withPicto className="w-110" />}
</div>
</div>
</div>

View File

@@ -0,0 +1,29 @@
import { useEffect } from "react";
import { useQueryLoader } from "react-relay";
import { RelayProvider } from "#/providers/RelayProviders";
import type { AuthLayoutQuery } from "./__generated__/AuthLayoutQuery.graphql";
import { AuthLayout, authLayoutQuery } from "./AuthLayout";
function AuthLayoutQueryLoader() {
const [queryRef, loadQuery] = useQueryLoader<AuthLayoutQuery>(authLayoutQuery);
useEffect(() => {
if (!queryRef) {
return loadQuery({});
}
});
if (!queryRef) return null;
return <AuthLayout queryRef={queryRef} />;
}
export default function AuthLayoutLoader() {
return (
<RelayProvider>
<AuthLayoutQueryLoader />
</RelayProvider>
);
}

View File

@@ -14,7 +14,6 @@ import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import type { ConnectPageMutation } from "./__generated__/ConnectPageMutation.graphql";
import type { ConnectPageQuery } from "./__generated__/ConnectPageQuery.graphql";
import { AuthLayout } from "./AuthLayout";
export const connectPageQuery = graphql`
query ConnectPageQuery {
@@ -126,49 +125,47 @@ export function ConnectPage(props: {
});
return (
<AuthLayout>
<div className="space-y-6 w-full max-w-md mx-auto">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">
{__(`Connect to ${organization.name}'s compliance page`)}
</h1>
<p className="text-txt-tertiary">
<div className="space-y-6 w-full max-w-md mx-auto">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">
{__(`Connect to ${organization.name}'s compliance page`)}
</h1>
<p className="text-txt-tertiary">
{__(
"Enter your email address to connect with a magic link and start requesting access to documents",
)}
</p>
</div>
<form onSubmit={e => void handleSubmit(e)} className="space-y-4">
<Field
label={__("Email")}
placeholder="john.doe@acme.com"
{...register("email")}
type="email"
error={formState.errors.email?.message}
/>
{magicLinkSent && (
<p className="text-txt-primary text-sm">
{__(
"Enter your email address to connect with a magic link and start requesting access to documents",
"Magic Link Sent! Check your emails and use the link to connect.",
)}
</p>
</div>
)}
<form onSubmit={e => void handleSubmit(e)} className="space-y-4">
<Field
label={__("Email")}
placeholder="john.doe@acme.com"
{...register("email")}
type="email"
error={formState.errors.email?.message}
/>
{magicLinkSent && (
<p className="text-txt-primary text-sm">
{__(
"Magic Link Sent! Check your emails and use the link to connect.",
)}
</p>
)}
<Button
type="submit"
className="w-full"
disabled={formState.isSubmitting || (magicLinkSent && timer !== 0)}
>
{magicLinkSent
? timer === 0
? __("Resend Link")
: `${__("Resend Link in")} ${timer}s`
: __("Send Magic Link")}
</Button>
</form>
</div>
</AuthLayout>
<Button
type="submit"
className="w-full"
disabled={formState.isSubmitting || (magicLinkSent && timer !== 0)}
>
{magicLinkSent
? timer === 0
? __("Resend Link")
: `${__("Resend Link in")} ${timer}s`
: __("Send Magic Link")}
</Button>
</form>
</div>
);
}

View File

@@ -12,7 +12,6 @@ import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { getPathPrefix } from "#/utils/pathPrefix";
import type { VerifyMagicLinkPageMutation } from "./__generated__/VerifyMagicLinkPageMutation.graphql";
import { AuthLayout } from "./AuthLayout";
const verifyMagicLinkMutation = graphql`
mutation VerifyMagicLinkPageMutation($input: VerifyMagicLinkInput!) {
@@ -87,39 +86,37 @@ export default function VerifyMagicLinkPagePageMutation() {
});
return (
<AuthLayout>
<div className="space-y-6 w-full max-w-md mx-auto">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Email Confirmation")}</h1>
<p className="text-txt-tertiary">
{__("Confirm your email address to complete registration")}
</p>
</div>
<form onSubmit={e => void handleSubmit(e)} className="space-y-4">
<Field
label={__("Confirmation Token")}
type="text"
placeholder={__("Enter your confirmation token")}
{...form.register("token")}
error={form.formState.errors.token?.message}
disabled={form.formState.isSubmitting}
help={__(
"The token has been automatically filled from the URL if available",
)}
/>
<Button
type="submit"
className="w-full"
disabled={form.formState.isSubmitting}
>
{form.formState.isSubmitting
? __("Confirming...")
: __("Confirm Email")}
</Button>
</form>
<div className="space-y-6 w-full max-w-md mx-auto">
<div className="space-y-2 text-center">
<h1 className="text-3xl font-bold">{__("Email Confirmation")}</h1>
<p className="text-txt-tertiary">
{__("Confirm your email address to complete registration")}
</p>
</div>
</AuthLayout>
<form onSubmit={e => void handleSubmit(e)} className="space-y-4">
<Field
label={__("Confirmation Token")}
type="text"
placeholder={__("Enter your confirmation token")}
{...form.register("token")}
error={form.formState.errors.token?.message}
disabled={form.formState.isSubmitting}
help={__(
"The token has been automatically filled from the URL if available",
)}
/>
<Button
type="submit"
className="w-full"
disabled={form.formState.isSubmitting}
>
{form.formState.isSubmitting
? __("Confirming...")
: __("Confirm Email")}
</Button>
</form>
</div>
);
}

View File

@@ -0,0 +1,108 @@
/**
* @generated SignedSource<<c3e41c1d38cd6923e718ea03bba5bb44>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type AuthLayoutQuery$variables = Record<PropertyKey, never>;
export type AuthLayoutQuery$data = {
readonly currentTrustCenter: {
readonly darkLogoFileUrl: string | null | undefined;
readonly logoFileUrl: string | null | undefined;
};
};
export type AuthLayoutQuery = {
response: AuthLayoutQuery$data;
variables: AuthLayoutQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoFileUrl",
"storageKey": null
},
v1 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "darkLogoFileUrl",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "AuthLayoutQuery",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "currentTrustCenter",
"plural": false,
"selections": [
(v0/*: any*/),
(v1/*: any*/)
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [],
"kind": "Operation",
"name": "AuthLayoutQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "currentTrustCenter",
"plural": false,
"selections": [
(v0/*: any*/),
(v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "92bbf193e99aa98673d1926bd1fdc890",
"id": null,
"metadata": {},
"name": "AuthLayoutQuery",
"operationKind": "query",
"text": "query AuthLayoutQuery {\n currentTrustCenter {\n logoFileUrl\n darkLogoFileUrl\n id\n }\n}\n"
}
};
})();
(node as any).hash = "d76b10f9b919f4224c4c8b478e66b4f1";
export default node;

View File

@@ -1,10 +1,11 @@
import { lazy } from "@probo/react-lazy";
import {
type AppRoute,
loaderFromQueryLoader,
routeFromAppRoute,
withQueryRef,
} from "@probo/routes";
import { Fragment, lazy } from "react";
import { Fragment } from "react";
import { loadQuery } from "react-relay";
import { createBrowserRouter, redirect, useRouteError } from "react-router";
@@ -34,12 +35,17 @@ function ErrorBoundary() {
const routes = [
{
path: "/connect",
Component: lazy(() => import("./pages/auth/ConnectPageLoader")),
},
{
path: "/verify-magic-link",
Component: lazy(() => import("./pages/auth/VerifyMagicLinkPage")),
Component: lazy(() => import("#/pages/auth/AuthLayoutLoader")),
children: [
{
path: "/connect",
Component: lazy(() => import("#/pages/auth/ConnectPageLoader")),
},
{
path: "/verify-magic-link",
Component: lazy(() => import("#/pages/auth/VerifyMagicLinkPage")),
},
],
},
{
path: "/",