Add trust center v2

Signed-off-by: Jonathan <contact@grafikart.fr>
This commit is contained in:
Jonathan
2025-09-26 16:42:52 +02:00
committed by Sacha Al Himdani
parent 44898be9d3
commit 55e85e5f67
182 changed files with 4609 additions and 425 deletions

View File

@@ -176,3 +176,4 @@ goreleaser-snapshot: ## Build a snapshot release with goreleaser
.PHONY: goreleaser-check
goreleaser-check: ## Check goreleaser configuration
goreleaser check

View File

@@ -1,54 +1 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config({
extends: [
// Remove ...tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
],
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config({
plugins: {
// Add the react-x and react-dom plugins
'react-x': reactX,
'react-dom': reactDom,
},
rules: {
// other rules...
// Enable its recommended typescript rules
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
```
# Console app

View File

@@ -1,10 +1,10 @@
import { useForm } from "react-hook-form";
import type { z, ZodType } from "zod";
import type { z, ZodTypeAny } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
export function useFormWithSchema<T extends ZodType<any, any, any>>(
export function useFormWithSchema<T extends ZodTypeAny>(
schema: T,
options: Parameters<typeof useForm<z.infer<T>>>[0]
options: Parameters<typeof useForm<z.infer<T>>>[0],
) {
return useForm<z.infer<T>>({
...options,

1
apps/trust/.env.example Normal file
View File

@@ -0,0 +1 @@
VITE_API_URL=http://localhost:8080

2
apps/trust/.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
__generated__/*.graphql linguist-generated
__generated__/*.js linguist-generated

26
apps/trust/.gitignore vendored Normal file
View File

@@ -0,0 +1,26 @@
public/pdfjs
# Logs
old
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

2
apps/trust/README.md Normal file
View File

@@ -0,0 +1,2 @@
# TrustCenter app

View File

@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)

View File

@@ -0,0 +1,5 @@
schema: '../../pkg/server/api/trust/v1/schema.graphql'
extensions:
endpoints:
TrustCenter:
url: http://localhost:8080/api/trust/v1/graphql

37
apps/trust/index.html Normal file
View File

@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="/favicons/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="/favicons/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="/favicons/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="/favicons/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="/favicons/apple-touch-icon-60x60.png" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="/favicons/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="/favicons/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="/favicons/apple-touch-icon-152x152.png" />
<link rel="icon" type="image/png" href="/favicons/favicon-196x196.png" sizes="196x196" />
<link rel="icon" type="image/png" href="/favicons/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/png" href="/favicons/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/favicons/favicon-16x16.png" sizes="16x16" />
<link rel="icon" type="image/png" href="/favicons/favicon-128.png" sizes="128x128" />
<meta name="application-name" content="&nbsp;" />
<meta name="msapplication-TileColor" content="#FFFFFF" />
<meta name="msapplication-TileImage" content="/favicons/mstile-144x144.png" />
<meta name="msapplication-square70x70logo" content="/favicons/mstile-70x70.png" />
<meta name="msapplication-square150x150logo" content="/favicons/mstile-150x150.png" />
<meta name="msapplication-wide310x150logo" content="/favicons/mstile-310x150.png" />
<meta name="msapplication-square310x310logo" content="/favicons/mstile-310x310.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Trust Center</title>
</head>
<body class="text-txt-primary bg-level-0">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

57
apps/trust/package.json Normal file
View File

@@ -0,0 +1,57 @@
{
"name": "@probo/trust",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"relay": "find src -type d -name \"__generated__\" -exec rm -rf {} + && npx relay-compiler ./relay.config.json",
"build": "tsc -b && vite build",
"lint": "eslint .",
"check": "tsc --noEmit -p tsconfig.app.json",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@probo/helpers": "1.0.0",
"@probo/hooks": "1.0.0",
"@probo/i18n": "1.0.0",
"@probo/ui": "1.0.0",
"clsx": "^2.1.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.56.4",
"react-pdf": "^10.1.0",
"react-relay": "^19.0.0",
"react-router": "^7.6.0",
"relay-runtime": "^19.0.0",
"usehooks-ts": "^3.1.1",
"zod": "^3.25.71"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@tailwindcss/vite": "^4.1.7",
"@types/node": "^22.15.21",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/react-relay": "^18.2.1",
"@types/relay-runtime": "^19.0.1",
"@vitejs/plugin-react": "^4.4.1",
"babel-plugin-relay": "^19.0.0",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"graphql": "^16.11.0",
"prettier": "^3.5.3",
"relay-compiler": "^19.0.0",
"tailwindcss": "^4.1.7",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.6"
},
"volta": {
"node": "24.4.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -0,0 +1,7 @@
{
"src": "./src",
"schema": "../../pkg/server/api/trust/v1/schema.graphql",
"language": "typescript",
"eagerEsModules": true,
"noFutureProofEnums": true
}

12
apps/trust/src/App.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { RouterProvider } from "react-router";
import { router } from "./routes";
import { Toasts } from "@probo/ui";
export function App() {
return (
<>
<RouterProvider router={router} />
<Toasts />
</>
);
}

View File

@@ -0,0 +1,153 @@
import { graphql } from "relay-runtime";
import type { AuditRowFragment$key } from "./__generated__/AuditRowFragment.graphql";
import { useFragment } from "react-relay";
import {
Breadcrumb,
Button,
Dialog,
DialogContent,
FrameworkLogo,
IconArrowInbox,
IconLock,
IconMedal,
Spinner,
Table,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useIsAuthenticated } from "/hooks/useIsAuthenticated";
import type { AuditRowDownloadMutation } from "./__generated__/AuditRowDownloadMutation.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { downloadFile } from "@probo/helpers";
import type { PropsWithChildren } from "react";
import { useLocation } from "react-router";
import { RequestAccessDialog } from "/components/RequestAccessDialog.tsx";
const downloadMutation = graphql`
mutation AuditRowDownloadMutation($input: ExportReportPDFInput!) {
exportReportPDF(input: $input) {
data
}
}
`;
const auditRowFragment = graphql`
fragment AuditRowFragment on Audit {
report {
id
filename
}
framework {
id
name
}
}
`;
export function AuditRow(props: { audit: AuditRowFragment$key }) {
const audit = useFragment(auditRowFragment, props.audit);
const { __ } = useTranslate();
const isAuthenticated = useIsAuthenticated();
const [commitDownload, downloading] =
useMutationWithToasts<AuditRowDownloadMutation>(downloadMutation);
const handleDownload = () => {
if (!audit.report?.id) {
return;
}
commitDownload({
variables: {
input: {
reportId: audit.report.id,
},
},
onSuccess(response) {
downloadFile(response.exportReportPDF.data, audit.report!.filename);
},
});
};
return (
<div className="text-sm border-1 border-border-solid -mt-[1px] flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
<div className="flex items-center gap-2">
<IconMedal size={16} className="flex-none" />
{audit.framework.name}
</div>
{audit.report ? (
isAuthenticated ? (
<Button
className="w-full md:w-max"
variant="secondary"
disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload}
>
{__("Download")}
</Button>
) : (
<RequestAccessDialog>
<Button
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
>
{__("Request access")}
</Button>
</RequestAccessDialog>
)
) : (
<span className=" text-txt-secondary">{__("No report")}</span>
)}
</div>
);
}
export function AuditRowAvatar(props: { audit: AuditRowFragment$key }) {
const audit = useFragment(auditRowFragment, props.audit);
return (
<AuditDialog audit={props.audit}>
<button className="block cursor-pointer aspect-square">
<FrameworkLogo
alt={audit.framework.name}
name={audit.framework.name}
className="size-full"
/>
</button>
</AuditDialog>
);
}
function AuditDialog(
props: PropsWithChildren<{ audit: AuditRowFragment$key }>,
) {
const audit = useFragment(auditRowFragment, props.audit);
const location = useLocation();
const { __ } = useTranslate();
const items = [
{
label: __("Certifications"),
to: location.pathname,
},
{
label: audit.framework.name,
to: location.pathname,
},
];
return (
<Dialog
trigger={props.children}
className="max-w-[500px]"
title={<Breadcrumb items={items} />}
>
<DialogContent className="p-4 lg:p-8 space-y-6">
<FrameworkLogo
alt={audit.framework.name}
name={audit.framework.name}
className="size-24 block mx-auto"
/>
<h2 className="text-xl font-semibold mb-1">{audit.framework.name}</h2>
<p className="text-txt-secondary">Framework description</p>
<Table>
<AuditRow audit={props.audit} />
</Table>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,80 @@
import { graphql } from "relay-runtime";
import type { DocumentRowFragment$key } from "./__generated__/DocumentRowFragment.graphql";
import { useFragment } from "react-relay";
import {
Button,
IconArrowInbox,
IconLock,
IconPageTextLine,
Spinner,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useIsAuthenticated } from "/hooks/useIsAuthenticated";
import type { DocumentRowDownloadMutation } from "./__generated__/DocumentRowDownloadMutation.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { downloadFile } from "@probo/helpers";
import { RequestAccessDialog } from "/components/RequestAccessDialog.tsx";
const downloadMutation = graphql`
mutation DocumentRowDownloadMutation($input: ExportDocumentPDFInput!) {
exportDocumentPDF(input: $input) {
data
}
}
`;
const documentRowFragment = graphql`
fragment DocumentRowFragment on Document {
id
title
}
`;
export function DocumentRow(props: { document: DocumentRowFragment$key }) {
const document = useFragment(documentRowFragment, props.document);
const { __ } = useTranslate();
const isAuthenticated = useIsAuthenticated();
const [commitDownload, downloading] =
useMutationWithToasts<DocumentRowDownloadMutation>(downloadMutation);
const handleDownload = () => {
commitDownload({
variables: {
input: {
documentId: document.id,
},
},
onSuccess(response) {
downloadFile(response.exportDocumentPDF.data, document.title);
},
});
};
return (
<div className="text-sm border-1 border-border-solid -mt-[1px] flex gap-3 flex-col md:flex-row md:justify-between px-6 py-3">
<div className="flex items-center gap-2">
<IconPageTextLine size={16} className=" flex-none" />
{document.title}
</div>
{isAuthenticated ? (
<Button
className="w-full md:w-max"
variant="secondary"
disabled={downloading}
icon={downloading ? Spinner : IconArrowInbox}
onClick={handleDownload}
>
{__("Download")}
</Button>
) : (
<RequestAccessDialog>
<Button
className="w-full md:w-max"
variant="secondary"
icon={IconLock}
>
{__("Request access")}
</Button>
</RequestAccessDialog>
)}
</div>
);
}

View File

@@ -0,0 +1,115 @@
import { Button, Card, Logo, Spinner } from "@probo/ui";
import { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useEffect } from "react";
import { PDFPreview } from "./PDFPreview";
import { useWindowSize } from "usehooks-ts";
import clsx from "clsx";
import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
const signMutation = graphql`
mutation NDADialogSignMutation($input: AcceptNonDisclosureAgreementInput!) {
acceptNonDisclosureAgreement(input: $input) {
success
}
}
`;
export function NDADialog({
name,
url,
fileName,
trustCenterId,
}: {
name: string;
url?: string | null;
fileName?: string | null;
trustCenterId: string;
}) {
const { __ } = useTranslate();
useEffect(() => {
document.body.style.setProperty("overflow", "hidden");
return () => {
document.body.style.removeProperty("overflow");
};
}, []);
const { width } = useWindowSize();
const isMobile = width < 1100;
const isDesktop = !isMobile;
const [commitSigning, isSigning] = useMutationWithToasts(signMutation, {
onSuccess: () => {
window.location.reload();
},
});
const handleSign = () => {
commitSigning({
variables: {
input: {
trustCenterId,
},
},
});
};
return (
<div className="fixed inset-0 bg-level-2 z-100 flex flex-col lg:h-screen">
<header className="flex items-center h-12 justify-between border-b border-border-solid px-4 flex-none">
<Logo />
</header>
<div className="grid lg:grid-cols-2 min-h-0 h-full">
<div className="max-w-[440px] mx-auto py-20">
<h1 className="text-2xl font-semibold mb-4">
{__("Review & Sign NDA")}
</h1>
<p className="text-txt-secondary">
{sprintf(
__(
"Access to %s Trust Center documents requires signing a Non-Disclosure Agreement (NDA). Please review the agreement below. Once signed, youll receive immediate access to the requested documents.",
),
name,
)}
</p>
{isMobile && url && (
<Card className="flex justify-between py-3 px-4 text-sm items-center my-6">
{fileName}
<Button variant="secondary" asChild>
<a target="_blank" rel="noopener noreferrer" href={url}>
{__("View document")}
</a>
</Button>
</Card>
)}
<Button
onClick={handleSign}
className="h-10 w-full my-8"
disabled={isSigning}
icon={isSigning ? Spinner : undefined}
>
{__("Review & Sign")}
</Button>
<p className="text-xs text-txt-secondary">
{__(
"By clicking Review & Sign, you agree to the terms of this NDA. If you have questions about the NDA, please contact security@probo.com.",
)}
</p>
<a
href="https://www.getprobo.com/"
className={clsx(
"flex gap-1 text-sm font-medium text-txt-tertiary items-center w-max mx-auto",
isMobile ? "mt-15" : "mt-30",
)}
>
Powered by <Logo withPicto className="h-6" />
</a>
</div>
{isDesktop && (
<div className="bg-subtle h-full border-l border-border-solid min-h-0">
{url && <PDFPreview src={url} name={fileName ?? ""} />}
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,125 @@
import { useTranslate } from "@probo/i18n";
import { Button, Card, IconBlock, IconLock, IconMedal } from "@probo/ui";
import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql";
import type { PropsWithChildren } from "react";
import { domain } from "@probo/helpers";
import { AuditRowAvatar } from "./AuditRow";
import { RequestAccessDialog } from "./RequestAccessDialog";
import { useIsAuthenticated } from "/hooks/useIsAuthenticated";
export function OrganizationSidebar({
trustCenter,
}: {
trustCenter: TrustGraphQuery$data["trustCenterBySlug"];
}) {
const { __ } = useTranslate();
const isAuthenticated = useIsAuthenticated();
if (!trustCenter) {
return null;
}
return (
<Card className="p-6 relative overflow-hidden border-b-1 border-border-low isolate">
<div className="h-21 bg-[#044E4114] absolute top-0 left-0 right-0 -z-1"></div>
{trustCenter.organization.logoUrl ? (
<img
alt=""
src={trustCenter.organization.logoUrl}
className="size-24 rounded-2xl border border-border-mid shadow-mid"
/>
) : (
<div className="size-24 rounded-2xl border border-border-mid bg-level-1 shadow-mid" />
)}
<h1 className="text-2xl mt-6">{trustCenter.organization.name}</h1>
<p className="text-sm text-txt-secondary mt-1">
{trustCenter.organization.description}
</p>
<hr className="my-6 -mx-6 h-[1px] bg-border-low border-none" />
{/* Business information */}
<div className="space-y-4">
<h2 className="text-xs text-txt-secondary flex gap-1 items-center">
<IconBlock size={16} />
{__("Business information")}
</h2>
{trustCenter.organization.websiteUrl && (
<BusinessInfo label={__("Website")}>
<a
href={trustCenter.organization.websiteUrl}
target="_blank"
rel="noopener noreferrer"
>
<span className="text-txt-info hover:underline ">
{domain(trustCenter.organization.websiteUrl)}
</span>
</a>
</BusinessInfo>
)}
{trustCenter.organization.email && (
<BusinessInfo label={__("Contact")}>
{trustCenter.organization.email}
</BusinessInfo>
)}
{trustCenter.organization.headquarterAddress && (
<BusinessInfo label={__("HQ address")}>
{trustCenter.organization.headquarterAddress}
</BusinessInfo>
)}
<hr className="my-6 -mx-6 h-[1px] bg-border-low border-none" />
{/* Certifications */}
<div className="space-y-4">
<h2 className="text-xs text-txt-secondary flex gap-1 items-center">
<IconMedal size={16} />
{__("Certifications")}
</h2>
<div
className="grid grid-cols-4 gap-4"
style={{
gridTemplateColumns: "repeat(auto-fit, 75px",
}}
>
{trustCenter.audits.edges.map((audit) => (
<AuditRowAvatar key={audit.node.id} audit={audit.node} />
))}
{trustCenter.audits.edges.map((audit) => (
<AuditRowAvatar key={audit.node.id} audit={audit.node} />
))}
{trustCenter.audits.edges.map((audit) => (
<AuditRowAvatar key={audit.node.id} audit={audit.node} />
))}
</div>
</div>
<hr className="my-6 -mx-6 h-[1px] bg-border-low border-none" />
{/* Actions */}
{!isAuthenticated && (
<RequestAccessDialog>
<Button variant="primary" icon={IconLock} className="w-full h-10">
{__("Request access")}
</Button>
</RequestAccessDialog>
)}
{/* <Button variant="secondary" icon={IconMail} className="w-full h-10">
{__("Subscribe to updates")}
</Button>*/}
</div>
</Card>
);
}
function BusinessInfo({
children,
label,
}: PropsWithChildren<{ label: string }>) {
return (
<div>
<div className="text-xs text-txt-secondary">{label}</div>
<div className="text-sm">{children}</div>
</div>
);
}

View File

@@ -0,0 +1,130 @@
import { Document, Page, pdfjs } from "react-pdf";
import "react-pdf/dist/Page/TextLayer.css";
import "react-pdf/dist/Page/AnnotationLayer.css";
import { type ComponentProps, useRef, useState } from "react";
import {
IconArrowInbox,
IconChevronLeft,
IconChevronRight,
IconPlusLarge,
Spinner,
} from "@probo/ui";
import { times } from "@probo/helpers";
import { IconMinusLarge } from "@probo/ui/src/Atoms/Icons/IconMinusLarge.tsx";
// Worker for PDF.js
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
const btnClass =
"size-8 grid place-items-center hover:bg-secondary-hover cursor-pointer rounded-sm disabled:opacity-30 transition-all";
export function PDFPreview({ src, name }: { src: string; name?: string }) {
const [numPages, setNumPages] = useState(0);
const [scale, setScale] = useState(1.0);
const [currentPage, setCurrentPage] = useState(1);
const documentRef: ComponentProps<typeof Document>["ref"] = useRef(null);
const wrapperRef = useRef<HTMLDivElement>(null);
const onDocumentLoadSuccess: ComponentProps<
typeof Document
>["onLoadSuccess"] = (document) => {
setNumPages(document.numPages);
setCurrentPage(1);
};
const zoomFactor = (factor: number) => () => {
setScale(scale * factor);
};
const movePage = (direction: 1 | -1) => () => {
if (currentPage === 1 && direction === -1) {
return;
}
const newPage = currentPage + direction;
const page = documentRef.current?.pages.current[newPage - 1];
if (!page) {
return;
}
page.scrollIntoView({
behavior: "smooth",
block: "start",
inline: "center",
});
setCurrentPage(newPage);
};
const resolveCurrentPage = () => {
if (!wrapperRef.current) {
return;
}
const pages = documentRef.current?.pages.current;
if (!pages?.length) {
return;
}
const parentRect = wrapperRef.current.getBoundingClientRect();
const parentMiddleY = parentRect.top + parentRect.height / 2;
for (let i = 0; i < pages.length; i++) {
const childRect = pages[i].getBoundingClientRect();
if (childRect.top <= parentMiddleY && childRect.bottom >= parentMiddleY) {
return setCurrentPage(i + 1);
}
}
};
return (
<div className="grid grid-rows-[max-content_1fr] h-full bg-subtle">
{/* Custom Zoom Controls */}
<nav className="flex-none flex items-center gap-2 bg-level-1 py-3 text-sm pl-4 pr-3 text-txt-primary">
<div>{name}</div>
<div className="mx-auto flex gap-1 items-center">
<button
onClick={movePage(-1)}
className={btnClass}
disabled={currentPage === 1}
>
<IconChevronLeft size={16} />
</button>
<div>
{currentPage} / {numPages}
</div>
<button onClick={movePage(1)} className={btnClass}>
<IconChevronRight size={16} />
</button>
</div>
<button onClick={zoomFactor(0.8)} className={btnClass}>
<IconMinusLarge size={16} />
</button>
<button onClick={zoomFactor(1.2)} className={btnClass}>
<IconPlusLarge size={16} />
</button>
<button onClick={zoomFactor(1.2)} className={btnClass}>
<IconArrowInbox size={16} />
</button>
</nav>
{/* PDF Document */}
<div
className="overflow-auto scroll-p-6"
onScrollEnd={resolveCurrentPage}
ref={wrapperRef}
>
<Document
file={src}
onLoadSuccess={onDocumentLoadSuccess}
className="flex flex-col gap-4 py-10"
ref={documentRef}
>
{numPages === 0 && <Spinner className="mx-auto" />}
{times(numPages, (index) => (
<Page
className="w-max h-max mx-auto shadow-mid"
key={index.toString()}
pageNumber={index + 1}
scale={scale} // Apply zoom via scale prop
/>
))}
</Document>
</div>
</div>
);
}

View File

@@ -0,0 +1,88 @@
import { useLocation, useRouteError } from "react-router";
import { IconPageCross } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { useEffect, useRef } from "react";
const classNames = {
wrapper: "py-10 text-center space-y-2 ",
title: "text-2xl flex gap-2 font-semibold items-center justify-center",
description: "text-base text-txt-tertiary",
detail:
"text-sm text-txt-tertiary font-mono text-start border border-border-low p-2 rounded bg-level-1 mt-2",
};
type Props = {
resetErrorBoundary?: () => void;
error?: string;
};
export function PageError({ resetErrorBoundary, error: propsError }: Props) {
const error = useRouteError() ?? propsError;
const { __ } = useTranslate();
const location = useLocation();
const baseLocation = useRef(location);
// Reset error boundary on page change
useEffect(() => {
if (
location.pathname !== baseLocation.current.pathname &&
resetErrorBoundary
) {
resetErrorBoundary();
}
}, [location, resetErrorBoundary]);
if (!error || (error && error.toString().includes("PAGE_NOT_FOUND"))) {
return (
<div className={classNames.wrapper}>
<h1 className={classNames.title}>
<IconPageCross size={26} />
{__("Page not found")}
</h1>
<p className={classNames.description}>
{__("The page you are looking for does not exist")}
</p>
</div>
);
}
if (
error
.toString()
.toLowerCase()
.match(/(token|expired|invalid|401|unauthorized)/)
) {
const isExpiredToken = error.toString().toLowerCase().includes("expired");
const title = isExpiredToken
? __("Expired token")
: __("Invalid Access Link");
const description = isExpiredToken
? __(
"This access link has expired. Trust center access links are valid for 7 days for security reasons."
)
: __(
"This access link is not valid. It may have been revoked or the link might be incorrect."
);
return (
<div className={classNames.wrapper}>
<h1 className={classNames.title}>
<IconPageCross size={26} />
{title}
</h1>
<p className={classNames.description}>{description}</p>
</div>
);
}
return (
<div className={classNames.wrapper}>
<h1 className={classNames.title}>{__("Unexpected error :(")}</h1>
<details>
<summary className={classNames.description}>
{__("Something went wrong")}
</summary>
<p className={classNames.detail}>{error.toString()}</p>
</details>
</div>
);
}

View File

@@ -0,0 +1,116 @@
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogTitle,
Field,
useDialogRef,
useToast,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { sprintf } from "@probo/helpers";
import { z } from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
import { graphql } from "relay-runtime";
import { useMutationWithToasts } from "/hooks/useMutationWithToast";
import { useTrustCenter } from "/hooks/useTrustCenter";
type Props = {
children: React.ReactNode;
};
const schema = z.object({
name: z.string(),
email: z.string().email(),
});
const requestAccessMutation = graphql`
mutation RequestAccessDialogMutation($input: CreateTrustCenterAccessInput!) {
createTrustCenterAccess(input: $input) {
trustCenterAccess {
id
}
}
}
`;
export function RequestAccessDialog({ children }: Props) {
const trustCenter = useTrustCenter();
const { toast } = useToast();
const { __ } = useTranslate();
const { handleSubmit, register } = useFormWithSchema(schema, {
defaultValues: {
name: "",
email: "",
},
});
const dialogRef = useDialogRef();
const [commitRequestAccess, isRequestingAccess] = useMutationWithToasts(
requestAccessMutation,
{
onSuccess: () => {
toast({
title: __("Success"),
description: __("Access request submitted successfully"),
variant: "success",
});
dialogRef.current?.close();
},
},
);
const onSubmit = handleSubmit((data) => {
commitRequestAccess({
variables: {
input: {
trustCenterId: trustCenter.id,
name: data.name,
email: data.email,
},
},
});
});
return (
<Dialog
ref={dialogRef}
trigger={children}
className="max-w-[500px] text-txt-primary"
>
<form onSubmit={onSubmit}>
<DialogTitle className="text-2xl font-semibold mb-4 pt-4 md:pt-8 px-4 md:px-8">
{__("Request access to documentation")}
</DialogTitle>
<DialogContent className="px-4 md:px-8 pb-4 md:pb-8 text-txt-primary">
<p className="text-txt-secondary mb-4">
{sprintf(
__(
"Request access to %s's Trust Center. Your request will be reviewed and you will receive an email notification with access instructions if approved.",
),
trustCenter.organization.name,
)}
</p>
<div className="space-y-4">
<Field
label={__("Full name")}
placeholder="John Doe"
{...register("name")}
type="text"
/>
<Field
label={__("Email")}
placeholder="john.doe@acme.com"
{...register("email")}
type="email"
/>
</div>
</DialogContent>
<DialogFooter>
<Button disabled={isRequestingAccess} type="submit">
{__("Continue")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}

View File

@@ -0,0 +1,9 @@
import type { PropsWithChildren } from "react";
export function RowHeader({ children }: PropsWithChildren) {
return (
<div className="bg-subtle text-xs font-medium text-txt-tertiary uppercase ">
{children}
</div>
);
}

View File

@@ -0,0 +1,18 @@
import type { PropsWithChildren } from "react";
import clsx from "clsx";
export function Rows({
children,
className,
}: PropsWithChildren<{ className?: string }>) {
return (
<div
className={clsx(
"*:first:rounded-t-lg *:last:rounded-b-lg *:px-6 *:py-3 *:-mt-[1px] *:border *:border-border-solid",
className,
)}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,25 @@
import { Skeleton, TabLink, Tabs } from "@probo/ui";
import { TabSkeleton } from "./TabSkeleton";
import { useParams } from "react-router";
import { useTranslate } from "@probo/i18n";
export function MainSkeleton() {
const { slug } = useParams<{ slug: string }>();
const baseTabUrl = `/trust/${slug}`;
const { __ } = useTranslate();
return (
<div className="grid grid-cols-1 max-w-[1280px] mx-4 pt-6 gap-4 lg:mx-auto lg:gap-10 lg:pt-20 lg:grid-cols-[400px_1fr] ">
<Skeleton className="w-full h-300" />
<main>
<Tabs className="mb-8">
<TabLink to={`${baseTabUrl}/overview`}>{__("Overview")}</TabLink>
<TabLink to={`${baseTabUrl}/documents`}>{__("Documents")}</TabLink>
<TabLink to={`${baseTabUrl}/subprocessors`}>
{__("Subprocessors")}
</TabLink>
</Tabs>
<TabSkeleton />
</main>
</div>
);
}

View File

@@ -0,0 +1,11 @@
import { Skeleton } from "@probo/ui";
export function TabSkeleton() {
return (
<div className="h-64">
<Skeleton className="h-6 w-[110px] mb-1" />
<Skeleton className="h-6 w-[250px] mb-4" />
<Skeleton className="w-full h-[180px]" />
</div>
);
}

View File

@@ -0,0 +1,53 @@
import { graphql } from "relay-runtime";
import type { VendorRowFragment$key } from "./__generated__/VendorRowFragment.graphql";
import { useFragment } from "react-relay";
import { IconPin } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { faviconUrl, getCountryName } from "@probo/helpers";
const vendorRowFragment = graphql`
fragment VendorRowFragment on Vendor {
id
name
category
websiteUrl
privacyPolicyUrl
countries
}
`;
export function VendorRow(props: { vendor: VendorRowFragment$key }) {
const vendor = useFragment(vendorRowFragment, props.vendor);
const logo = faviconUrl(vendor.websiteUrl);
const { __ } = useTranslate();
return (
<div className="flex text-sm leading-tight gap-3 md:items-center">
{logo ? (
<img src={logo} className="size-8 md:size-6 flex-none" alt="" />
) : (
<div className="size-8 md:size-6 flex-none" />
)}
<div className="flex flex-col md:flex-row flex-1 gap-0.5">
<div>{vendor.name}</div>
{vendor.privacyPolicyUrl && (
<a
href={vendor.privacyPolicyUrl}
target="_blank"
className="text-txt-info md:mx-auto"
>
{vendor.privacyPolicyUrl.split("//").at(-1)}
</a>
)}
<div className="flex gap-1 text-txt-secondary items-center">
<IconPin size={16} className="flex-none" />
<span>
{vendor.countries
.map((country) => getCountryName(__, country))
.join(", ")}
</span>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,92 @@
/**
* @generated SignedSource<<ac19789e6716697e6723a0a7b108cb81>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type ExportReportPDFInput = {
reportId: string;
};
export type AuditRowDownloadMutation$variables = {
input: ExportReportPDFInput;
};
export type AuditRowDownloadMutation$data = {
readonly exportReportPDF: {
readonly data: string;
};
};
export type AuditRowDownloadMutation = {
response: AuditRowDownloadMutation$data;
variables: AuditRowDownloadMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "ExportReportPDFPayload",
"kind": "LinkedField",
"name": "exportReportPDF",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "data",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "AuditRowDownloadMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "AuditRowDownloadMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "79de6a9f2755587818dac20ec392b8e7",
"id": null,
"metadata": {},
"name": "AuditRowDownloadMutation",
"operationKind": "mutation",
"text": "mutation AuditRowDownloadMutation(\n $input: ExportReportPDFInput!\n) {\n exportReportPDF(input: $input) {\n data\n }\n}\n"
}
};
})();
(node as any).hash = "e3d7ceb7a0da979dad83b1b8289e192e";
export default node;

View File

@@ -0,0 +1,89 @@
/**
* @generated SignedSource<<5ad8bcd5ca3b7635248d13cef0a24edf>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type AuditRowFragment$data = {
readonly framework: {
readonly id: string;
readonly name: string;
};
readonly report: {
readonly filename: string;
readonly id: string;
} | null | undefined;
readonly " $fragmentType": "AuditRowFragment";
};
export type AuditRowFragment$key = {
readonly " $data"?: AuditRowFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"AuditRowFragment">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "AuditRowFragment",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Report",
"kind": "LinkedField",
"name": "report",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "filename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "Framework",
"kind": "LinkedField",
"name": "framework",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
}
],
"storageKey": null
}
],
"type": "Audit",
"abstractKey": null
};
})();
(node as any).hash = "255e447eb5ac9b4cb889e7a8f0463902";
export default node;

View File

@@ -0,0 +1,92 @@
/**
* @generated SignedSource<<cae4044b1357cd4510e12f83f19001ba>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type ExportDocumentPDFInput = {
documentId: string;
};
export type DocumentRowDownloadMutation$variables = {
input: ExportDocumentPDFInput;
};
export type DocumentRowDownloadMutation$data = {
readonly exportDocumentPDF: {
readonly data: string;
};
};
export type DocumentRowDownloadMutation = {
response: DocumentRowDownloadMutation$data;
variables: DocumentRowDownloadMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "ExportDocumentPDFPayload",
"kind": "LinkedField",
"name": "exportDocumentPDF",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "data",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "DocumentRowDownloadMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "DocumentRowDownloadMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "126681ef86a7a6aa5c831bdf53b7cf3e",
"id": null,
"metadata": {},
"name": "DocumentRowDownloadMutation",
"operationKind": "mutation",
"text": "mutation DocumentRowDownloadMutation(\n $input: ExportDocumentPDFInput!\n) {\n exportDocumentPDF(input: $input) {\n data\n }\n}\n"
}
};
})();
(node as any).hash = "8e48a364a7f9cf3d3a51a9db60ff792f";
export default node;

View File

@@ -0,0 +1,50 @@
/**
* @generated SignedSource<<266bdec238fe7397f3cff10dc7da8fc6>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type DocumentRowFragment$data = {
readonly id: string;
readonly title: string;
readonly " $fragmentType": "DocumentRowFragment";
};
export type DocumentRowFragment$key = {
readonly " $data"?: DocumentRowFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"DocumentRowFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "DocumentRowFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
}
],
"type": "Document",
"abstractKey": null
};
(node as any).hash = "437d68812bcc68724d2e347fea22b5e3";
export default node;

View File

@@ -0,0 +1,92 @@
/**
* @generated SignedSource<<febdedca828f8b6dc384f0d4f08e02f8>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type AcceptNonDisclosureAgreementInput = {
trustCenterId: string;
};
export type NDADialogSignMutation$variables = {
input: AcceptNonDisclosureAgreementInput;
};
export type NDADialogSignMutation$data = {
readonly acceptNonDisclosureAgreement: {
readonly success: boolean;
};
};
export type NDADialogSignMutation = {
response: NDADialogSignMutation$data;
variables: NDADialogSignMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "AcceptNonDisclosureAgreementPayload",
"kind": "LinkedField",
"name": "acceptNonDisclosureAgreement",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "success",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "NDADialogSignMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "NDADialogSignMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "130cfc307dca0525194e0103a0548bd0",
"id": null,
"metadata": {},
"name": "NDADialogSignMutation",
"operationKind": "mutation",
"text": "mutation NDADialogSignMutation(\n $input: AcceptNonDisclosureAgreementInput!\n) {\n acceptNonDisclosureAgreement(input: $input) {\n success\n }\n}\n"
}
};
})();
(node as any).hash = "1b9447e5cbb2ec7dce4f3f9c68493555";
export default node;

View File

@@ -0,0 +1,107 @@
/**
* @generated SignedSource<<843902dd4fb6e2a5ee4d2d175ddcab03>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type CreateTrustCenterAccessInput = {
email: string;
name: string;
trustCenterId: string;
};
export type RequestAccessDialogMutation$variables = {
input: CreateTrustCenterAccessInput;
};
export type RequestAccessDialogMutation$data = {
readonly createTrustCenterAccess: {
readonly trustCenterAccess: {
readonly id: string;
};
};
};
export type RequestAccessDialogMutation = {
response: RequestAccessDialogMutation$data;
variables: RequestAccessDialogMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "CreateTrustCenterAccessPayload",
"kind": "LinkedField",
"name": "createTrustCenterAccess",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TrustCenterAccess",
"kind": "LinkedField",
"name": "trustCenterAccess",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "RequestAccessDialogMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "RequestAccessDialogMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "13fedcfe7c72292417b76b3624c5434f",
"id": null,
"metadata": {},
"name": "RequestAccessDialogMutation",
"operationKind": "mutation",
"text": "mutation RequestAccessDialogMutation(\n $input: CreateTrustCenterAccessInput!\n) {\n createTrustCenterAccess(input: $input) {\n trustCenterAccess {\n id\n }\n }\n}\n"
}
};
})();
(node as any).hash = "18075ac4298dd3ca05bbcf8fb1717a9d";
export default node;

View File

@@ -0,0 +1,84 @@
/**
* @generated SignedSource<<4f486a2ab88b07376ece5f29705c2553>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type CountryCode = "AD" | "AE" | "AF" | "AG" | "AI" | "AL" | "AM" | "AO" | "AQ" | "AR" | "AS" | "AT" | "AU" | "AW" | "AX" | "AZ" | "BA" | "BB" | "BD" | "BE" | "BF" | "BG" | "BH" | "BI" | "BJ" | "BL" | "BM" | "BN" | "BO" | "BQ" | "BR" | "BS" | "BT" | "BV" | "BW" | "BY" | "BZ" | "CA" | "CC" | "CD" | "CF" | "CG" | "CH" | "CI" | "CK" | "CL" | "CM" | "CN" | "CO" | "CR" | "CU" | "CV" | "CW" | "CX" | "CY" | "CZ" | "DE" | "DJ" | "DK" | "DM" | "DO" | "DZ" | "EC" | "EE" | "EG" | "EH" | "ER" | "ES" | "ET" | "FI" | "FJ" | "FK" | "FM" | "FO" | "FR" | "GA" | "GB" | "GD" | "GE" | "GF" | "GG" | "GH" | "GI" | "GL" | "GM" | "GN" | "GP" | "GQ" | "GR" | "GT" | "GU" | "GW" | "GY" | "HK" | "HM" | "HN" | "HR" | "HT" | "HU" | "ID" | "IE" | "IL" | "IM" | "IN" | "IO" | "IQ" | "IR" | "IS" | "IT" | "JE" | "JM" | "JO" | "JP" | "KE" | "KG" | "KH" | "KI" | "KM" | "KN" | "KP" | "KR" | "KW" | "KY" | "KZ" | "LA" | "LB" | "LC" | "LI" | "LK" | "LR" | "LS" | "LT" | "LU" | "LV" | "LY" | "MA" | "MC" | "MD" | "ME" | "MF" | "MG" | "MH" | "MK" | "ML" | "MM" | "MN" | "MO" | "MP" | "MQ" | "MR" | "MS" | "MT" | "MU" | "MV" | "MW" | "MX" | "MY" | "MZ" | "NA" | "NC" | "NE" | "NF" | "NG" | "NI" | "NL" | "NO" | "NP" | "NR" | "NU" | "NZ" | "OM" | "PA" | "PE" | "PF" | "PG" | "PH" | "PK" | "PL" | "PM" | "PN" | "PR" | "PS" | "PT" | "PW" | "PY" | "QA" | "RE" | "RO" | "RS" | "RU" | "RW" | "SA" | "SB" | "SC" | "SD" | "SE" | "SG" | "SH" | "SI" | "SJ" | "SK" | "SL" | "SM" | "SN" | "SO" | "SR" | "SS" | "ST" | "SV" | "SX" | "SY" | "SZ" | "TC" | "TD" | "TF" | "TG" | "TH" | "TJ" | "TK" | "TL" | "TM" | "TN" | "TO" | "TR" | "TT" | "TV" | "TW" | "TZ" | "UA" | "UG" | "UM" | "US" | "UY" | "UZ" | "VA" | "VC" | "VE" | "VG" | "VI" | "VN" | "VU" | "WF" | "WS" | "YE" | "YT" | "ZA" | "ZM" | "ZW";
export type VendorCategory = "ANALYTICS" | "CLOUD_MONITORING" | "CLOUD_PROVIDER" | "COLLABORATION" | "CUSTOMER_SUPPORT" | "DATA_STORAGE_AND_PROCESSING" | "DOCUMENT_MANAGEMENT" | "EMPLOYEE_MANAGEMENT" | "ENGINEERING" | "FINANCE" | "IDENTITY_PROVIDER" | "IT" | "MARKETING" | "OFFICE_OPERATIONS" | "OTHER" | "PASSWORD_MANAGEMENT" | "PRODUCT_AND_DESIGN" | "PROFESSIONAL_SERVICES" | "RECRUITING" | "SALES" | "SECURITY" | "VERSION_CONTROL";
import { FragmentRefs } from "relay-runtime";
export type VendorRowFragment$data = {
readonly category: VendorCategory;
readonly countries: ReadonlyArray<CountryCode>;
readonly id: string;
readonly name: string;
readonly privacyPolicyUrl: string | null | undefined;
readonly websiteUrl: string | null | undefined;
readonly " $fragmentType": "VendorRowFragment";
};
export type VendorRowFragment$key = {
readonly " $data"?: VendorRowFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"VendorRowFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "VendorRowFragment",
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "privacyPolicyUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "countries",
"storageKey": null
}
],
"type": "Vendor",
"abstractKey": null
};
(node as any).hash = "eba669a2b65ef7b9990799516b7d9932";
export default node;

View File

@@ -0,0 +1,10 @@
export function documentTypeLabel(type: string, __: (s: string) => string) {
switch (type) {
case "POLICY":
return __("Policy");
case "ISMS":
return __("Security");
default:
return __("Other");
}
}

View File

@@ -0,0 +1,19 @@
import { useEffect, useRef } from "react";
/**
* Hook to handle cleanup after a delay
*
* Used for disposing the graphQL query when the component unmounts
*/
export function useCleanup(callback: () => void, delay: number) {
const timer = useRef<ReturnType<typeof setTimeout>>(null);
useEffect(() => {
if (timer.current) {
clearTimeout(timer.current);
}
return () => {
timer.current = setTimeout(callback, delay);
};
}, [callback, delay]);
}

View File

@@ -0,0 +1,13 @@
import { useForm } from "react-hook-form";
import type { z, ZodTypeAny } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
export function useFormWithSchema<T extends ZodTypeAny>(
schema: T,
options: Parameters<typeof useForm<z.infer<T>>>[0],
) {
return useForm<z.infer<T>>({
...options,
resolver: zodResolver(schema),
});
}

View File

@@ -0,0 +1,6 @@
import { useContext } from "react";
import { AuthContext } from "/providers/AuthProvider";
export function useIsAuthenticated(): boolean {
return useContext(AuthContext).isAuthenticated;
}

View File

@@ -0,0 +1,63 @@
import { useCallback } from "react";
import { useMutation, type UseMutationConfig } from "react-relay";
import { useToast } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import type { MutationParameters, GraphQLTaggedNode } from "relay-runtime";
/**
* A decorated useMutation hook that emits toast notifications on success or error.
*/
export function useMutationWithToasts<T extends MutationParameters>(
query: GraphQLTaggedNode,
baseOptions?: {
onSuccess?: (response: T["response"]) => void;
errorMessage?: string;
}
) {
const [mutate, isLoading] = useMutation<T>(query);
const { toast } = useToast();
const { __ } = useTranslate();
const mutateWithToast = useCallback(
(
queryOptions: UseMutationConfig<T> & {
onSuccess?: (response: T["response"]) => void;
errorMessage?: string;
}
) => {
const options = { ...baseOptions, ...queryOptions };
return new Promise<void>((resolve, reject) =>
mutate({
...queryOptions,
onCompleted: (response, error) => {
options.onCompleted?.(response, error);
if (error) {
toast({
title: __("Error"),
description:
options.errorMessage ??
__("Failed to commit this operation."),
variant: "error",
});
reject(error);
return;
}
options.onSuccess?.(response);
resolve();
},
onError: (error) => {
toast({
title: __("Error"),
description:
options.errorMessage ?? __("Failed to commit this operation."),
variant: "error",
});
reject(error);
},
})
);
},
[mutate]
);
return [mutateWithToast, isLoading] as const;
}

View File

@@ -0,0 +1,13 @@
import { TrustCenterContext } from "/providers/TrustCenterProvider";
import { useContext } from "react";
export function useTrustCenter(): {
id: string;
organization: { name: string };
} {
const context = useContext(TrustCenterContext);
if (!context) {
throw new Error("useTrustCenter must be used within a TrustCenterProvider");
}
return context;
}

10
apps/trust/src/index.css Normal file
View File

@@ -0,0 +1,10 @@
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap");
@import "tailwindcss";
@import "@probo/ui/src/theme.css";
@import "tw-animate-css";
@source "../../../packages/ui/src";
@source "../../../packages/helpers/src";
.react-pdf__Page__annotations.annotationLayer {
display: none;
}

View File

@@ -0,0 +1,67 @@
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import type { TrustGraphQuery } from "/queries/__generated__/TrustGraphQuery.graphql.ts";
import { trustGraphQuery } from "/queries/TrustGraph.ts";
import { Logo, TabLink, Tabs } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { OrganizationSidebar } from "/components/OrganizationSidebar";
import { Outlet } from "react-router";
import { NDADialog } from "/components/NDADialog";
import { AuthProvider } from "/providers/AuthProvider";
import { TrustCenterProvider } from "/providers/TrustCenterProvider";
type Props = {
queryRef: PreloadedQuery<TrustGraphQuery>;
};
export function MainLayout(props: Props) {
const { __ } = useTranslate();
const trustCenter = usePreloadedQuery(
trustGraphQuery,
props.queryRef,
).trustCenterBySlug;
if (!trustCenter) {
return null;
}
const baseTabUrl = `/trust/${trustCenter.slug}`;
const showNDADialog =
trustCenter.isUserAuthenticated &&
!trustCenter.hasAcceptedNonDisclosureAgreement;
return (
<AuthProvider isAuthenticated={trustCenter.isUserAuthenticated}>
<TrustCenterProvider trustCenter={trustCenter}>
{showNDADialog && (
<NDADialog
name={trustCenter.organization.name}
trustCenterId={trustCenter.id}
url={trustCenter.ndaFileUrl}
fileName={trustCenter.ndaFileName}
/>
)}
<div className="grid grid-cols-1 max-w-[1280px] mx-4 pt-6 gap-4 lg:mx-auto lg:gap-10 lg:pt-20 lg:grid-cols-[400px_1fr] lg:items-start ">
<OrganizationSidebar trustCenter={trustCenter} />
<main>
<Tabs className="mb-8">
<TabLink to={`${baseTabUrl}/overview`}>{__("Overview")}</TabLink>
<TabLink to={`${baseTabUrl}/documents`}>
{__("Documents")}
</TabLink>
<TabLink to={`${baseTabUrl}/subprocessors`}>
{__("Subprocessors")}
</TabLink>
</Tabs>
<Outlet context={{ trustCenter }} />
</main>
</div>
<a
href="https://www.getprobo.com/"
className="flex gap-2 text-sm font-medium text-txt-tertiary items-center w-max mx-auto my-10"
>
{__("Powered by")} <Logo withPicto className="h-6" />
</a>
</TrustCenterProvider>
</AuthProvider>
);
}

27
apps/trust/src/main.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import { App } from "./App";
import { RelayProvider } from "./providers/RelayProviders";
import { TranslatorProvider } from "./providers/TranslatorProvider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
},
},
});
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<RelayProvider>
<TranslatorProvider>
<App />
</TranslatorProvider>
</RelayProvider>
</QueryClientProvider>
</StrictMode>
);

3
apps/trust/src/module.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare module "https://cdn.jsdelivr.net/npm/pdfjs-dist@5.4.149/+esm" {
export * from "pdfjs-dist";
}

View File

@@ -0,0 +1,80 @@
import { useTranslate } from "@probo/i18n";
import { useEffect, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router";
import { buildEndpoint } from "/providers/RelayProviders";
import { PageError } from "/components/PageError";
import { Spinner } from "@probo/ui";
/**
* Page requested with an access token to authenticate the user for the Trust center
*/
export function AccessPage() {
const { __ } = useTranslate();
const { slug } = useParams<{ slug: string }>();
const [searchParams] = useSearchParams();
const token = searchParams.get("token");
const navigate = useNavigate();
const isValidRequest = !!(slug && token);
const [error, setError] = useState<string | null>(() => {
if (!slug) {
return __("Invalid trust center");
}
if (!token) {
return __("Invalid access token");
}
return null;
});
// Initiate an authentication attempt
useEffect(() => {
if (!isValidRequest) {
return;
}
fetch(buildEndpoint("/api/trust/v1/auth/authenticate"), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
token,
}),
})
.then((response) => {
// For invalid response throw an error
if (!response.ok) {
const defaultMessage = `HTTP ${response.status}: ${response.statusText}`;
return response
.json()
.then((json) => {
throw new Error(json.message ?? defaultMessage);
})
.catch(() => {
throw new Error(defaultMessage);
});
}
return response.json();
})
.then((data) => {
if (data.success) {
navigate(`/trust/${slug}`);
return;
}
throw new Error(data.message ?? __("Authentication failed"));
})
.catch((error) => {
setError(error.message);
});
}, [isValidRequest, slug, token, __, navigate]);
if (error) {
return <PageError error={error} />;
}
return (
<div className="p-4 text-center flex items-center justify-center gap-2">
<Spinner size={16} />
{__("Redirecting to trust center")}
</div>
);
}

View File

@@ -0,0 +1,42 @@
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { trustDocumentsQuery } from "/queries/TrustGraph";
import type { TrustGraphDocumentsQuery } from "/queries/__generated__/TrustGraphDocumentsQuery.graphql.ts";
import { groupBy, objectEntries } from "@probo/helpers";
import { documentTypeLabel } from "/helpers/documents";
import { useTranslate } from "@probo/i18n";
import { Fragment } from "react";
import { DocumentRow } from "/components/DocumentRow";
import { Rows } from "/components/Rows.tsx";
import { RowHeader } from "/components/RowHeader.tsx";
type Props = {
queryRef: PreloadedQuery<TrustGraphDocumentsQuery>;
};
export function DocumentsPage({ queryRef }: Props) {
const { __ } = useTranslate();
const data = usePreloadedQuery(trustDocumentsQuery, queryRef);
const documents =
data.trustCenterBySlug?.documents.edges.map((edge) => edge.node) ?? [];
const documentsPerType = groupBy(documents, (document) =>
documentTypeLabel(document.documentType, __),
);
return (
<div>
<h2 className="font-medium mb-1">{__("Documents")}</h2>
<p className="text-sm text-txt-secondary mb-4">
{__("Security and compliance documentation:")}
</p>
<Rows className="mb-8">
{objectEntries(documentsPerType).map(([label, documents]) => (
<Fragment key={label}>
<RowHeader>{label}</RowHeader>
{documents.map((document) => (
<DocumentRow key={document.id} document={document} />
))}
</Fragment>
))}
</Rows>
</div>
);
}

View File

@@ -0,0 +1,147 @@
import { useFragment } from "react-relay";
import { graphql } from "relay-runtime";
import type { OverviewFragment$key } from "./__generated__/OverviewFragment.graphql";
import { Link, useOutletContext } from "react-router";
import { groupBy, objectEntries, sprintf } from "@probo/helpers";
import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql";
import { useTranslate } from "@probo/i18n";
import { Card, IconChevronRight } from "@probo/ui";
import { AuditRow } from "/components/AuditRow";
import { documentTypeLabel } from "/helpers/documents";
import { Fragment } from "react";
import { DocumentRow } from "/components/DocumentRow";
import { VendorRow } from "/components/VendorRow";
import { RowHeader } from "/components/RowHeader.tsx";
import { Rows } from "/components/Rows.tsx";
const overviewFragment = graphql`
fragment OverviewFragment on TrustCenter {
references(first: 14) {
edges {
node {
id
name
logoUrl
websiteUrl
}
}
}
vendors(first: 3) {
edges {
node {
id
...VendorRowFragment
}
}
}
documents(first: 5) {
edges {
node {
id
...DocumentRowFragment
documentType
}
}
}
}
`;
export function OverviewPage() {
const { trustCenter } = useOutletContext<{
trustCenter: OverviewFragment$key &
TrustGraphQuery$data["trustCenterBySlug"];
}>();
const { __ } = useTranslate();
const fragment = useFragment(overviewFragment, trustCenter);
const documentsPerType = groupBy(
fragment.documents.edges.map((edge) => edge.node),
(node) => documentTypeLabel(node.documentType, __),
);
return (
<div>
<h2 className="font-medium mb-1">{__("Documents")}</h2>
<p className="text-sm text-txt-secondary mb-4">
{__("Security and compliance documentation:")}
</p>
<Rows className="mb-8">
<RowHeader>{__("Certifications")}</RowHeader>
{trustCenter.audits.edges.map((edge) => (
<AuditRow key={edge.node.id} audit={edge.node} />
))}
{objectEntries(documentsPerType).map(([label, documents]) => (
<Fragment key={label}>
<RowHeader>{label}</RowHeader>
{documents.map((document) => (
<DocumentRow key={document.id} document={document} />
))}
</Fragment>
))}
<Link
to={`/trust/${trustCenter.slug}/documents`}
className="text-sm font-medium flex gap-2 items-center"
>
{__("See all documents")}
<IconChevronRight size={16} />
</Link>
</Rows>
<h2 className="font-medium mb-1">{__("Subprocessors")}</h2>
<p className="text-sm text-txt-secondary mb-4">
{sprintf(
__("Third-party subprocessors %s work with:"),
trustCenter.organization.name,
)}
</p>
<Rows className="mb-8 *:py-5">
{fragment.vendors.edges.map((edge) => (
<VendorRow key={edge.node.id} vendor={edge.node} />
))}
<Link
to={`/trust/${trustCenter.slug}/subprocessors`}
className="text-sm font-medium flex gap-2 items-center"
>
{__("See all subprocessors")}
<IconChevronRight size={16} />
</Link>
</Rows>
<References
references={fragment.references.edges.map((edge) => edge.node)}
/>
</div>
);
}
type Reference = {
name: string;
logoUrl: string;
websiteUrl: string;
id: string;
};
function References({ references }: { references: Reference[] }) {
const { __ } = useTranslate();
return (
<div>
<h2 className="font-medium mb-4">{__("Trusted by")}</h2>
<Card className="grid grid-cols-2 flex-wrap p-6 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-7">
{references.map((reference) => (
<a
key={reference.id}
href={reference.websiteUrl}
target="_blank"
rel="noopener noreferrer"
className="flex flex-col justify-center items-center gap-2"
>
<img
src={reference.logoUrl}
alt={reference.name}
className="rounded-2xl size-12 block"
/>
<span className="text-xs text-txt-secondary">{reference.name}</span>
</a>
))}
</Card>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { trustVendorsQuery } from "/queries/TrustGraph";
import { sprintf } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import type { TrustGraphVendorsQuery } from "/queries/__generated__/TrustGraphVendorsQuery.graphql";
import { VendorRow } from "/components/VendorRow";
import { Rows } from "/components/Rows.tsx";
type Props = {
queryRef: PreloadedQuery<TrustGraphVendorsQuery>;
};
export function SubprocessorsPage({ queryRef }: Props) {
const { __ } = useTranslate();
const data = usePreloadedQuery(trustVendorsQuery, queryRef);
const vendors =
data.trustCenterBySlug?.vendors.edges.map((edge) => edge.node) ?? [];
return (
<div>
<h2 className="font-medium mb-1">{__("Subprocessors")}</h2>
<p className="text-sm text-txt-secondary mb-4">
{sprintf(
__("Third-party subprocessors %s work with:"),
data.trustCenterBySlug?.organization.name ?? "",
)}
</p>
<Rows>
{vendors.map((vendor) => (
<VendorRow key={vendor.id} vendor={vendor} />
))}
</Rows>
</div>
);
}

View File

@@ -0,0 +1,229 @@
/**
* @generated SignedSource<<0767a7e9f6fb3f0464027b92a5239521>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
export type DocumentType = "ISMS" | "OTHER" | "POLICY";
import { FragmentRefs } from "relay-runtime";
export type OverviewFragment$data = {
readonly documents: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly documentType: DocumentType;
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"DocumentRowFragment">;
};
}>;
};
readonly references: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly logoUrl: string;
readonly name: string;
readonly websiteUrl: string;
};
}>;
};
readonly vendors: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"VendorRowFragment">;
};
}>;
};
readonly " $fragmentType": "OverviewFragment";
};
export type OverviewFragment$key = {
readonly " $data"?: OverviewFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"OverviewFragment">;
};
const node: ReaderFragment = (function(){
var v0 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "OverviewFragment",
"selections": [
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 14
}
],
"concreteType": "TrustCenterReferenceConnection",
"kind": "LinkedField",
"name": "references",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TrustCenterReferenceEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TrustCenterReference",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "references(first:14)"
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 3
}
],
"concreteType": "VendorConnection",
"kind": "LinkedField",
"name": "vendors",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Vendor",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "VendorRowFragment"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "vendors(first:3)"
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 5
}
],
"concreteType": "DocumentConnection",
"kind": "LinkedField",
"name": "documents",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "DocumentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Document",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v0/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "DocumentRowFragment"
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "documentType",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "documents(first:5)"
}
],
"type": "TrustCenter",
"abstractKey": null
};
})();
(node as any).hash = "2f42031d2017248051c8dfea72b4e502";
export default node;

View File

@@ -0,0 +1,13 @@
import { createContext, useMemo } from "react";
export const AuthContext = createContext({ isAuthenticated: false });
type Props = {
children: React.ReactNode;
isAuthenticated: boolean;
};
export function AuthProvider({ children, isAuthenticated }: Props) {
const value = useMemo(() => ({ isAuthenticated }), [isAuthenticated]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

View File

@@ -0,0 +1,146 @@
import {
Environment,
type FetchFunction,
Network,
RecordSource,
Store,
} from "relay-runtime";
import { GraphQLError } from "graphql";
import type { PropsWithChildren } from "react";
import { RelayEnvironmentProvider } from "react-relay";
export class UnAuthenticatedError extends Error {
constructor() {
super("UNAUTHENTICATED");
this.name = "UnAuthenticatedError";
}
}
export class InternalServerError extends Error {
constructor() {
super("INTERNAL_SERVER_ERROR");
this.name = "InternalServerError";
}
}
export function buildEndpoint(path: string): string {
const host = import.meta.env.VITE_API_URL;
if (!host) {
return path;
}
const formattedHost =
host.startsWith("http://") || host.startsWith("https://")
? host
: `https://${host}`;
const url = new URL(formattedHost);
if (path) {
url.pathname = path.startsWith("/") ? path : `/${path}`;
}
return url.toString();
}
const hasUnauthenticatedError = (error: GraphQLError) =>
error.extensions?.code == "UNAUTHENTICATED";
const fetchRelay: FetchFunction = async (
request,
variables,
_,
uploadables,
) => {
const requestInit: RequestInit = {
method: "POST",
credentials: "include",
headers: {},
};
if (uploadables) {
const formData = new FormData();
formData.append(
"operations",
JSON.stringify({
operationName: request.name,
query: request.text,
variables: variables,
}),
);
const uploadableMap: {
[key: string]: string[];
} = {};
Object.keys(uploadables).forEach((key, index) => {
uploadableMap[index] = [`variables.${key}`];
});
formData.append("map", JSON.stringify(uploadableMap));
Object.keys(uploadables).forEach((key, index) => {
formData.append(index.toString(), uploadables[key]);
});
requestInit.body = formData;
} else {
requestInit.headers = {
Accept:
"application/graphql-response+json; charset=utf-8, application/json; charset=utf-8",
"Content-Type": "application/json",
};
requestInit.body = JSON.stringify({
operationName: request.name,
query: request.text,
variables,
});
}
const response = await fetch(
buildEndpoint("/api/trust/v1/graphql"),
requestInit,
);
if (response.status === 500) {
throw new InternalServerError();
}
const json = await response.json();
if (json.errors) {
const errors = json.errors as GraphQLError[];
if (errors.find(hasUnauthenticatedError)) {
throw new UnAuthenticatedError();
}
throw new Error(`Error fetching GraphQL query '${request.name}'`);
}
return json;
};
const source = new RecordSource();
const store = new Store(source, {
queryCacheExpirationTime: 1 * 60 * 1000,
gcReleaseBufferSize: 20,
});
export const relayEnvironment = new Environment({
network: Network.create(fetchRelay),
store,
});
/**
* Provider for relay with the probo environment
*/
export function RelayProvider({ children }: PropsWithChildren) {
return (
<RelayEnvironmentProvider environment={relayEnvironment}>
{children}
</RelayEnvironmentProvider>
);
}

View File

@@ -0,0 +1,18 @@
import type { PropsWithChildren } from "react";
import { TranslatorProvider as ProboTranslatorProvider } from "../../../../packages/i18n/TranslatorProvider";
// TODO : implement a way to retrieve translations strings
const loader = () => {
return Promise.resolve({} as Record<string, string>);
};
/**
* Provider for the translator
*/
export function TranslatorProvider({ children }: PropsWithChildren) {
return (
<ProboTranslatorProvider lang="en" loader={loader}>
{children}
</ProboTranslatorProvider>
);
}

View File

@@ -0,0 +1,20 @@
import { createContext, type ReactNode } from "react";
import type { TrustGraphQuery$data } from "/queries/__generated__/TrustGraphQuery.graphql";
export const TrustCenterContext = createContext<
TrustGraphQuery$data["trustCenterBySlug"] | null
>(null);
export const TrustCenterProvider = ({
children,
trustCenter,
}: {
children: ReactNode;
trustCenter: TrustGraphQuery$data["trustCenterBySlug"];
}) => {
return (
<TrustCenterContext.Provider value={trustCenter}>
{children}
</TrustCenterContext.Provider>
);
};

View File

@@ -0,0 +1,70 @@
import { graphql } from "relay-runtime";
export const trustGraphQuery = graphql`
query TrustGraphQuery($slug: String!) {
trustCenterBySlug(slug: $slug) {
id
slug
isUserAuthenticated
hasAcceptedNonDisclosureAgreement
ndaFileName
ndaFileUrl
organization {
name
description
websiteUrl
logoUrl
email
headquarterAddress
}
...OverviewFragment
audits(first: 50) {
edges {
node {
id
...AuditRowFragment
}
}
}
}
}
`;
export const trustDocumentsQuery = graphql`
query TrustGraphDocumentsQuery($slug: String!) {
trustCenterBySlug(slug: $slug) {
id
organization {
name
}
documents(first: 50) {
edges {
node {
id
documentType
...DocumentRowFragment
}
}
}
}
}
`;
export const trustVendorsQuery = graphql`
query TrustGraphVendorsQuery($slug: String!) {
trustCenterBySlug(slug: $slug) {
id
organization {
name
}
vendors(first: 50) {
edges {
node {
id
...VendorRowFragment
}
}
}
}
}
`;

View File

@@ -0,0 +1,245 @@
/**
* @generated SignedSource<<55d256287ee091b57754d2d76080ad25>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type DocumentType = "ISMS" | "OTHER" | "POLICY";
export type TrustGraphDocumentsQuery$variables = {
slug: string;
};
export type TrustGraphDocumentsQuery$data = {
readonly trustCenterBySlug: {
readonly documents: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly documentType: DocumentType;
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"DocumentRowFragment">;
};
}>;
};
readonly id: string;
readonly organization: {
readonly name: string;
};
} | null | undefined;
};
export type TrustGraphDocumentsQuery = {
response: TrustGraphDocumentsQuery$data;
variables: TrustGraphDocumentsQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "slug"
}
],
v1 = [
{
"kind": "Variable",
"name": "slug",
"variableName": "slug"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v4 = [
{
"kind": "Literal",
"name": "first",
"value": 50
}
],
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "documentType",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "TrustGraphDocumentsQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "trustCenterBySlug",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": (v4/*: any*/),
"concreteType": "DocumentConnection",
"kind": "LinkedField",
"name": "documents",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "DocumentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Document",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v5/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "DocumentRowFragment"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "documents(first:50)"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "TrustGraphDocumentsQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "trustCenterBySlug",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
(v3/*: any*/),
(v2/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": (v4/*: any*/),
"concreteType": "DocumentConnection",
"kind": "LinkedField",
"name": "documents",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "DocumentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Document",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v5/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "documents(first:50)"
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "71e1dc4102d19cdd2fb8d0ff60a391ca",
"id": null,
"metadata": {},
"name": "TrustGraphDocumentsQuery",
"operationKind": "query",
"text": "query TrustGraphDocumentsQuery(\n $slug: String!\n) {\n trustCenterBySlug(slug: $slug) {\n id\n organization {\n name\n id\n }\n documents(first: 50) {\n edges {\n node {\n id\n documentType\n ...DocumentRowFragment\n }\n }\n }\n }\n}\n\nfragment DocumentRowFragment on Document {\n id\n title\n}\n"
}
};
})();
(node as any).hash = "9c56f70fca2afb316f4fd38d646ec545";
export default node;

View File

@@ -0,0 +1,525 @@
/**
* @generated SignedSource<<669a97d59bae0528905d34c18586ce11>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type TrustGraphQuery$variables = {
slug: string;
};
export type TrustGraphQuery$data = {
readonly trustCenterBySlug: {
readonly audits: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"AuditRowFragment">;
};
}>;
};
readonly hasAcceptedNonDisclosureAgreement: boolean;
readonly id: string;
readonly isUserAuthenticated: boolean;
readonly ndaFileName: string | null | undefined;
readonly ndaFileUrl: string | null | undefined;
readonly organization: {
readonly description: string | null | undefined;
readonly email: string | null | undefined;
readonly headquarterAddress: string | null | undefined;
readonly logoUrl: string | null | undefined;
readonly name: string;
readonly websiteUrl: string | null | undefined;
};
readonly slug: string;
readonly " $fragmentSpreads": FragmentRefs<"OverviewFragment">;
} | null | undefined;
};
export type TrustGraphQuery = {
response: TrustGraphQuery$data;
variables: TrustGraphQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "slug"
}
],
v1 = [
{
"kind": "Variable",
"name": "slug",
"variableName": "slug"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "slug",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "isUserAuthenticated",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasAcceptedNonDisclosureAgreement",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "ndaFileName",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "ndaFileUrl",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "description",
"storageKey": null
},
v10 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
},
v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
v12 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
v13 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "headquarterAddress",
"storageKey": null
},
v14 = [
{
"kind": "Literal",
"name": "first",
"value": 50
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "TrustGraphQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "trustCenterBySlug",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/),
(v13/*: any*/)
],
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
"name": "OverviewFragment"
},
{
"alias": null,
"args": (v14/*: any*/),
"concreteType": "AuditConnection",
"kind": "LinkedField",
"name": "audits",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "AuditEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Audit",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "AuditRowFragment"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "audits(first:50)"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "TrustGraphQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "trustCenterBySlug",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/),
(v13/*: any*/),
(v2/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 14
}
],
"concreteType": "TrustCenterReferenceConnection",
"kind": "LinkedField",
"name": "references",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TrustCenterReferenceEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "TrustCenterReference",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v8/*: any*/),
(v11/*: any*/),
(v10/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "references(first:14)"
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 3
}
],
"concreteType": "VendorConnection",
"kind": "LinkedField",
"name": "vendors",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Vendor",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v8/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
(v10/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "privacyPolicyUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "countries",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "vendors(first:3)"
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "first",
"value": 5
}
],
"concreteType": "DocumentConnection",
"kind": "LinkedField",
"name": "documents",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "DocumentEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Document",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "title",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "documentType",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "documents(first:5)"
},
{
"alias": null,
"args": (v14/*: any*/),
"concreteType": "AuditConnection",
"kind": "LinkedField",
"name": "audits",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "AuditEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Audit",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Report",
"kind": "LinkedField",
"name": "report",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "filename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "Framework",
"kind": "LinkedField",
"name": "framework",
"plural": false,
"selections": [
(v2/*: any*/),
(v8/*: any*/)
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "audits(first:50)"
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "00f2723e0dac553f037af3570903fb51",
"id": null,
"metadata": {},
"name": "TrustGraphQuery",
"operationKind": "query",
"text": "query TrustGraphQuery(\n $slug: String!\n) {\n trustCenterBySlug(slug: $slug) {\n id\n slug\n isUserAuthenticated\n hasAcceptedNonDisclosureAgreement\n ndaFileName\n ndaFileUrl\n organization {\n name\n description\n websiteUrl\n logoUrl\n email\n headquarterAddress\n id\n }\n ...OverviewFragment\n audits(first: 50) {\n edges {\n node {\n id\n ...AuditRowFragment\n }\n }\n }\n }\n}\n\nfragment AuditRowFragment on Audit {\n report {\n id\n filename\n }\n framework {\n id\n name\n }\n}\n\nfragment DocumentRowFragment on Document {\n id\n title\n}\n\nfragment OverviewFragment on TrustCenter {\n references(first: 14) {\n edges {\n node {\n id\n name\n logoUrl\n websiteUrl\n }\n }\n }\n vendors(first: 3) {\n edges {\n node {\n id\n ...VendorRowFragment\n }\n }\n }\n documents(first: 5) {\n edges {\n node {\n id\n ...DocumentRowFragment\n documentType\n }\n }\n }\n}\n\nfragment VendorRowFragment on Vendor {\n id\n name\n category\n websiteUrl\n privacyPolicyUrl\n countries\n}\n"
}
};
})();
(node as any).hash = "192a8d21a41871de201009a2dae65870";
export default node;

View File

@@ -0,0 +1,256 @@
/**
* @generated SignedSource<<43ebc34eede5683873c41a65f93e58b1>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type TrustGraphVendorsQuery$variables = {
slug: string;
};
export type TrustGraphVendorsQuery$data = {
readonly trustCenterBySlug: {
readonly id: string;
readonly organization: {
readonly name: string;
};
readonly vendors: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly " $fragmentSpreads": FragmentRefs<"VendorRowFragment">;
};
}>;
};
} | null | undefined;
};
export type TrustGraphVendorsQuery = {
response: TrustGraphVendorsQuery$data;
variables: TrustGraphVendorsQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "slug"
}
],
v1 = [
{
"kind": "Variable",
"name": "slug",
"variableName": "slug"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v4 = [
{
"kind": "Literal",
"name": "first",
"value": 50
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "TrustGraphVendorsQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "trustCenterBySlug",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": (v4/*: any*/),
"concreteType": "VendorConnection",
"kind": "LinkedField",
"name": "vendors",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Vendor",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
"name": "VendorRowFragment"
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "vendors(first:50)"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "TrustGraphVendorsQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "TrustCenter",
"kind": "LinkedField",
"name": "trustCenterBySlug",
"plural": false,
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "organization",
"plural": false,
"selections": [
(v3/*: any*/),
(v2/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": (v4/*: any*/),
"concreteType": "VendorConnection",
"kind": "LinkedField",
"name": "vendors",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "VendorEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Vendor",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "category",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "websiteUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "privacyPolicyUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "countries",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "vendors(first:50)"
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "1975e4b062f66fef036fc08bd7ff0b53",
"id": null,
"metadata": {},
"name": "TrustGraphVendorsQuery",
"operationKind": "query",
"text": "query TrustGraphVendorsQuery(\n $slug: String!\n) {\n trustCenterBySlug(slug: $slug) {\n id\n organization {\n name\n id\n }\n vendors(first: 50) {\n edges {\n node {\n id\n ...VendorRowFragment\n }\n }\n }\n }\n}\n\nfragment VendorRowFragment on Vendor {\n id\n name\n category\n websiteUrl\n privacyPolicyUrl\n countries\n}\n"
}
};
})();
(node as any).hash = "b21c7b35a682aa1fc4210e1c717e55b4";
export default node;

153
apps/trust/src/routes.tsx Normal file
View File

@@ -0,0 +1,153 @@
import {
createBrowserRouter,
Navigate,
redirect,
type RouteObject,
useLoaderData,
useRouteError,
} from "react-router";
import { type ComponentType, Fragment, Suspense } from "react";
import {
relayEnvironment,
UnAuthenticatedError,
} from "./providers/RelayProviders";
import { loadQuery, type PreloadedQuery } from "react-relay";
import { useCleanup } from "./hooks/useDelayedEffect";
import { PageError } from "./components/PageError";
import { MainLayout } from "/layouts/MainLayout";
import {
trustGraphQuery,
trustDocumentsQuery,
trustVendorsQuery,
} from "/queries/TrustGraph";
import { OverviewPage } from "/pages/OverviewPage";
import { DocumentsPage } from "/pages/DocumentsPage";
import { SubprocessorsPage } from "/pages/SubprocessorsPage";
import { AccessPage } from "./pages/AccessPage.tsx";
import { TabSkeleton } from "./components/Skeletons/TabSkeleton";
import { MainSkeleton } from "./components/Skeletons/MainSkeleton";
export type AppRoute = Omit<RouteObject, "Component" | "children"> & {
Component?: ComponentType<any>;
children?: AppRoute[];
fallback?: ComponentType;
queryLoader?: (params: any) => PreloadedQuery<any>;
};
/**
* Top level error boundary
*/
function ErrorBoundary({ error: propsError }: { error?: string }) {
const error = useRouteError() ?? propsError;
if (error instanceof UnAuthenticatedError) {
return <Navigate to="/auth/login" />;
}
return <PageError error={error?.toString()} />;
}
const routes = [
{
path: "/",
Component: Fragment,
ErrorBoundary: ErrorBoundary,
},
{
path: "/trust/:slug/access",
Component: AccessPage,
ErrorBoundary: ErrorBoundary,
},
{
path: "/trust/:slug",
queryLoader: ({ slug }) =>
loadQuery(relayEnvironment, trustGraphQuery, { slug: slug }),
Component: MainLayout,
fallback: MainSkeleton,
ErrorBoundary: ErrorBoundary,
children: [
{
path: "",
loader: ({ params }) => {
throw redirect(`/trust/${params.slug}/overview`);
},
},
{
path: "overview",
fallback: TabSkeleton,
Component: OverviewPage,
},
{
path: "documents",
fallback: TabSkeleton,
Component: DocumentsPage,
queryLoader: ({ slug }) =>
loadQuery(relayEnvironment, trustDocumentsQuery, { slug: slug }),
},
{
path: "subprocessors",
fallback: TabSkeleton,
Component: SubprocessorsPage,
queryLoader: ({ slug }) =>
loadQuery(relayEnvironment, trustVendorsQuery, { slug: slug }),
},
],
},
// Fallback URL to the NotFound Page
{
path: "*",
Component: PageError,
},
] satisfies AppRoute[];
/**
* Wrap components with suspense to handle lazy loading & relay loading states
*/
function routeTransformer({
fallback: FallbackComponent,
queryLoader,
...route
}: AppRoute): RouteObject {
let result = { ...route };
if (FallbackComponent && route.Component) {
const OriginalComponent = route.Component;
result = {
...result,
Component: (props) => (
<Suspense fallback={<FallbackComponent />}>
<OriginalComponent {...props} />
</Suspense>
),
};
}
if (queryLoader && route.Component) {
const OriginalComponent = route.Component;
result = {
...result,
loader: ({ params }) => {
const query = queryLoader(params as Record<string, string>);
return {
queryRef: query,
dispose: query.dispose,
};
},
Component: () => {
const { queryRef, dispose } = useLoaderData();
useCleanup(dispose, 1000);
return (
<Suspense fallback={FallbackComponent ? <FallbackComponent /> : null}>
<OriginalComponent queryRef={queryRef} />
</Suspense>
);
},
};
}
return {
...result,
children: route.children?.map(routeTransformer),
} as RouteObject;
}
export const router = createBrowserRouter(routes.map(routeTransformer));

8
apps/trust/src/types.ts Normal file
View File

@@ -0,0 +1,8 @@
export type NodeOf<T> =
NonNullable<T> extends
| { readonly edges: ReadonlyArray<{ readonly node: infer U }> }
| undefined
? U
: never;
export type ItemOf<T> = T extends (infer U)[] ? U : never;

9
apps/trust/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"/*": ["src/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

7
apps/trust/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

33
apps/trust/vite.config.ts Normal file
View File

@@ -0,0 +1,33 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { fileURLToPath, URL } from "node:url";
// https://vite.dev/config/
export default defineConfig({
plugins: [react({ babel: { plugins: ["relay"] } }), tailwindcss()],
server: {
port: 5174,
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
},
},
},
resolve: {
alias: {
"/type": fileURLToPath(new URL("./src/type.ts", import.meta.url)),
"/components": fileURLToPath(
new URL("./src/components", import.meta.url)
),
"/queries": fileURLToPath(new URL("./src/queries", import.meta.url)),
"/helpers": fileURLToPath(new URL("./src/helpers", import.meta.url)),
"/hooks": fileURLToPath(new URL("./src/hooks", import.meta.url)),
"/layouts": fileURLToPath(new URL("./src/layouts", import.meta.url)),
"/pages": fileURLToPath(new URL("./src/pages", import.meta.url)),
"/routes": fileURLToPath(new URL("./src/routes", import.meta.url)),
"/providers": fileURLToPath(new URL("./src/providers", import.meta.url)),
},
},
});

333
package-lock.json generated
View File

@@ -68,6 +68,50 @@
"vite": "^6.3.6"
}
},
"apps/trust": {
"name": "@probo/trust",
"version": "0.0.0",
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@probo/helpers": "1.0.0",
"@probo/hooks": "1.0.0",
"@probo/i18n": "1.0.0",
"@probo/ui": "1.0.0",
"clsx": "^2.1.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-error-boundary": "^6.0.0",
"react-hook-form": "^7.56.4",
"react-pdf": "^10.1.0",
"react-relay": "^19.0.0",
"react-router": "^7.6.0",
"relay-runtime": "^19.0.0",
"usehooks-ts": "^3.1.1",
"zod": "^3.25.71"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
"@tailwindcss/vite": "^4.1.7",
"@types/node": "^22.15.21",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/react-relay": "^18.2.1",
"@types/relay-runtime": "^19.0.1",
"@vitejs/plugin-react": "^4.4.1",
"babel-plugin-relay": "^19.0.0",
"eslint": "^9.25.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"graphql": "^16.11.0",
"prettier": "^3.5.3",
"relay-compiler": "^19.0.0",
"tailwindcss": "^4.1.7",
"typescript": "~5.8.3",
"typescript-eslint": "^8.30.1",
"vite": "^6.3.6"
}
},
"node_modules/@adobe/css-tools": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz",
@@ -1305,6 +1349,191 @@
"react": ">=16"
}
},
"node_modules/@napi-rs/canvas": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz",
"integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==",
"license": "MIT",
"optional": true,
"workspaces": [
"e2e/*"
],
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "0.1.80",
"@napi-rs/canvas-darwin-arm64": "0.1.80",
"@napi-rs/canvas-darwin-x64": "0.1.80",
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80",
"@napi-rs/canvas-linux-arm64-gnu": "0.1.80",
"@napi-rs/canvas-linux-arm64-musl": "0.1.80",
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.80",
"@napi-rs/canvas-linux-x64-gnu": "0.1.80",
"@napi-rs/canvas-linux-x64-musl": "0.1.80",
"@napi-rs/canvas-win32-x64-msvc": "0.1.80"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz",
"integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz",
"integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz",
"integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz",
"integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz",
"integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz",
"integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz",
"integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz",
"integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz",
"integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "0.1.80",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz",
"integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1389,6 +1618,10 @@
"resolved": "packages/react-lazy",
"link": true
},
"node_modules/@probo/trust": {
"resolved": "apps/trust",
"link": true
},
"node_modules/@probo/tsconfig": {
"resolved": "packages/tsconfig",
"link": true
@@ -7179,6 +7412,15 @@
"source-map-js": "^1.2.0"
}
},
"node_modules/make-cancellable-promise": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-2.0.0.tgz",
"integrity": "sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1"
}
},
"node_modules/make-dir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
@@ -7208,6 +7450,15 @@
"node": ">=10"
}
},
"node_modules/make-event-props": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-2.0.0.tgz",
"integrity": "sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/make-event-props?sponsor=1"
}
},
"node_modules/map-or-similar": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz",
@@ -7527,6 +7778,23 @@
"map-or-similar": "^1.5.0"
}
},
"node_modules/merge-refs": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-2.0.0.tgz",
"integrity": "sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==",
"license": "MIT",
"funding": {
"url": "https://github.com/wojtekmaj/merge-refs?sponsor=1"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -8522,6 +8790,18 @@
"node": ">= 14.16"
}
},
"node_modules/pdfjs-dist": {
"version": "5.3.93",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.3.93.tgz",
"integrity": "sha512-w3fQKVL1oGn8FRyx5JUG5tnbblggDqyx2XzA5brsJ5hSuS+I0NdnJANhmeWKLjotdbPQucLBug5t0MeWr0AAdg==",
"license": "Apache-2.0",
"engines": {
"node": ">=20.16.0 || >=22.3.0"
},
"optionalDependencies": {
"@napi-rs/canvas": "^0.1.71"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -8952,6 +9232,35 @@
"react": ">=18"
}
},
"node_modules/react-pdf": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-10.1.0.tgz",
"integrity": "sha512-iUI1YqWgwwZcsXjrehTp3Yi8nT/bvTaWULaRMMyJWvoqqSlopk4LQQ9GDqUnDtX3gzT2glrqrLbjIPl56a+Q3w==",
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"dequal": "^2.0.3",
"make-cancellable-promise": "^2.0.0",
"make-event-props": "^2.0.0",
"merge-refs": "^2.0.0",
"pdfjs-dist": "5.3.93",
"tiny-invariant": "^1.0.0",
"warning": "^4.0.0"
},
"funding": {
"url": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@@ -9852,7 +10161,6 @@
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"dev": true,
"license": "MIT"
},
"node_modules/tinybench": {
@@ -10782,6 +11090,15 @@
"node": ">=14.0.0"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
@@ -10994,20 +11311,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@@ -25,6 +25,6 @@
"license": "MIT",
"packageManager": "npm@11.1.0",
"volta": {
"node": "v24.4.0"
"node": "24.4.0"
}
}

View File

@@ -1,5 +1,5 @@
export { objectKeys, objectEntries, cleanFormData } from "./object";
export { sprintf, faviconUrl, slugify } from "./string";
export { sprintf, faviconUrl, slugify, domain } from "./string";
export {
getTreatment,
getRiskImpacts,
@@ -12,14 +12,37 @@ export { randomInt } from "./number";
export { getMeasureStateLabel, measureStates } from "./measure";
export { getRole, getRoles, peopleRoles } from "./people";
export { certificationCategoryLabel, certifications } from "./certifications";
export { getCountryName, getCountryOptions, getCountryLabel, countries, type CountryCode } from "./countries";
export {
getCountryName,
getCountryOptions,
getCountryLabel,
countries,
type CountryCode,
} from "./countries";
export { availableFrameworks } from "./frameworks";
export { getDocumentTypeLabel, documentTypes } from "./documents";
export { getAssetTypeVariant, getCriticityVariant } from "./assets";
export { getSnapshotTypeLabel, getSnapshotTypeUrlPath, snapshotTypes, validateSnapshotConsistency } from "./snapshots";
export { getAuditStateLabel, getAuditStateVariant, auditStates } from "./audits";
export { getStatusVariant, getStatusLabel, getStatusOptions } from "./registryStatus";
export { getObligationStatusVariant, getObligationStatusLabel, getObligationStatusOptions } from "./obligationStatus";
export {
getSnapshotTypeLabel,
getSnapshotTypeUrlPath,
snapshotTypes,
validateSnapshotConsistency,
} from "./snapshots";
export {
getAuditStateLabel,
getAuditStateVariant,
auditStates,
} from "./audits";
export {
getStatusVariant,
getStatusLabel,
getStatusOptions,
} from "./registryStatus";
export {
getObligationStatusVariant,
getObligationStatusLabel,
getObligationStatusOptions,
} from "./obligationStatus";
export { promisifyMutation } from "./relay";
export { fileType, fileSize } from "./file";
export { formatDatetime, formatDate } from "./date";

View File

@@ -30,3 +30,11 @@ export function slugify(str: string): string {
.replace(/[^a-z0-9 ]/g, "") // remove all chars not letters, numbers and spaces (to be replaced)
.replace(/\s+/g, "-");
}
export function domain(url: string): string {
const parts = url.split("//");
if (parts.length > 1) {
url = parts[1];
}
return url.split("/")[0];
}

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 17C12.2833 17 12.5208 16.9042 12.7125 16.7125C12.9042 16.5208 13 16.2833 13 16V12C13 11.7167 12.9042 11.4792 12.7125 11.2875C12.5208 11.0958 12.2833 11 12 11C11.7167 11 11.4792 11.0958 11.2875 11.2875C11.0958 11.4792 11 11.7167 11 12V16C11 16.2833 11.0958 16.5208 11.2875 16.7125C11.4792 16.9042 11.7167 17 12 17ZM12 9C12.2833 9 12.5208 8.90417 12.7125 8.7125C12.9042 8.52083 13 8.28333 13 8C13 7.71667 12.9042 7.47917 12.7125 7.2875C12.5208 7.09583 12.2833 7 12 7C11.7167 7 11.4792 7.09583 11.2875 7.2875C11.0958 7.47917 11 7.71667 11 8C11 8.28333 11.0958 8.52083 11.2875 8.7125C11.4792 8.90417 11.7167 9 12 9ZM12 21.9C11.8833 21.9 11.775 21.8917 11.675 21.875C11.575 21.8583 11.475 21.8333 11.375 21.8C9.125 21.05 7.33333 19.6625 6 17.6375C4.66667 15.6125 4 13.4333 4 11.1V6.375C4 5.95833 4.12083 5.58333 4.3625 5.25C4.60417 4.91667 4.91667 4.675 5.3 4.525L11.3 2.275C11.5333 2.19167 11.7667 2.15 12 2.15C12.2333 2.15 12.4667 2.19167 12.7 2.275L18.7 4.525C19.0833 4.675 19.3958 4.91667 19.6375 5.25C19.8792 5.58333 20 5.95833 20 6.375V11.1C20 13.4333 19.3333 15.6125 18 17.6375C16.6667 19.6625 14.875 21.05 12.625 21.8C12.525 21.8333 12.425 21.8583 12.325 21.875C12.225 21.8917 12.1167 21.9 12 21.9ZM12 19.9C13.7333 19.35 15.1667 18.25 16.3 16.6C17.4333 14.95 18 13.1167 18 11.1V6.375L12 4.125L6 6.375V11.1C6 13.1167 6.56667 14.95 7.7 16.6C8.83333 18.25 10.2667 19.35 12 19.9Z" fill="#141E12"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.95 12.7L9.55 11.3C9.35 11.1 9.11667 11 8.85 11C8.58333 11 8.35 11.1 8.15 11.3C7.95 11.5 7.85 11.7375 7.85 12.0125C7.85 12.2875 7.95 12.525 8.15 12.725L10.25 14.85C10.45 15.05 10.6833 15.15 10.95 15.15C11.2167 15.15 11.45 15.05 11.65 14.85L15.9 10.6C16.1 10.4 16.2 10.1625 16.2 9.8875C16.2 9.6125 16.1 9.375 15.9 9.175C15.7 8.975 15.4625 8.875 15.1875 8.875C14.9125 8.875 14.675 8.975 14.475 9.175L10.95 12.7ZM12 21.9C11.8833 21.9 11.775 21.8917 11.675 21.875C11.575 21.8583 11.475 21.8333 11.375 21.8C9.125 21.05 7.33333 19.6625 6 17.6375C4.66667 15.6125 4 13.4333 4 11.1V6.375C4 5.95833 4.12083 5.58333 4.3625 5.25C4.60417 4.91667 4.91667 4.675 5.3 4.525L11.3 2.275C11.5333 2.19167 11.7667 2.15 12 2.15C12.2333 2.15 12.4667 2.19167 12.7 2.275L18.7 4.525C19.0833 4.675 19.3958 4.91667 19.6375 5.25C19.8792 5.58333 20 5.95833 20 6.375V11.1C20 13.4333 19.3333 15.6125 18 17.6375C16.6667 19.6625 14.875 21.05 12.625 21.8C12.525 21.8333 12.425 21.8583 12.325 21.875C12.225 21.8917 12.1167 21.9 12 21.9ZM12 19.9C13.7333 19.35 15.1667 18.25 16.3 16.6C17.4333 14.95 18 13.1167 18 11.1V6.375L12 4.125L6 6.375V11.1C6 13.1167 6.56667 14.95 7.7 16.6C8.83333 18.25 10.2667 19.35 12 19.9Z" fill="#141E12"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 13.4L13.4 14.8C13.5833 14.9833 13.8167 15.075 14.1 15.075C14.3833 15.075 14.6167 14.9833 14.8 14.8C14.9833 14.6167 15.075 14.3833 15.075 14.1C15.075 13.8167 14.9833 13.5833 14.8 13.4L13.4 12L14.8 10.6C14.9833 10.4167 15.075 10.1833 15.075 9.9C15.075 9.61667 14.9833 9.38334 14.8 9.2C14.6167 9.01667 14.3833 8.925 14.1 8.925C13.8167 8.925 13.5833 9.01667 13.4 9.2L12 10.6L10.6 9.2C10.4167 9.01667 10.1833 8.925 9.9 8.925C9.61667 8.925 9.38333 9.01667 9.2 9.2C9.01667 9.38334 8.925 9.61667 8.925 9.9C8.925 10.1833 9.01667 10.4167 9.2 10.6L10.6 12L9.2 13.4C9.01667 13.5833 8.925 13.8167 8.925 14.1C8.925 14.3833 9.01667 14.6167 9.2 14.8C9.38333 14.9833 9.61667 15.075 9.9 15.075C10.1833 15.075 10.4167 14.9833 10.6 14.8L12 13.4ZM12 21.9C11.8833 21.9 11.775 21.8917 11.675 21.875C11.575 21.8583 11.475 21.8333 11.375 21.8C9.125 21.05 7.33333 19.6625 6 17.6375C4.66667 15.6125 4 13.4333 4 11.1V6.375C4 5.95833 4.12083 5.58333 4.3625 5.25C4.60417 4.91667 4.91667 4.675 5.3 4.525L11.3 2.275C11.5333 2.19167 11.7667 2.15 12 2.15C12.2333 2.15 12.4667 2.19167 12.7 2.275L18.7 4.525C19.0833 4.675 19.3958 4.91667 19.6375 5.25C19.8792 5.58333 20 5.95833 20 6.375V11.1C20 13.4333 19.3333 15.6125 18 17.6375C16.6667 19.6625 14.875 21.05 12.625 21.8C12.525 21.8333 12.425 21.8583 12.325 21.875C12.225 21.8917 12.1167 21.9 12 21.9ZM12 19.9C13.7333 19.35 15.1667 18.25 16.3 16.6C17.4333 14.95 18 13.1167 18 11.1V6.375L12 4.125L6 6.375V11.1C6 13.1167 6.56667 14.95 7.7 16.6C8.83333 18.25 10.2667 19.35 12 19.9Z" fill="#141E12"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,19 @@
import type { IconProps } from "./type.ts";
export function IconMail({ size = 24, className }: IconProps) {
return (
<svg
width={size}
height={size}
className={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 16 17"
>
<path
fill="currentColor"
d="M13.33 6.27v5.07h1.34V6.27h-1.34Zm-1.46 6.54H4.13v1.33h7.74v-1.33Zm-9.2-1.47V6.27H1.33v5.07h1.34Zm1.46-6.53h7.74V3.47H4.13v1.34Zm0 8c-.38 0-.63 0-.82-.02a.76.76 0 0 1-.28-.05l-.6 1.18c.25.13.51.18.77.2.26.02.57.02.93.02v-1.33Zm-2.8-1.47c0 .36 0 .68.02.93.03.26.07.53.2.78l1.19-.6a.76.76 0 0 1-.06-.29l-.01-.82H1.33Zm1.7 1.4a.67.67 0 0 1-.3-.3l-1.18.6c.2.39.5.7.88.88l.6-1.18Zm10.3-1.4-.01.82a.76.76 0 0 1-.06.28l1.19.6c.13-.24.17-.5.2-.77.02-.25.02-.57.02-.93h-1.34Zm-1.46 2.8c.36 0 .67 0 .93-.02s.52-.07.77-.2l-.6-1.18a.76.76 0 0 1-.28.05c-.2.02-.44.02-.82.02v1.33Zm1.4-1.7a.67.67 0 0 1-.3.3l.6 1.18a2 2 0 0 0 .88-.87l-1.19-.6Zm1.4-6.17c0-.36 0-.67-.02-.93a2.03 2.03 0 0 0-.2-.77l-1.19.6c.02.03.04.1.06.28l.01.82h1.34Zm-2.8-1.46.82.01c.18.02.25.04.28.06l.6-1.19a2.04 2.04 0 0 0-.77-.2c-.26-.02-.57-.02-.93-.02v1.34Zm2.58-.24a2 2 0 0 0-.88-.88l-.6 1.2c.12.05.23.16.3.28l1.18-.6ZM2.67 6.27l.01-.82a.76.76 0 0 1 .06-.28l-1.19-.6c-.13.25-.17.51-.2.77-.02.26-.02.57-.02.93h1.34Zm1.46-2.8c-.36 0-.67 0-.93.02-.26.03-.52.07-.77.2l.6 1.2c.03-.03.1-.05.28-.07l.82-.01V3.47Zm-1.4 1.7a.67.67 0 0 1 .3-.29l-.6-1.19a2 2 0 0 0-.88.88l1.19.6Zm10.85-1.12L8.42 8.27l.85 1.03 5.15-4.22-.84-1.03Zm-6 4.22L2.42 4.05l-.84 1.03L6.73 9.3l.85-1.03Zm.84 0c-.24.2-.6.2-.84 0L6.73 9.3a2 2 0 0 0 2.54 0l-.85-1.03Z"
/>
</svg>
);
}

View File

@@ -0,0 +1,21 @@
import type { IconProps } from "./type.ts";
export function IconMedal({ size = 24, className }: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="transparent"
className={className}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.33331 10.1673V14.0249C5.33331 14.3965 5.72447 14.6383 6.05692 14.4721L7.77638 13.6123C7.91711 13.5419 8.08285 13.5419 8.22358 13.6123L9.94305 14.4721C10.2755 14.6383 10.6666 14.3965 10.6666 14.0249V10.1673M12.6666 6.00065C12.6666 8.57798 10.5773 10.6673 7.99998 10.6673C5.42265 10.6673 3.33331 8.57798 3.33331 6.00065C3.33331 3.42332 5.42265 1.33398 7.99998 1.33398C10.5773 1.33398 12.6666 3.42332 12.6666 6.00065Z"
stroke="currentColor"
strokeWidth="1.33333"
strokeLinejoin="round"
/>
</svg>
);
}

View File

@@ -0,0 +1,19 @@
import type {IconProps} from "./type.ts";
export function IconMinusLarge({size = 16, className}: IconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="currentColor"
className={className}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.3333 7.33203C13.7015 7.33203 14 7.63051 14 7.9987C14 8.36689 13.7015 8.66536 13.3333 8.66536H2.66667C2.29848 8.66536 2 8.36689 2 7.9987C2 7.63051 2.29848 7.33203 2.66667 7.33203H13.3333Z"
fill="currentColor"
/>
</svg>
);
}

View File

@@ -0,0 +1,27 @@
import type { IconProps } from "./type.ts";
export function IconPin({ size = 24, className }: IconProps) {
return (
<svg
width={size}
height={size}
className={className}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 17 16"
>
<path
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="1.33"
d="M10 6.67a1.67 1.67 0 1 1-3.33 0 1.67 1.67 0 0 1 3.33 0Z"
/>
<path
stroke="currentColor"
strokeLinejoin="round"
strokeWidth="1.33"
d="M13 6.67c0 2.91-2.6 5.55-3.91 6.71-.44.38-1.07.38-1.51 0-1.32-1.16-3.91-3.8-3.91-6.71a4.67 4.67 0 1 1 9.33 0Z"
/>
</svg>
);
}

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.4 13.9032L17.1216 10.6248C15.9516 9.4548 14.0484 9.4548 12.8796 10.6248L4.48679 19.0176C4.46639 19.038 4.46039 19.0668 4.44239 19.0884C5.10239 19.884 6.08639 20.4 7.19999 20.4H16.8C18.7884 20.4 20.4 18.7884 20.4 16.8V13.9032Z" fill="#141E12"/>
<path d="M7.1996 4.80038C5.87436 4.8006 4.80029 5.87454 4.80019 7.1998V16.8004C4.8004 18.1256 5.87443 19.1996 7.1996 19.1998H16.8002C18.1254 19.1997 19.1994 18.1256 19.1996 16.8004V7.1998C19.1995 5.87447 18.1255 4.80049 16.8002 4.80038H7.1996ZM16.8002 2.39999C19.451 2.4001 21.5999 4.54899 21.6 7.1998V16.8004C21.5998 19.4511 19.4509 21.6001 16.8002 21.6002H7.1996C4.54894 21.6 2.40001 19.451 2.3998 16.8004V7.1998C2.3999 4.54905 4.54888 2.4002 7.1996 2.39999H16.8002Z" fill="#141E12"/>
<path d="M8.99999 10.8C9.9941 10.8 10.8 9.99411 10.8 8.99999C10.8 8.00588 9.9941 7.19999 8.99999 7.19999C8.00588 7.19999 7.19999 8.00588 7.19999 8.99999C7.19999 9.99411 8.00588 10.8 8.99999 10.8Z" fill="#141E12"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.0491 4.80802C19.0491 4.80802 19.0491 4.80801 19.0491 4.80802L4.8771 9.53241C4.83932 9.545 4.82796 9.55887 4.8227 9.56584C4.81377 9.57765 4.80347 9.59908 4.80073 9.62873C4.79798 9.65838 4.80416 9.6814 4.8108 9.69473C4.81474 9.70264 4.82338 9.71838 4.85816 9.73769L9.81272 12.4902L12.7019 9.60107C13.1705 9.13244 13.9303 9.13244 14.3989 9.60107C14.8676 10.0697 14.8676 10.8295 14.3989 11.2981L11.5098 14.1873L14.2622 19.1416C14.2815 19.1764 14.2974 19.1853 14.3053 19.1892C14.3186 19.1958 14.3416 19.202 14.3713 19.1993C14.4009 19.1965 14.4224 19.1862 14.4342 19.1773C14.4411 19.172 14.455 19.1607 14.4676 19.1229L19.192 4.95092C19.192 4.95093 19.192 4.95091 19.192 4.95092C19.2031 4.91743 19.2002 4.90003 19.1976 4.88993C19.1937 4.87511 19.1838 4.85485 19.1645 4.83553C19.1452 4.8162 19.1249 4.80626 19.1101 4.80244C19.1 4.79983 19.0826 4.79686 19.0491 4.80802ZM9.13872 14.8613L3.69304 11.8359C1.78214 10.7748 2.0434 7.94718 4.1181 7.25559C4.11809 7.25559 4.11811 7.25558 4.1181 7.25559L18.2901 2.53119C20.2542 1.87648 22.1235 3.74576 21.4688 5.70987L16.7444 19.8819C16.7444 19.8819 16.7444 19.8819 16.7444 19.8819C16.0529 21.9565 13.2254 22.2178 12.1642 20.3072C12.1642 20.3071 12.1642 20.3072 12.1642 20.3072L9.13872 14.8613Z" fill="#141E12"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

Some files were not shown because too many files have changed in this diff Show More