Add Trust Center top bar to compliance portal
Build the Trust Center top navigation in a Relay-wired layout route. The
portal now mounts a Relay environment and provider, and the root layout
loads a query whose fragment feeds the TopBar: brand, ghost-pill nav with
an active state from the router, and a guest "Get Access" button versus an
authenticated user menu. Placeholder section routes keep the nav links and
active state functional until real pages land.
Wire the Relay tagged-template transform via @rolldown/plugin-babel and
set the router basename to the /trust/{slug} path prefix so the app
resolves under its served path.
Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -25,6 +25,8 @@
|
|||||||
"relay-runtime": "^21.0.1"
|
"relay-runtime": "^21.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.29.0",
|
||||||
|
"@rolldown/plugin-babel": "^0.2.3",
|
||||||
"@tailwindcss/vite": "^4.3.1",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
"@types/node": "^25.9.3",
|
"@types/node": "^25.9.3",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
|
|||||||
@@ -14,8 +14,13 @@
|
|||||||
|
|
||||||
import { RouterProvider } from "react-router";
|
import { RouterProvider } from "react-router";
|
||||||
|
|
||||||
|
import { RelayProvider } from "#/lib/relay/RelayProvider";
|
||||||
import { router } from "#/routes";
|
import { router } from "#/routes";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return <RouterProvider router={router} />;
|
return (
|
||||||
|
<RelayProvider>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</RelayProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
116
apps/compliance-portal/src/components/TopBar/TopBar.tsx
Normal file
116
apps/compliance-portal/src/components/TopBar/TopBar.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { LockSimpleIcon } from "@phosphor-icons/react";
|
||||||
|
import { Avatar } from "@probo/ui/src/v2/Avatar/Avatar";
|
||||||
|
import { Button } from "@probo/ui/src/v2/Button/Button";
|
||||||
|
import { Link } from "@probo/ui/src/v2/Button/Link";
|
||||||
|
import { Text } from "@probo/ui/src/v2/typography/Text";
|
||||||
|
import { graphql, useFragment } from "react-relay";
|
||||||
|
import { Link as RouterLink, useLocation } from "react-router";
|
||||||
|
|
||||||
|
import type { TopBar_query$key } from "./__generated__/TopBar_query.graphql";
|
||||||
|
import { TopBarUserMenu } from "./TopBarUserMenu";
|
||||||
|
import { topBar } from "./variants";
|
||||||
|
|
||||||
|
const NAV_ITEMS = [
|
||||||
|
{ to: "/documents", label: "Documents" },
|
||||||
|
{ to: "/subprocessors", label: "Subprocessors" },
|
||||||
|
{ to: "/updates", label: "Updates" },
|
||||||
|
{ to: "/requests", label: "Requests" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const topBarFragment = graphql`
|
||||||
|
fragment TopBar_query on Query {
|
||||||
|
viewer {
|
||||||
|
id
|
||||||
|
...TopBarUserMenu_identity
|
||||||
|
}
|
||||||
|
currentTrustCenter @required(action: THROW) {
|
||||||
|
organization {
|
||||||
|
name
|
||||||
|
logo {
|
||||||
|
downloadUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface TopBarProps {
|
||||||
|
queryKey: TopBar_query$key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActive(pathname: string, to: string): boolean {
|
||||||
|
return pathname === to || pathname.startsWith(`${to}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TopBar({ queryKey }: TopBarProps) {
|
||||||
|
const data = useFragment(topBarFragment, queryKey);
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const { organization } = data.currentTrustCenter;
|
||||||
|
const organizationName = organization.name;
|
||||||
|
const logoUrl = organization.logo?.downloadUrl ?? undefined;
|
||||||
|
|
||||||
|
const slots = topBar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className={slots.bar()}>
|
||||||
|
<div className={slots.inner()}>
|
||||||
|
<RouterLink to="/" className={slots.brand()}>
|
||||||
|
<Avatar
|
||||||
|
size={1}
|
||||||
|
variant="soft"
|
||||||
|
color="neutral"
|
||||||
|
radius="small"
|
||||||
|
src={logoUrl}
|
||||||
|
fallback={organizationName.charAt(0) || "?"}
|
||||||
|
className={slots.logo()}
|
||||||
|
/>
|
||||||
|
<Text size={2} weight="medium" color="neutral" highContrast>
|
||||||
|
{organizationName}
|
||||||
|
</Text>
|
||||||
|
<Text size={2} color="neutral">
|
||||||
|
Compliance Portal
|
||||||
|
</Text>
|
||||||
|
</RouterLink>
|
||||||
|
|
||||||
|
<div className={slots.spacer()} />
|
||||||
|
|
||||||
|
<nav className={slots.nav()}>
|
||||||
|
{NAV_ITEMS.map(item => (
|
||||||
|
<Link
|
||||||
|
key={item.to}
|
||||||
|
to={item.to}
|
||||||
|
variant="ghost"
|
||||||
|
color="neutral"
|
||||||
|
size={2}
|
||||||
|
active={isActive(pathname, item.to)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
{data.viewer == null
|
||||||
|
? (
|
||||||
|
<Button variant="solid" color="neutral" highContrast iconStart={<LockSimpleIcon />}>
|
||||||
|
Get Access
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
: <TopBarUserMenu identityKey={data.viewer} />}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { AvatarSkeleton } from "@probo/ui/src/v2/Avatar/AvatarSkeleton";
|
||||||
|
import { ButtonSkeleton } from "@probo/ui/src/v2/Button/ButtonSkeleton";
|
||||||
|
import { TextSkeleton } from "@probo/ui/src/v2/typography/TextSkeleton";
|
||||||
|
|
||||||
|
import { topBar } from "./variants";
|
||||||
|
|
||||||
|
const NAV_ITEM_KEYS = ["documents", "subprocessors", "updates", "requests"] as const;
|
||||||
|
|
||||||
|
// Loading placeholder paired with TopBar: reuses the same layout slots with
|
||||||
|
// skeleton primitives. Imports no Relay / Base UI, so it renders instantly.
|
||||||
|
export function TopBarSkeleton() {
|
||||||
|
const slots = topBar();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={slots.bar()}>
|
||||||
|
<div className={slots.inner()}>
|
||||||
|
<div className={slots.brand()}>
|
||||||
|
<AvatarSkeleton size={1} radius="small" />
|
||||||
|
<TextSkeleton size={2} className="w-20" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={slots.spacer()} />
|
||||||
|
|
||||||
|
<nav className={slots.nav()}>
|
||||||
|
{NAV_ITEM_KEYS.map(key => (
|
||||||
|
<TextSkeleton key={key} size={2} className="mx-3 w-16" />
|
||||||
|
))}
|
||||||
|
<ButtonSkeleton size={2} />
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { CaretDownIcon, SignOutIcon, UserIcon } from "@phosphor-icons/react";
|
||||||
|
import { Avatar } from "@probo/ui/src/v2/Avatar/Avatar";
|
||||||
|
import { Dropdown } from "@probo/ui/src/v2/Dropdown/Dropdown";
|
||||||
|
import { DropdownGroupLabel } from "@probo/ui/src/v2/Dropdown/DropdownGroupLabel";
|
||||||
|
import { DropdownItem } from "@probo/ui/src/v2/Dropdown/DropdownItem";
|
||||||
|
import { DropdownPopup } from "@probo/ui/src/v2/Dropdown/DropdownPopup";
|
||||||
|
import { DropdownSeparator } from "@probo/ui/src/v2/Dropdown/DropdownSeparator";
|
||||||
|
import { DropdownTrigger } from "@probo/ui/src/v2/Dropdown/DropdownTrigger";
|
||||||
|
import { Text } from "@probo/ui/src/v2/typography/Text";
|
||||||
|
import { graphql, useFragment } from "react-relay";
|
||||||
|
|
||||||
|
import type { TopBarUserMenu_identity$key } from "./__generated__/TopBarUserMenu_identity.graphql";
|
||||||
|
import { topBarUserMenuTrigger } from "./variants";
|
||||||
|
|
||||||
|
const topBarUserMenuFragment = graphql`
|
||||||
|
fragment TopBarUserMenu_identity on Identity {
|
||||||
|
fullName
|
||||||
|
email
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface TopBarUserMenuProps {
|
||||||
|
identityKey: TopBarUserMenu_identity$key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TopBarUserMenu({ identityKey }: TopBarUserMenuProps) {
|
||||||
|
const identity = useFragment(topBarUserMenuFragment, identityKey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger
|
||||||
|
render={(
|
||||||
|
<button type="button" className={topBarUserMenuTrigger()} aria-label={identity.fullName}>
|
||||||
|
<Avatar
|
||||||
|
size={1}
|
||||||
|
variant="soft"
|
||||||
|
color="gold"
|
||||||
|
radius="small"
|
||||||
|
fallback={<UserIcon />}
|
||||||
|
/>
|
||||||
|
<Text size={2} weight="medium" color="neutral" highContrast>
|
||||||
|
{identity.fullName}
|
||||||
|
</Text>
|
||||||
|
<CaretDownIcon className="size-4 text-sand-11" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<DropdownPopup align="end">
|
||||||
|
<DropdownGroupLabel>{identity.email}</DropdownGroupLabel>
|
||||||
|
<DropdownSeparator />
|
||||||
|
<DropdownItem color="error" iconStart={<SignOutIcon />}>
|
||||||
|
Sign out
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownPopup>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
apps/compliance-portal/src/components/TopBar/variants.ts
Normal file
38
apps/compliance-portal/src/components/TopBar/variants.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { tv } from "tailwind-variants/lite";
|
||||||
|
|
||||||
|
// Trust Center top navigation bar. Slots are shared by the live TopBar and its
|
||||||
|
// skeleton so the loading placeholder is structurally identical.
|
||||||
|
export const topBar = tv({
|
||||||
|
slots: {
|
||||||
|
bar: "flex h-14 items-center bg-sand-1 px-8",
|
||||||
|
inner: "mx-auto flex w-full max-w-[1024px] items-center gap-10",
|
||||||
|
brand: "flex items-center gap-2",
|
||||||
|
logo: "shrink-0",
|
||||||
|
spacer: "h-px flex-1",
|
||||||
|
nav: "flex items-center gap-1",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rounded pill that opens the authenticated user menu.
|
||||||
|
export const topBarUserMenuTrigger = tv({
|
||||||
|
base: [
|
||||||
|
"flex h-8 items-center gap-2 rounded-full py-1 pr-2.5 pl-1",
|
||||||
|
"cursor-pointer outline-none transition-colors select-none",
|
||||||
|
"hover:bg-sand-3 data-[popup-open]:bg-sand-3",
|
||||||
|
"focus-visible:ring-2 focus-visible:ring-sand-8 focus-visible:ring-offset-1 focus-visible:ring-offset-sand-1",
|
||||||
|
],
|
||||||
|
});
|
||||||
26
apps/compliance-portal/src/lib/relay/RelayProvider.tsx
Normal file
26
apps/compliance-portal/src/lib/relay/RelayProvider.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import type { PropsWithChildren } from "react";
|
||||||
|
import { RelayEnvironmentProvider } from "react-relay";
|
||||||
|
|
||||||
|
import { environment } from "#/lib/relay/environment";
|
||||||
|
|
||||||
|
export function RelayProvider({ children }: PropsWithChildren) {
|
||||||
|
return (
|
||||||
|
<RelayEnvironmentProvider environment={environment}>
|
||||||
|
{children}
|
||||||
|
</RelayEnvironmentProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
apps/compliance-portal/src/lib/relay/environment.ts
Normal file
29
apps/compliance-portal/src/lib/relay/environment.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { makeFetchQuery } from "@probo/relay";
|
||||||
|
import { Environment, Network, RecordSource, Store } from "relay-runtime";
|
||||||
|
|
||||||
|
import { buildEndpoint } from "#/lib/http/endpoint";
|
||||||
|
|
||||||
|
const store = new Store(new RecordSource(), {
|
||||||
|
queryCacheExpirationTime: 1 * 60 * 1000,
|
||||||
|
gcReleaseBufferSize: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const environment = new Environment({
|
||||||
|
configName: "complianceportal",
|
||||||
|
network: Network.create(makeFetchQuery(buildEndpoint())),
|
||||||
|
store,
|
||||||
|
});
|
||||||
@@ -12,11 +12,30 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import type { PreloadedQuery } from "react-relay";
|
||||||
|
import { graphql, usePreloadedQuery } from "react-relay";
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
|
|
||||||
export default function MainLayout() {
|
import { TopBar } from "#/components/TopBar/TopBar";
|
||||||
|
|
||||||
|
import type { MainLayoutQuery } from "./__generated__/MainLayoutQuery.graphql";
|
||||||
|
|
||||||
|
export const mainLayoutQuery = graphql`
|
||||||
|
query MainLayoutQuery {
|
||||||
|
...TopBar_query
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface MainLayoutProps {
|
||||||
|
queryRef: PreloadedQuery<MainLayoutQuery>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MainLayout({ queryRef }: MainLayoutProps) {
|
||||||
|
const data = usePreloadedQuery<MainLayoutQuery>(mainLayoutQuery, queryRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-sand-1">
|
<div className="min-h-screen bg-sand-1">
|
||||||
|
<TopBar queryKey={data} />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
34
apps/compliance-portal/src/pages/MainLayoutLoader.tsx
Normal file
34
apps/compliance-portal/src/pages/MainLayoutLoader.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useQueryLoader } from "react-relay";
|
||||||
|
|
||||||
|
import type { MainLayoutQuery } from "./__generated__/MainLayoutQuery.graphql";
|
||||||
|
import { MainLayout, mainLayoutQuery } from "./MainLayout";
|
||||||
|
import { MainLayoutSkeleton } from "./MainLayoutSkeleton";
|
||||||
|
|
||||||
|
export default function MainLayoutLoader() {
|
||||||
|
const [queryRef, loadQuery] = useQueryLoader<MainLayoutQuery>(mainLayoutQuery);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadQuery({});
|
||||||
|
}, [loadQuery]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <MainLayoutSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MainLayout queryRef={queryRef} />;
|
||||||
|
}
|
||||||
23
apps/compliance-portal/src/pages/MainLayoutSkeleton.tsx
Normal file
23
apps/compliance-portal/src/pages/MainLayoutSkeleton.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { TopBarSkeleton } from "#/components/TopBar/TopBarSkeleton";
|
||||||
|
|
||||||
|
export function MainLayoutSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-sand-1">
|
||||||
|
<TopBarSkeleton />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
apps/compliance-portal/src/pages/PlaceholderPage.tsx
Normal file
30
apps/compliance-portal/src/pages/PlaceholderPage.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { Heading } from "@probo/ui/src/v2/typography/Heading";
|
||||||
|
import { useLocation } from "react-router";
|
||||||
|
|
||||||
|
// Temporary stub for the Trust Center sections so the top-bar nav links resolve
|
||||||
|
// and the active state renders. Real pages replace these per section.
|
||||||
|
export default function PlaceholderPage() {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const segment = pathname.replace(/^\//, "").split("/")[0] ?? "";
|
||||||
|
const title = segment.charAt(0).toUpperCase() + segment.slice(1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="mx-auto w-full max-w-[1024px] px-8 py-10">
|
||||||
|
<Heading>{title}</Heading>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,19 +17,40 @@ import { type AppRoute, routeFromAppRoute } from "@probo/routes";
|
|||||||
import { createBrowserRouter } from "react-router";
|
import { createBrowserRouter } from "react-router";
|
||||||
|
|
||||||
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
|
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
|
||||||
|
import { getPathPrefix } from "#/lib/http/pathPrefix";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
Fallback: PageSkeleton,
|
Fallback: PageSkeleton,
|
||||||
Component: lazy(() => import("#/pages/MainLayout")),
|
Component: lazy(() => import("#/pages/MainLayoutLoader")),
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
Component: lazy(() => import("#/pages/HomePage")),
|
Component: lazy(() => import("#/pages/HomePage")),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "documents",
|
||||||
|
Component: lazy(() => import("#/pages/PlaceholderPage")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "subprocessors",
|
||||||
|
Component: lazy(() => import("#/pages/PlaceholderPage")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "updates",
|
||||||
|
Component: lazy(() => import("#/pages/PlaceholderPage")),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "requests",
|
||||||
|
Component: lazy(() => import("#/pages/PlaceholderPage")),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] satisfies AppRoute[];
|
] satisfies AppRoute[];
|
||||||
|
|
||||||
export const router = createBrowserRouter(routes.map(routeFromAppRoute));
|
// The portal is served under a /trust/{slug} path prefix (or a bare custom
|
||||||
|
// domain). Match the router basename to that prefix so the routes resolve.
|
||||||
|
export const router = createBrowserRouter(routes.map(routeFromAppRoute), {
|
||||||
|
basename: getPathPrefix() || "/",
|
||||||
|
});
|
||||||
|
|||||||
@@ -14,13 +14,16 @@
|
|||||||
|
|
||||||
import { fileURLToPath, URL } from "node:url";
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
|
||||||
|
import babel from "@rolldown/plugin-babel";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
|
// @vitejs/plugin-react@6 (Vite 8) no longer runs Babel, so the Relay tagged
|
||||||
|
// template transform is applied via @rolldown/plugin-babel instead.
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), babel({ plugins: ["relay"] }), tailwindcss()],
|
||||||
build: {
|
build: {
|
||||||
assetsDir: "assets",
|
assetsDir: "assets",
|
||||||
},
|
},
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -44,6 +44,8 @@
|
|||||||
"relay-runtime": "^21.0.1"
|
"relay-runtime": "^21.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.29.0",
|
||||||
|
"@rolldown/plugin-babel": "^0.2.3",
|
||||||
"@tailwindcss/vite": "^4.3.1",
|
"@tailwindcss/vite": "^4.3.1",
|
||||||
"@types/node": "^25.9.3",
|
"@types/node": "^25.9.3",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.17",
|
||||||
|
|||||||
Reference in New Issue
Block a user