diff --git a/GNUmakefile b/GNUmakefile index e05679d65..987769df0 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -176,3 +176,4 @@ goreleaser-snapshot: ## Build a snapshot release with goreleaser .PHONY: goreleaser-check goreleaser-check: ## Check goreleaser configuration goreleaser check + diff --git a/apps/console/README.md b/apps/console/README.md index da9844432..edbf97a48 100644 --- a/apps/console/README.md +++ b/apps/console/README.md @@ -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 diff --git a/apps/console/src/hooks/useFormWithSchema.ts b/apps/console/src/hooks/useFormWithSchema.ts index 565c466d7..6a7de80be 100644 --- a/apps/console/src/hooks/useFormWithSchema.ts +++ b/apps/console/src/hooks/useFormWithSchema.ts @@ -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>( +export function useFormWithSchema( schema: T, - options: Parameters>>[0] + options: Parameters>>[0], ) { return useForm>({ ...options, diff --git a/apps/trust/.env.example b/apps/trust/.env.example new file mode 100644 index 000000000..460e575be --- /dev/null +++ b/apps/trust/.env.example @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:8080 diff --git a/apps/trust/.gitattributes b/apps/trust/.gitattributes new file mode 100644 index 000000000..7a1fb9a7a --- /dev/null +++ b/apps/trust/.gitattributes @@ -0,0 +1,2 @@ +__generated__/*.graphql linguist-generated +__generated__/*.js linguist-generated diff --git a/apps/trust/.gitignore b/apps/trust/.gitignore new file mode 100644 index 000000000..b403dd978 --- /dev/null +++ b/apps/trust/.gitignore @@ -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? diff --git a/apps/trust/README.md b/apps/trust/README.md new file mode 100644 index 000000000..0efe71567 --- /dev/null +++ b/apps/trust/README.md @@ -0,0 +1,2 @@ +# TrustCenter app + diff --git a/apps/trust/eslint.config.js b/apps/trust/eslint.config.js new file mode 100644 index 000000000..092408a9f --- /dev/null +++ b/apps/trust/eslint.config.js @@ -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 }, + ], + }, + }, +) diff --git a/apps/trust/graphql.config.yml b/apps/trust/graphql.config.yml new file mode 100644 index 000000000..6b90c56ba --- /dev/null +++ b/apps/trust/graphql.config.yml @@ -0,0 +1,5 @@ +schema: '../../pkg/server/api/trust/v1/schema.graphql' +extensions: + endpoints: + TrustCenter: + url: http://localhost:8080/api/trust/v1/graphql diff --git a/apps/trust/index.html b/apps/trust/index.html new file mode 100644 index 000000000..d39ba7dd9 --- /dev/null +++ b/apps/trust/index.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Trust Center + + + +
+ + + + diff --git a/apps/trust/package.json b/apps/trust/package.json new file mode 100644 index 000000000..b93257c1b --- /dev/null +++ b/apps/trust/package.json @@ -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" + } +} diff --git a/apps/trust/public/favicons/apple-touch-icon-114x114.png b/apps/trust/public/favicons/apple-touch-icon-114x114.png new file mode 100644 index 000000000..444ea14a8 Binary files /dev/null and b/apps/trust/public/favicons/apple-touch-icon-114x114.png differ diff --git a/apps/trust/public/favicons/apple-touch-icon-120x120.png b/apps/trust/public/favicons/apple-touch-icon-120x120.png new file mode 100644 index 000000000..def547fc9 Binary files /dev/null and b/apps/trust/public/favicons/apple-touch-icon-120x120.png differ diff --git a/apps/trust/public/favicons/apple-touch-icon-144x144.png b/apps/trust/public/favicons/apple-touch-icon-144x144.png new file mode 100644 index 000000000..7149269f0 Binary files /dev/null and b/apps/trust/public/favicons/apple-touch-icon-144x144.png differ diff --git a/apps/trust/public/favicons/apple-touch-icon-152x152.png b/apps/trust/public/favicons/apple-touch-icon-152x152.png new file mode 100644 index 000000000..26b41822b Binary files /dev/null and b/apps/trust/public/favicons/apple-touch-icon-152x152.png differ diff --git a/apps/trust/public/favicons/apple-touch-icon-57x57.png b/apps/trust/public/favicons/apple-touch-icon-57x57.png new file mode 100644 index 000000000..f53ce4bcf Binary files /dev/null and b/apps/trust/public/favicons/apple-touch-icon-57x57.png differ diff --git a/apps/trust/public/favicons/apple-touch-icon-60x60.png b/apps/trust/public/favicons/apple-touch-icon-60x60.png new file mode 100644 index 000000000..6209814a1 Binary files /dev/null and b/apps/trust/public/favicons/apple-touch-icon-60x60.png differ diff --git a/apps/trust/public/favicons/apple-touch-icon-72x72.png b/apps/trust/public/favicons/apple-touch-icon-72x72.png new file mode 100644 index 000000000..b59da6556 Binary files /dev/null and b/apps/trust/public/favicons/apple-touch-icon-72x72.png differ diff --git a/apps/trust/public/favicons/apple-touch-icon-76x76.png b/apps/trust/public/favicons/apple-touch-icon-76x76.png new file mode 100644 index 000000000..c40e2387c Binary files /dev/null and b/apps/trust/public/favicons/apple-touch-icon-76x76.png differ diff --git a/apps/trust/public/favicons/favicon-128.png b/apps/trust/public/favicons/favicon-128.png new file mode 100644 index 000000000..015f14572 Binary files /dev/null and b/apps/trust/public/favicons/favicon-128.png differ diff --git a/apps/trust/public/favicons/favicon-16x16.png b/apps/trust/public/favicons/favicon-16x16.png new file mode 100644 index 000000000..ab74a6336 Binary files /dev/null and b/apps/trust/public/favicons/favicon-16x16.png differ diff --git a/apps/trust/public/favicons/favicon-196x196.png b/apps/trust/public/favicons/favicon-196x196.png new file mode 100644 index 000000000..f9a182d38 Binary files /dev/null and b/apps/trust/public/favicons/favicon-196x196.png differ diff --git a/apps/trust/public/favicons/favicon-32x32.png b/apps/trust/public/favicons/favicon-32x32.png new file mode 100644 index 000000000..0d5835f0e Binary files /dev/null and b/apps/trust/public/favicons/favicon-32x32.png differ diff --git a/apps/trust/public/favicons/favicon-96x96.png b/apps/trust/public/favicons/favicon-96x96.png new file mode 100644 index 000000000..d45c99538 Binary files /dev/null and b/apps/trust/public/favicons/favicon-96x96.png differ diff --git a/apps/trust/public/favicons/favicon.ico b/apps/trust/public/favicons/favicon.ico new file mode 100644 index 000000000..d3925d54e Binary files /dev/null and b/apps/trust/public/favicons/favicon.ico differ diff --git a/apps/trust/public/favicons/mstile-144x144.png b/apps/trust/public/favicons/mstile-144x144.png new file mode 100644 index 000000000..7149269f0 Binary files /dev/null and b/apps/trust/public/favicons/mstile-144x144.png differ diff --git a/apps/trust/public/favicons/mstile-150x150.png b/apps/trust/public/favicons/mstile-150x150.png new file mode 100644 index 000000000..a1b242663 Binary files /dev/null and b/apps/trust/public/favicons/mstile-150x150.png differ diff --git a/apps/trust/public/favicons/mstile-310x150.png b/apps/trust/public/favicons/mstile-310x150.png new file mode 100644 index 000000000..33898ee97 Binary files /dev/null and b/apps/trust/public/favicons/mstile-310x150.png differ diff --git a/apps/trust/public/favicons/mstile-310x310.png b/apps/trust/public/favicons/mstile-310x310.png new file mode 100644 index 000000000..7e2b51750 Binary files /dev/null and b/apps/trust/public/favicons/mstile-310x310.png differ diff --git a/apps/trust/public/favicons/mstile-70x70.png b/apps/trust/public/favicons/mstile-70x70.png new file mode 100644 index 000000000..015f14572 Binary files /dev/null and b/apps/trust/public/favicons/mstile-70x70.png differ diff --git a/apps/trust/public/logos/gdpr.png b/apps/trust/public/logos/gdpr.png new file mode 100644 index 000000000..72510b40a Binary files /dev/null and b/apps/trust/public/logos/gdpr.png differ diff --git a/apps/trust/public/logos/hipaa.png b/apps/trust/public/logos/hipaa.png new file mode 100644 index 000000000..0b453b713 Binary files /dev/null and b/apps/trust/public/logos/hipaa.png differ diff --git a/apps/trust/public/logos/iso27001.png b/apps/trust/public/logos/iso27001.png new file mode 100644 index 000000000..00dff487e Binary files /dev/null and b/apps/trust/public/logos/iso27001.png differ diff --git a/apps/trust/public/logos/soc2.png b/apps/trust/public/logos/soc2.png new file mode 100644 index 000000000..f3b9be6c8 Binary files /dev/null and b/apps/trust/public/logos/soc2.png differ diff --git a/apps/trust/public/logos/sox.png b/apps/trust/public/logos/sox.png new file mode 100644 index 000000000..7daf0bef5 Binary files /dev/null and b/apps/trust/public/logos/sox.png differ diff --git a/apps/trust/public/robots.txt b/apps/trust/public/robots.txt new file mode 100644 index 000000000..1f53798bb --- /dev/null +++ b/apps/trust/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/apps/trust/relay.config.json b/apps/trust/relay.config.json new file mode 100644 index 000000000..213567742 --- /dev/null +++ b/apps/trust/relay.config.json @@ -0,0 +1,7 @@ +{ + "src": "./src", + "schema": "../../pkg/server/api/trust/v1/schema.graphql", + "language": "typescript", + "eagerEsModules": true, + "noFutureProofEnums": true +} diff --git a/apps/trust/src/App.tsx b/apps/trust/src/App.tsx new file mode 100644 index 000000000..6efc4ed96 --- /dev/null +++ b/apps/trust/src/App.tsx @@ -0,0 +1,12 @@ +import { RouterProvider } from "react-router"; +import { router } from "./routes"; +import { Toasts } from "@probo/ui"; + +export function App() { + return ( + <> + + + + ); +} diff --git a/apps/trust/src/components/AuditRow.tsx b/apps/trust/src/components/AuditRow.tsx new file mode 100644 index 000000000..31df66736 --- /dev/null +++ b/apps/trust/src/components/AuditRow.tsx @@ -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(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 ( +
+
+ + {audit.framework.name} +
+ {audit.report ? ( + isAuthenticated ? ( + + ) : ( + + + + ) + ) : ( + {__("No report")} + )} +
+ ); +} + +export function AuditRowAvatar(props: { audit: AuditRowFragment$key }) { + const audit = useFragment(auditRowFragment, props.audit); + return ( + + + + ); +} + +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 ( + } + > + + +

{audit.framework.name}

+

Framework description

+ + +
+
+
+ ); +} diff --git a/apps/trust/src/components/DocumentRow.tsx b/apps/trust/src/components/DocumentRow.tsx new file mode 100644 index 000000000..f45a2fc9d --- /dev/null +++ b/apps/trust/src/components/DocumentRow.tsx @@ -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(downloadMutation); + const handleDownload = () => { + commitDownload({ + variables: { + input: { + documentId: document.id, + }, + }, + onSuccess(response) { + downloadFile(response.exportDocumentPDF.data, document.title); + }, + }); + }; + return ( +
+
+ + {document.title} +
+ {isAuthenticated ? ( + + ) : ( + + + + )} +
+ ); +} diff --git a/apps/trust/src/components/NDADialog.tsx b/apps/trust/src/components/NDADialog.tsx new file mode 100644 index 000000000..9f1780335 --- /dev/null +++ b/apps/trust/src/components/NDADialog.tsx @@ -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 ( +
+
+ +
+
+
+

+ {__("Review & Sign NDA")} +

+

+ {sprintf( + __( + "Access to %s Trust Center documents requires signing a Non-Disclosure Agreement (NDA). Please review the agreement below. Once signed, you’ll receive immediate access to the requested documents.", + ), + name, + )} +

+ {isMobile && url && ( + + {fileName} + + + )} + +

+ {__( + "By clicking Review & Sign, you agree to the terms of this NDA. If you have questions about the NDA, please contact security@probo.com.", + )} +

+ + Powered by + +
+ {isDesktop && ( +
+ {url && } +
+ )} +
+
+ ); +} diff --git a/apps/trust/src/components/OrganizationSidebar.tsx b/apps/trust/src/components/OrganizationSidebar.tsx new file mode 100644 index 000000000..4d8cffa60 --- /dev/null +++ b/apps/trust/src/components/OrganizationSidebar.tsx @@ -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 ( + +
+ {trustCenter.organization.logoUrl ? ( + + ) : ( +
+ )} +

{trustCenter.organization.name}

+

+ {trustCenter.organization.description} +

+ +
+ + {/* Business information */} +
+

+ + {__("Business information")} +

+ {trustCenter.organization.websiteUrl && ( + + + + {domain(trustCenter.organization.websiteUrl)} + + + + )} + {trustCenter.organization.email && ( + + {trustCenter.organization.email} + + )} + {trustCenter.organization.headquarterAddress && ( + + {trustCenter.organization.headquarterAddress} + + )} + +
+ + {/* Certifications */} +
+

+ + {__("Certifications")} +

+
+ {trustCenter.audits.edges.map((audit) => ( + + ))} + {trustCenter.audits.edges.map((audit) => ( + + ))} + {trustCenter.audits.edges.map((audit) => ( + + ))} +
+
+ +
+ + {/* Actions */} + {!isAuthenticated && ( + + + + )} + {/* */} +
+ + ); +} + +function BusinessInfo({ + children, + label, +}: PropsWithChildren<{ label: string }>) { + return ( +
+
{label}
+
{children}
+
+ ); +} diff --git a/apps/trust/src/components/PDFPreview.tsx b/apps/trust/src/components/PDFPreview.tsx new file mode 100644 index 000000000..8e378115c --- /dev/null +++ b/apps/trust/src/components/PDFPreview.tsx @@ -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["ref"] = useRef(null); + const wrapperRef = useRef(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 ( +
+ {/* Custom Zoom Controls */} + + + {/* PDF Document */} +
+ + {numPages === 0 && } + {times(numPages, (index) => ( + + ))} + +
+
+ ); +} diff --git a/apps/trust/src/components/PageError.tsx b/apps/trust/src/components/PageError.tsx new file mode 100644 index 000000000..d4cbda36f --- /dev/null +++ b/apps/trust/src/components/PageError.tsx @@ -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 ( +
+

+ + {__("Page not found")} +

+

+ {__("The page you are looking for does not exist")} +

+
+ ); + } + + 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 ( +
+

+ + {title} +

+

{description}

+
+ ); + } + + return ( +
+

{__("Unexpected error :(")}

+
+ + {__("Something went wrong")} + +

{error.toString()}

+
+
+ ); +} diff --git a/apps/trust/src/components/RequestAccessDialog.tsx b/apps/trust/src/components/RequestAccessDialog.tsx new file mode 100644 index 000000000..f5e317a5e --- /dev/null +++ b/apps/trust/src/components/RequestAccessDialog.tsx @@ -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 ( + +
+ + {__("Request access to documentation")} + + +

+ {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, + )} +

+
+ + +
+
+ + + +
+
+ ); +} diff --git a/apps/trust/src/components/RowHeader.tsx b/apps/trust/src/components/RowHeader.tsx new file mode 100644 index 000000000..98172bd6f --- /dev/null +++ b/apps/trust/src/components/RowHeader.tsx @@ -0,0 +1,9 @@ +import type { PropsWithChildren } from "react"; + +export function RowHeader({ children }: PropsWithChildren) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/trust/src/components/Rows.tsx b/apps/trust/src/components/Rows.tsx new file mode 100644 index 000000000..20e3f4528 --- /dev/null +++ b/apps/trust/src/components/Rows.tsx @@ -0,0 +1,18 @@ +import type { PropsWithChildren } from "react"; +import clsx from "clsx"; + +export function Rows({ + children, + className, +}: PropsWithChildren<{ className?: string }>) { + return ( +
+ {children} +
+ ); +} diff --git a/apps/trust/src/components/Skeletons/MainSkeleton.tsx b/apps/trust/src/components/Skeletons/MainSkeleton.tsx new file mode 100644 index 000000000..644eda575 --- /dev/null +++ b/apps/trust/src/components/Skeletons/MainSkeleton.tsx @@ -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 ( +
+ +
+ + {__("Overview")} + {__("Documents")} + + {__("Subprocessors")} + + + +
+
+ ); +} diff --git a/apps/trust/src/components/Skeletons/TabSkeleton.tsx b/apps/trust/src/components/Skeletons/TabSkeleton.tsx new file mode 100644 index 000000000..619555748 --- /dev/null +++ b/apps/trust/src/components/Skeletons/TabSkeleton.tsx @@ -0,0 +1,11 @@ +import { Skeleton } from "@probo/ui"; + +export function TabSkeleton() { + return ( +
+ + + +
+ ); +} diff --git a/apps/trust/src/components/VendorRow.tsx b/apps/trust/src/components/VendorRow.tsx new file mode 100644 index 000000000..e24564c30 --- /dev/null +++ b/apps/trust/src/components/VendorRow.tsx @@ -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 ( +
+ {logo ? ( + + ) : ( +
+ )} +
+
{vendor.name}
+ {vendor.privacyPolicyUrl && ( + + {vendor.privacyPolicyUrl.split("//").at(-1)} + + )} +
+ + + {vendor.countries + .map((country) => getCountryName(__, country)) + .join(", ")} + +
+
+
+ ); +} diff --git a/apps/trust/src/components/__generated__/AuditRowDownloadMutation.graphql.ts b/apps/trust/src/components/__generated__/AuditRowDownloadMutation.graphql.ts new file mode 100644 index 000000000..9853f3d2d --- /dev/null +++ b/apps/trust/src/components/__generated__/AuditRowDownloadMutation.graphql.ts @@ -0,0 +1,92 @@ +/** + * @generated SignedSource<> + * @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; diff --git a/apps/trust/src/components/__generated__/AuditRowFragment.graphql.ts b/apps/trust/src/components/__generated__/AuditRowFragment.graphql.ts new file mode 100644 index 000000000..c437da8c2 --- /dev/null +++ b/apps/trust/src/components/__generated__/AuditRowFragment.graphql.ts @@ -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; diff --git a/apps/trust/src/components/__generated__/DocumentRowDownloadMutation.graphql.ts b/apps/trust/src/components/__generated__/DocumentRowDownloadMutation.graphql.ts new file mode 100644 index 000000000..aff494c9f --- /dev/null +++ b/apps/trust/src/components/__generated__/DocumentRowDownloadMutation.graphql.ts @@ -0,0 +1,92 @@ +/** + * @generated SignedSource<> + * @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; diff --git a/apps/trust/src/components/__generated__/DocumentRowFragment.graphql.ts b/apps/trust/src/components/__generated__/DocumentRowFragment.graphql.ts new file mode 100644 index 000000000..b534b430c --- /dev/null +++ b/apps/trust/src/components/__generated__/DocumentRowFragment.graphql.ts @@ -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; diff --git a/apps/trust/src/components/__generated__/NDADialogSignMutation.graphql.ts b/apps/trust/src/components/__generated__/NDADialogSignMutation.graphql.ts new file mode 100644 index 000000000..c8b734ba7 --- /dev/null +++ b/apps/trust/src/components/__generated__/NDADialogSignMutation.graphql.ts @@ -0,0 +1,92 @@ +/** + * @generated SignedSource<> + * @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; diff --git a/apps/trust/src/components/__generated__/RequestAccessDialogMutation.graphql.ts b/apps/trust/src/components/__generated__/RequestAccessDialogMutation.graphql.ts new file mode 100644 index 000000000..4a6a65f37 --- /dev/null +++ b/apps/trust/src/components/__generated__/RequestAccessDialogMutation.graphql.ts @@ -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; diff --git a/apps/trust/src/components/__generated__/VendorRowFragment.graphql.ts b/apps/trust/src/components/__generated__/VendorRowFragment.graphql.ts new file mode 100644 index 000000000..463e62159 --- /dev/null +++ b/apps/trust/src/components/__generated__/VendorRowFragment.graphql.ts @@ -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; + 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; diff --git a/apps/trust/src/helpers/documents.ts b/apps/trust/src/helpers/documents.ts new file mode 100644 index 000000000..9795ba288 --- /dev/null +++ b/apps/trust/src/helpers/documents.ts @@ -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"); + } +} diff --git a/apps/trust/src/hooks/useDelayedEffect.ts b/apps/trust/src/hooks/useDelayedEffect.ts new file mode 100644 index 000000000..ee421db8a --- /dev/null +++ b/apps/trust/src/hooks/useDelayedEffect.ts @@ -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>(null); + + useEffect(() => { + if (timer.current) { + clearTimeout(timer.current); + } + return () => { + timer.current = setTimeout(callback, delay); + }; + }, [callback, delay]); +} diff --git a/apps/trust/src/hooks/useFormWithSchema.ts b/apps/trust/src/hooks/useFormWithSchema.ts new file mode 100644 index 000000000..6a7de80be --- /dev/null +++ b/apps/trust/src/hooks/useFormWithSchema.ts @@ -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( + schema: T, + options: Parameters>>[0], +) { + return useForm>({ + ...options, + resolver: zodResolver(schema), + }); +} diff --git a/apps/trust/src/hooks/useIsAuthenticated.ts b/apps/trust/src/hooks/useIsAuthenticated.ts new file mode 100644 index 000000000..64537af87 --- /dev/null +++ b/apps/trust/src/hooks/useIsAuthenticated.ts @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { AuthContext } from "/providers/AuthProvider"; + +export function useIsAuthenticated(): boolean { + return useContext(AuthContext).isAuthenticated; +} diff --git a/apps/trust/src/hooks/useMutationWithToast.ts b/apps/trust/src/hooks/useMutationWithToast.ts new file mode 100644 index 000000000..acaff1096 --- /dev/null +++ b/apps/trust/src/hooks/useMutationWithToast.ts @@ -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( + query: GraphQLTaggedNode, + baseOptions?: { + onSuccess?: (response: T["response"]) => void; + errorMessage?: string; + } +) { + const [mutate, isLoading] = useMutation(query); + const { toast } = useToast(); + const { __ } = useTranslate(); + const mutateWithToast = useCallback( + ( + queryOptions: UseMutationConfig & { + onSuccess?: (response: T["response"]) => void; + errorMessage?: string; + } + ) => { + const options = { ...baseOptions, ...queryOptions }; + return new Promise((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; +} diff --git a/apps/trust/src/hooks/useTrustCenter.ts b/apps/trust/src/hooks/useTrustCenter.ts new file mode 100644 index 000000000..931ffef8a --- /dev/null +++ b/apps/trust/src/hooks/useTrustCenter.ts @@ -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; +} diff --git a/apps/trust/src/index.css b/apps/trust/src/index.css new file mode 100644 index 000000000..a4e4ea3fb --- /dev/null +++ b/apps/trust/src/index.css @@ -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; +} diff --git a/apps/trust/src/layouts/MainLayout.tsx b/apps/trust/src/layouts/MainLayout.tsx new file mode 100644 index 000000000..c87352264 --- /dev/null +++ b/apps/trust/src/layouts/MainLayout.tsx @@ -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; +}; + +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 ( + + + {showNDADialog && ( + + )} +
+ +
+ + {__("Overview")} + + {__("Documents")} + + + {__("Subprocessors")} + + + +
+
+ + + {__("Powered by")} + +
+
+ ); +} diff --git a/apps/trust/src/main.tsx b/apps/trust/src/main.tsx new file mode 100644 index 000000000..099511216 --- /dev/null +++ b/apps/trust/src/main.tsx @@ -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( + + + + + + + + + +); diff --git a/apps/trust/src/module.d.ts b/apps/trust/src/module.d.ts new file mode 100644 index 000000000..dcc99adf2 --- /dev/null +++ b/apps/trust/src/module.d.ts @@ -0,0 +1,3 @@ +declare module "https://cdn.jsdelivr.net/npm/pdfjs-dist@5.4.149/+esm" { + export * from "pdfjs-dist"; +} diff --git a/apps/trust/src/pages/AccessPage.tsx b/apps/trust/src/pages/AccessPage.tsx new file mode 100644 index 000000000..1f9d93d3b --- /dev/null +++ b/apps/trust/src/pages/AccessPage.tsx @@ -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(() => { + 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 ; + } + + return ( +
+ + {__("Redirecting to trust center")} +
+ ); +} diff --git a/apps/trust/src/pages/DocumentsPage.tsx b/apps/trust/src/pages/DocumentsPage.tsx new file mode 100644 index 000000000..efcdde216 --- /dev/null +++ b/apps/trust/src/pages/DocumentsPage.tsx @@ -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; +}; + +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 ( +
+

{__("Documents")}

+

+ {__("Security and compliance documentation:")} +

+ + {objectEntries(documentsPerType).map(([label, documents]) => ( + + {label} + {documents.map((document) => ( + + ))} + + ))} + +
+ ); +} diff --git a/apps/trust/src/pages/OverviewPage.tsx b/apps/trust/src/pages/OverviewPage.tsx new file mode 100644 index 000000000..b0beeb572 --- /dev/null +++ b/apps/trust/src/pages/OverviewPage.tsx @@ -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 ( +
+

{__("Documents")}

+

+ {__("Security and compliance documentation:")} +

+ + {__("Certifications")} + {trustCenter.audits.edges.map((edge) => ( + + ))} + {objectEntries(documentsPerType).map(([label, documents]) => ( + + {label} + {documents.map((document) => ( + + ))} + + ))} + + {__("See all documents")} + + + + +

{__("Subprocessors")}

+

+ {sprintf( + __("Third-party subprocessors %s work with:"), + trustCenter.organization.name, + )} +

+ + {fragment.vendors.edges.map((edge) => ( + + ))} + + {__("See all subprocessors")} + + + + + edge.node)} + /> +
+ ); +} + +type Reference = { + name: string; + logoUrl: string; + websiteUrl: string; + id: string; +}; + +function References({ references }: { references: Reference[] }) { + const { __ } = useTranslate(); + return ( +
+

{__("Trusted by")}

+ + {references.map((reference) => ( + + {reference.name} + {reference.name} + + ))} + +
+ ); +} diff --git a/apps/trust/src/pages/SubprocessorsPage.tsx b/apps/trust/src/pages/SubprocessorsPage.tsx new file mode 100644 index 000000000..4bbc6cfb6 --- /dev/null +++ b/apps/trust/src/pages/SubprocessorsPage.tsx @@ -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; +}; + +export function SubprocessorsPage({ queryRef }: Props) { + const { __ } = useTranslate(); + const data = usePreloadedQuery(trustVendorsQuery, queryRef); + const vendors = + data.trustCenterBySlug?.vendors.edges.map((edge) => edge.node) ?? []; + return ( +
+

{__("Subprocessors")}

+

+ {sprintf( + __("Third-party subprocessors %s work with:"), + data.trustCenterBySlug?.organization.name ?? "", + )} +

+ + {vendors.map((vendor) => ( + + ))} + +
+ ); +} diff --git a/apps/trust/src/pages/__generated__/OverviewFragment.graphql.ts b/apps/trust/src/pages/__generated__/OverviewFragment.graphql.ts new file mode 100644 index 000000000..2299db14a --- /dev/null +++ b/apps/trust/src/pages/__generated__/OverviewFragment.graphql.ts @@ -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; diff --git a/apps/trust/src/providers/AuthProvider.tsx b/apps/trust/src/providers/AuthProvider.tsx new file mode 100644 index 000000000..cbe9b1293 --- /dev/null +++ b/apps/trust/src/providers/AuthProvider.tsx @@ -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 {children}; +} diff --git a/apps/trust/src/providers/RelayProviders.tsx b/apps/trust/src/providers/RelayProviders.tsx new file mode 100644 index 000000000..645593082 --- /dev/null +++ b/apps/trust/src/providers/RelayProviders.tsx @@ -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 ( + + {children} + + ); +} diff --git a/apps/trust/src/providers/TranslatorProvider.tsx b/apps/trust/src/providers/TranslatorProvider.tsx new file mode 100644 index 000000000..c32f80e65 --- /dev/null +++ b/apps/trust/src/providers/TranslatorProvider.tsx @@ -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); +}; + +/** + * Provider for the translator + */ +export function TranslatorProvider({ children }: PropsWithChildren) { + return ( + + {children} + + ); +} diff --git a/apps/trust/src/providers/TrustCenterProvider.tsx b/apps/trust/src/providers/TrustCenterProvider.tsx new file mode 100644 index 000000000..daad3ad0f --- /dev/null +++ b/apps/trust/src/providers/TrustCenterProvider.tsx @@ -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 ( + + {children} + + ); +}; diff --git a/apps/trust/src/queries/TrustGraph.ts b/apps/trust/src/queries/TrustGraph.ts new file mode 100644 index 000000000..2bfed290e --- /dev/null +++ b/apps/trust/src/queries/TrustGraph.ts @@ -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 + } + } + } + } + } +`; diff --git a/apps/trust/src/queries/__generated__/TrustGraphDocumentsQuery.graphql.ts b/apps/trust/src/queries/__generated__/TrustGraphDocumentsQuery.graphql.ts new file mode 100644 index 000000000..f5d6f96fd --- /dev/null +++ b/apps/trust/src/queries/__generated__/TrustGraphDocumentsQuery.graphql.ts @@ -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; diff --git a/apps/trust/src/queries/__generated__/TrustGraphQuery.graphql.ts b/apps/trust/src/queries/__generated__/TrustGraphQuery.graphql.ts new file mode 100644 index 000000000..c54756d38 --- /dev/null +++ b/apps/trust/src/queries/__generated__/TrustGraphQuery.graphql.ts @@ -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; diff --git a/apps/trust/src/queries/__generated__/TrustGraphVendorsQuery.graphql.ts b/apps/trust/src/queries/__generated__/TrustGraphVendorsQuery.graphql.ts new file mode 100644 index 000000000..cd913d5ce --- /dev/null +++ b/apps/trust/src/queries/__generated__/TrustGraphVendorsQuery.graphql.ts @@ -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; diff --git a/apps/trust/src/routes.tsx b/apps/trust/src/routes.tsx new file mode 100644 index 000000000..ea167c27b --- /dev/null +++ b/apps/trust/src/routes.tsx @@ -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 & { + Component?: ComponentType; + children?: AppRoute[]; + fallback?: ComponentType; + queryLoader?: (params: any) => PreloadedQuery; +}; + +/** + * Top level error boundary + */ +function ErrorBoundary({ error: propsError }: { error?: string }) { + const error = useRouteError() ?? propsError; + + if (error instanceof UnAuthenticatedError) { + return ; + } + + return ; +} + +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) => ( + }> + + + ), + }; + } + if (queryLoader && route.Component) { + const OriginalComponent = route.Component; + result = { + ...result, + loader: ({ params }) => { + const query = queryLoader(params as Record); + return { + queryRef: query, + dispose: query.dispose, + }; + }, + Component: () => { + const { queryRef, dispose } = useLoaderData(); + + useCleanup(dispose, 1000); + + return ( + : null}> + + + ); + }, + }; + } + return { + ...result, + children: route.children?.map(routeTransformer), + } as RouteObject; +} + +export const router = createBrowserRouter(routes.map(routeTransformer)); diff --git a/apps/trust/src/types.ts b/apps/trust/src/types.ts new file mode 100644 index 000000000..583413b93 --- /dev/null +++ b/apps/trust/src/types.ts @@ -0,0 +1,8 @@ +export type NodeOf = + NonNullable extends + | { readonly edges: ReadonlyArray<{ readonly node: infer U }> } + | undefined + ? U + : never; + +export type ItemOf = T extends (infer U)[] ? U : never; diff --git a/apps/trust/src/vite-env.d.ts b/apps/trust/src/vite-env.d.ts new file mode 100644 index 000000000..29c29a188 --- /dev/null +++ b/apps/trust/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/apps/trust/tsconfig.app.json b/apps/trust/tsconfig.app.json new file mode 100644 index 000000000..40a8e8c55 --- /dev/null +++ b/apps/trust/tsconfig.app.json @@ -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"] +} diff --git a/apps/trust/tsconfig.json b/apps/trust/tsconfig.json new file mode 100644 index 000000000..1ffef600d --- /dev/null +++ b/apps/trust/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/apps/trust/tsconfig.node.json b/apps/trust/tsconfig.node.json new file mode 100644 index 000000000..9728af2d8 --- /dev/null +++ b/apps/trust/tsconfig.node.json @@ -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"] +} diff --git a/apps/trust/vite.config.ts b/apps/trust/vite.config.ts new file mode 100644 index 000000000..068c8cdbd --- /dev/null +++ b/apps/trust/vite.config.ts @@ -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)), + }, + }, +}); diff --git a/package-lock.json b/package-lock.json index 0ee39c472..0d60de96f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 221919490..951c46353 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,6 @@ "license": "MIT", "packageManager": "npm@11.1.0", "volta": { - "node": "v24.4.0" + "node": "24.4.0" } } diff --git a/packages/helpers/src/index.ts b/packages/helpers/src/index.ts index 3f355ff9a..58e91da17 100644 --- a/packages/helpers/src/index.ts +++ b/packages/helpers/src/index.ts @@ -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"; diff --git a/packages/helpers/src/string.ts b/packages/helpers/src/string.ts index e026a644d..7af9f542f 100644 --- a/packages/helpers/src/string.ts +++ b/packages/helpers/src/string.ts @@ -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]; +} diff --git a/packages/ui/src/Atoms/Icons/Frame-1.svg b/packages/ui/src/Atoms/Icons/Frame-1.svg deleted file mode 100644 index c68462645..000000000 --- a/packages/ui/src/Atoms/Icons/Frame-1.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/Frame-2.svg b/packages/ui/src/Atoms/Icons/Frame-2.svg deleted file mode 100644 index 0b2a32f6e..000000000 --- a/packages/ui/src/Atoms/Icons/Frame-2.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/Frame.svg b/packages/ui/src/Atoms/Icons/Frame.svg deleted file mode 100644 index c53d1c0c8..000000000 --- a/packages/ui/src/Atoms/Icons/Frame.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/IconMail.tsx b/packages/ui/src/Atoms/Icons/IconMail.tsx new file mode 100644 index 000000000..13d1df5cc --- /dev/null +++ b/packages/ui/src/Atoms/Icons/IconMail.tsx @@ -0,0 +1,19 @@ +import type { IconProps } from "./type.ts"; + +export function IconMail({ size = 24, className }: IconProps) { + return ( + + + + ); +} diff --git a/packages/ui/src/Atoms/Icons/IconMedal.tsx b/packages/ui/src/Atoms/Icons/IconMedal.tsx new file mode 100644 index 000000000..d641a43c9 --- /dev/null +++ b/packages/ui/src/Atoms/Icons/IconMedal.tsx @@ -0,0 +1,21 @@ +import type { IconProps } from "./type.ts"; + +export function IconMedal({ size = 24, className }: IconProps) { + return ( + + + + ); +} diff --git a/packages/ui/src/Atoms/Icons/IconMinusLarge.tsx b/packages/ui/src/Atoms/Icons/IconMinusLarge.tsx new file mode 100644 index 000000000..fe1450ab8 --- /dev/null +++ b/packages/ui/src/Atoms/Icons/IconMinusLarge.tsx @@ -0,0 +1,19 @@ +import type {IconProps} from "./type.ts"; + +export function IconMinusLarge({size = 16, className}: IconProps) { + return ( + + + + ); +} diff --git a/packages/ui/src/Atoms/Icons/IconPin.tsx b/packages/ui/src/Atoms/Icons/IconPin.tsx new file mode 100644 index 000000000..0b29889d7 --- /dev/null +++ b/packages/ui/src/Atoms/Icons/IconPin.tsx @@ -0,0 +1,27 @@ +import type { IconProps } from "./type.ts"; + +export function IconPin({ size = 24, className }: IconProps) { + return ( + + + + + ); +} diff --git a/packages/ui/src/Atoms/Icons/Image.svg b/packages/ui/src/Atoms/Icons/Image.svg deleted file mode 100644 index 44440122c..000000000 --- a/packages/ui/src/Atoms/Icons/Image.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/ui/src/Atoms/Icons/Send.svg b/packages/ui/src/Atoms/Icons/Send.svg deleted file mode 100644 index 97eb34760..000000000 --- a/packages/ui/src/Atoms/Icons/Send.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/arrow-box-left.svg b/packages/ui/src/Atoms/Icons/arrow-box-left.svg deleted file mode 100644 index 1e4bc130a..000000000 --- a/packages/ui/src/Atoms/Icons/arrow-box-left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/arrow-corner-down-left.svg b/packages/ui/src/Atoms/Icons/arrow-corner-down-left.svg deleted file mode 100644 index f9fa083f7..000000000 --- a/packages/ui/src/Atoms/Icons/arrow-corner-down-left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/arrow-down.svg b/packages/ui/src/Atoms/Icons/arrow-down.svg deleted file mode 100644 index 9348a7a53..000000000 --- a/packages/ui/src/Atoms/Icons/arrow-down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/arrow-inbox-1.svg b/packages/ui/src/Atoms/Icons/arrow-inbox-1.svg deleted file mode 100644 index bc36e8844..000000000 --- a/packages/ui/src/Atoms/Icons/arrow-inbox-1.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/arrow-inbox.svg b/packages/ui/src/Atoms/Icons/arrow-inbox.svg deleted file mode 100644 index 7c9f76e24..000000000 --- a/packages/ui/src/Atoms/Icons/arrow-inbox.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/arrow-link.svg b/packages/ui/src/Atoms/Icons/arrow-link.svg deleted file mode 100644 index 75a2eb34c..000000000 --- a/packages/ui/src/Atoms/Icons/arrow-link.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ui/src/Atoms/Icons/arrow-up.svg b/packages/ui/src/Atoms/Icons/arrow-up.svg deleted file mode 100644 index e8208a8c0..000000000 --- a/packages/ui/src/Atoms/Icons/arrow-up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/bank.svg b/packages/ui/src/Atoms/Icons/bank.svg deleted file mode 100644 index c4aedb364..000000000 --- a/packages/ui/src/Atoms/Icons/bank.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/bell-2.svg b/packages/ui/src/Atoms/Icons/bell-2.svg deleted file mode 100644 index 8f692ce24..000000000 --- a/packages/ui/src/Atoms/Icons/bell-2.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/block-1.svg b/packages/ui/src/Atoms/Icons/block-1.svg deleted file mode 100644 index 4580b79d3..000000000 --- a/packages/ui/src/Atoms/Icons/block-1.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/block.svg b/packages/ui/src/Atoms/Icons/block.svg deleted file mode 100644 index c55d24462..000000000 --- a/packages/ui/src/Atoms/Icons/block.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/book.svg b/packages/ui/src/Atoms/Icons/book.svg deleted file mode 100644 index 96695a558..000000000 --- a/packages/ui/src/Atoms/Icons/book.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/calendar-1.svg b/packages/ui/src/Atoms/Icons/calendar-1.svg deleted file mode 100644 index a533ac85d..000000000 --- a/packages/ui/src/Atoms/Icons/calendar-1.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ui/src/Atoms/Icons/calendar-2.svg b/packages/ui/src/Atoms/Icons/calendar-2.svg deleted file mode 100644 index 12ae0009c..000000000 --- a/packages/ui/src/Atoms/Icons/calendar-2.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/checkmark-1.svg b/packages/ui/src/Atoms/Icons/checkmark-1.svg deleted file mode 100644 index 6fe15fe43..000000000 --- a/packages/ui/src/Atoms/Icons/checkmark-1.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/checkmark-2-small.svg b/packages/ui/src/Atoms/Icons/checkmark-2-small.svg deleted file mode 100644 index a8264ebe0..000000000 --- a/packages/ui/src/Atoms/Icons/checkmark-2-small.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/chevron-down.svg b/packages/ui/src/Atoms/Icons/chevron-down.svg deleted file mode 100644 index 6077bd3e2..000000000 --- a/packages/ui/src/Atoms/Icons/chevron-down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/chevron-grabber-vertical.svg b/packages/ui/src/Atoms/Icons/chevron-grabber-vertical.svg deleted file mode 100644 index a0ab5ddb2..000000000 --- a/packages/ui/src/Atoms/Icons/chevron-grabber-vertical.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/chevron-left.svg b/packages/ui/src/Atoms/Icons/chevron-left.svg deleted file mode 100644 index e5145d888..000000000 --- a/packages/ui/src/Atoms/Icons/chevron-left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/chevron-right.svg b/packages/ui/src/Atoms/Icons/chevron-right.svg deleted file mode 100644 index 53861cf1b..000000000 --- a/packages/ui/src/Atoms/Icons/chevron-right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/chevron-triangle-down-small.svg b/packages/ui/src/Atoms/Icons/chevron-triangle-down-small.svg deleted file mode 100644 index afae78b2c..000000000 --- a/packages/ui/src/Atoms/Icons/chevron-triangle-down-small.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/chevron-up.svg b/packages/ui/src/Atoms/Icons/chevron-up.svg deleted file mode 100644 index 97a5b333f..000000000 --- a/packages/ui/src/Atoms/Icons/chevron-up.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/circle-check-1.svg b/packages/ui/src/Atoms/Icons/circle-check-1.svg deleted file mode 100644 index 7ffc640eb..000000000 --- a/packages/ui/src/Atoms/Icons/circle-check-1.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ui/src/Atoms/Icons/circle-check.svg b/packages/ui/src/Atoms/Icons/circle-check.svg deleted file mode 100644 index be7466163..000000000 --- a/packages/ui/src/Atoms/Icons/circle-check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/circle-info.svg b/packages/ui/src/Atoms/Icons/circle-info.svg deleted file mode 100644 index df970b8b9..000000000 --- a/packages/ui/src/Atoms/Icons/circle-info.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/circle-progress.svg b/packages/ui/src/Atoms/Icons/circle-progress.svg deleted file mode 100644 index 06cf12749..000000000 --- a/packages/ui/src/Atoms/Icons/circle-progress.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/circle-questionmark-1.svg b/packages/ui/src/Atoms/Icons/circle-questionmark-1.svg deleted file mode 100644 index 9fb1c05a3..000000000 --- a/packages/ui/src/Atoms/Icons/circle-questionmark-1.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/circle-questionmark-solid.svg b/packages/ui/src/Atoms/Icons/circle-questionmark-solid.svg deleted file mode 100644 index bb71a709f..000000000 --- a/packages/ui/src/Atoms/Icons/circle-questionmark-solid.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/circle-questionmark.svg b/packages/ui/src/Atoms/Icons/circle-questionmark.svg deleted file mode 100644 index 24eba8cad..000000000 --- a/packages/ui/src/Atoms/Icons/circle-questionmark.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/ui/src/Atoms/Icons/circle-x.svg b/packages/ui/src/Atoms/Icons/circle-x.svg deleted file mode 100644 index 934dc263e..000000000 --- a/packages/ui/src/Atoms/Icons/circle-x.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/clock.svg b/packages/ui/src/Atoms/Icons/clock.svg deleted file mode 100644 index 7bfb28be5..000000000 --- a/packages/ui/src/Atoms/Icons/clock.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/collapse.svg b/packages/ui/src/Atoms/Icons/collapse.svg deleted file mode 100644 index 1c1c899aa..000000000 --- a/packages/ui/src/Atoms/Icons/collapse.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/cross-large-x.svg b/packages/ui/src/Atoms/Icons/cross-large-x.svg deleted file mode 100644 index 7e966c9a4..000000000 --- a/packages/ui/src/Atoms/Icons/cross-large-x.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/dot-grid-1x3-horizontal.svg b/packages/ui/src/Atoms/Icons/dot-grid-1x3-horizontal.svg deleted file mode 100644 index ea7d6b883..000000000 --- a/packages/ui/src/Atoms/Icons/dot-grid-1x3-horizontal.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/ui/src/Atoms/Icons/expand.svg b/packages/ui/src/Atoms/Icons/expand.svg deleted file mode 100644 index e7edcc6dd..000000000 --- a/packages/ui/src/Atoms/Icons/expand.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/ui/src/Atoms/Icons/filter.svg b/packages/ui/src/Atoms/Icons/filter.svg deleted file mode 100644 index ce12d347d..000000000 --- a/packages/ui/src/Atoms/Icons/filter.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/ui/src/Atoms/Icons/fire-3.svg b/packages/ui/src/Atoms/Icons/fire-3.svg deleted file mode 100644 index 45bb911f3..000000000 --- a/packages/ui/src/Atoms/Icons/fire-3.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/folder-2.svg b/packages/ui/src/Atoms/Icons/folder-2.svg deleted file mode 100644 index de4b55f99..000000000 --- a/packages/ui/src/Atoms/Icons/folder-2.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/folder-upload.svg b/packages/ui/src/Atoms/Icons/folder-upload.svg deleted file mode 100644 index dc9ee5dde..000000000 --- a/packages/ui/src/Atoms/Icons/folder-upload.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/generator.ts b/packages/ui/src/Atoms/Icons/generator.ts deleted file mode 100644 index a3d5e411f..000000000 --- a/packages/ui/src/Atoms/Icons/generator.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* -Generates Icon components from SVG files - */ - -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const svgFiles = fs - .readdirSync(path.join(__dirname)) - .filter((file) => file.endsWith(".svg")); - -function camelCase(s: string) { - return s.replace(/-([a-z0-9])/g, (g) => g[1].toUpperCase()); -} - -function capitalize(s: string) { - return s.charAt(0).toUpperCase() + s.slice(1); -} - -const exports = []; - -for (const svgFile of svgFiles) { - const iconName = svgFile.replace(".svg", ""); - const componentName = capitalize(camelCase(iconName)); - let svgContent = fs.readFileSync(path.join(__dirname, svgFile), "utf-8"); - - svgContent = svgContent.replace(/width="[^"]*"/, "width={size}"); - svgContent = svgContent.replace(/height="[^"]*"/, "height={size}"); - svgContent = svgContent.replace(/fill="[^"]*"/g, 'fill="currentColor"'); - svgContent = svgContent.replace(/xmlns=/g, "className={className} xmlns="); - svgContent = svgContent.replace(/([a-z]+-[a-z]+)="/g, function (_, attr) { - return `${camelCase(attr)}="`; - }); - - const componentContent = `import type {IconProps} from "./type.ts"; - -export function Icon${componentName}({size = 24, className}: IconProps) { - return ${svgContent} -}`; - - fs.writeFileSync( - path.join(__dirname, `Icon${componentName}.tsx`), - componentContent, - ); - exports.push( - `export { Icon${componentName} } from "./Icon${componentName}.tsx"`, - ); -} - -// Write exports in the index.ts file -fs.writeFileSync( - path.join(__dirname, "index.tsx"), - `${exports.join(";\n")};\n`, -); diff --git a/packages/ui/src/Atoms/Icons/group-1-1.svg b/packages/ui/src/Atoms/Icons/group-1-1.svg deleted file mode 100644 index b05a4b80a..000000000 --- a/packages/ui/src/Atoms/Icons/group-1-1.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/packages/ui/src/Atoms/Icons/group-1.svg b/packages/ui/src/Atoms/Icons/group-1.svg deleted file mode 100644 index bf045ad96..000000000 --- a/packages/ui/src/Atoms/Icons/group-1.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/home.svg b/packages/ui/src/Atoms/Icons/home.svg deleted file mode 100644 index 9d8b1f824..000000000 --- a/packages/ui/src/Atoms/Icons/home.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/import-2.svg b/packages/ui/src/Atoms/Icons/import-2.svg deleted file mode 100644 index 1f5f6632a..000000000 --- a/packages/ui/src/Atoms/Icons/import-2.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ui/src/Atoms/Icons/in-progress.svg b/packages/ui/src/Atoms/Icons/in-progress.svg deleted file mode 100644 index 1d7c9c114..000000000 --- a/packages/ui/src/Atoms/Icons/in-progress.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ui/src/Atoms/Icons/inbox-empty.svg b/packages/ui/src/Atoms/Icons/inbox-empty.svg deleted file mode 100644 index ebf243e12..000000000 --- a/packages/ui/src/Atoms/Icons/inbox-empty.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/index.tsx b/packages/ui/src/Atoms/Icons/index.tsx index 9bf21bb9b..ad7415554 100644 --- a/packages/ui/src/Atoms/Icons/index.tsx +++ b/packages/ui/src/Atoms/Icons/index.tsx @@ -1,6 +1,8 @@ export { IconArrowUp } from "./IconArrowUp.tsx"; export { IconBox } from "./IconBox.tsx"; export { IconBell2 } from "./IconBell2.tsx"; +export { IconMedal } from "./IconMedal.tsx"; +export { IconMail } from "./IconMail.tsx"; export { IconArrowInbox1 } from "./IconArrowInbox1.tsx"; export { IconCircleX } from "./IconCircleX.tsx"; export { IconFire3 } from "./IconFire3.tsx"; @@ -79,3 +81,5 @@ export { IconBlock } from "./IconBlock.tsx"; export { IconChevronDown } from "./IconChevronDown.tsx"; export { IconPageCross } from "./IconPageCross.tsx"; export { IconRotateCw } from "./IconRotateCw.tsx"; +export { IconPin } from "./IconPin.tsx"; +export { IconMinusLarge } from "./IconMinusLarge.tsx"; diff --git a/packages/ui/src/Atoms/Icons/list-stack.svg b/packages/ui/src/Atoms/Icons/list-stack.svg deleted file mode 100644 index 726542168..000000000 --- a/packages/ui/src/Atoms/Icons/list-stack.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/ui/src/Atoms/Icons/magnifying-glass.svg b/packages/ui/src/Atoms/Icons/magnifying-glass.svg deleted file mode 100644 index 18450cf2d..000000000 --- a/packages/ui/src/Atoms/Icons/magnifying-glass.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/not-started.svg b/packages/ui/src/Atoms/Icons/not-started.svg deleted file mode 100644 index b848ce9f1..000000000 --- a/packages/ui/src/Atoms/Icons/not-started.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/page-check-1.svg b/packages/ui/src/Atoms/Icons/page-check-1.svg deleted file mode 100644 index 52600bb3a..000000000 --- a/packages/ui/src/Atoms/Icons/page-check-1.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ui/src/Atoms/Icons/page-check.svg b/packages/ui/src/Atoms/Icons/page-check.svg deleted file mode 100644 index b1a7be091..000000000 --- a/packages/ui/src/Atoms/Icons/page-check.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ui/src/Atoms/Icons/page-cross.svg b/packages/ui/src/Atoms/Icons/page-cross.svg deleted file mode 100644 index daa32892e..000000000 --- a/packages/ui/src/Atoms/Icons/page-cross.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ui/src/Atoms/Icons/page-text-line.svg b/packages/ui/src/Atoms/Icons/page-text-line.svg deleted file mode 100644 index c6abed23f..000000000 --- a/packages/ui/src/Atoms/Icons/page-text-line.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/page-text-solid.svg b/packages/ui/src/Atoms/Icons/page-text-solid.svg deleted file mode 100644 index 19dc06415..000000000 --- a/packages/ui/src/Atoms/Icons/page-text-solid.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/pencil.svg b/packages/ui/src/Atoms/Icons/pencil.svg deleted file mode 100644 index cd510b65e..000000000 --- a/packages/ui/src/Atoms/Icons/pencil.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/people-add-2-line.svg b/packages/ui/src/Atoms/Icons/people-add-2-line.svg deleted file mode 100644 index 06adfdd29..000000000 --- a/packages/ui/src/Atoms/Icons/people-add-2-line.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/people-add.svg b/packages/ui/src/Atoms/Icons/people-add.svg deleted file mode 100644 index 45fbe26a3..000000000 --- a/packages/ui/src/Atoms/Icons/people-add.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/ui/src/Atoms/Icons/plus-large.svg b/packages/ui/src/Atoms/Icons/plus-large.svg deleted file mode 100644 index 38fe572a5..000000000 --- a/packages/ui/src/Atoms/Icons/plus-large.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/plus-small.svg b/packages/ui/src/Atoms/Icons/plus-small.svg deleted file mode 100644 index 4cf246917..000000000 --- a/packages/ui/src/Atoms/Icons/plus-small.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/priority.svg b/packages/ui/src/Atoms/Icons/priority.svg deleted file mode 100644 index 0d2d59422..000000000 --- a/packages/ui/src/Atoms/Icons/priority.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/ui/src/Atoms/Icons/radio-unchecked.svg b/packages/ui/src/Atoms/Icons/radio-unchecked.svg deleted file mode 100644 index 2b36ecc96..000000000 --- a/packages/ui/src/Atoms/Icons/radio-unchecked.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/rec-keyframe.svg b/packages/ui/src/Atoms/Icons/rec-keyframe.svg deleted file mode 100644 index 7f8e0656c..000000000 --- a/packages/ui/src/Atoms/Icons/rec-keyframe.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/settings-gear-2.svg b/packages/ui/src/Atoms/Icons/settings-gear-2.svg deleted file mode 100644 index 2ad9ac8fe..000000000 --- a/packages/ui/src/Atoms/Icons/settings-gear-2.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/shield.svg b/packages/ui/src/Atoms/Icons/shield.svg deleted file mode 100644 index c460d1af9..000000000 --- a/packages/ui/src/Atoms/Icons/shield.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/signature.svg b/packages/ui/src/Atoms/Icons/signature.svg deleted file mode 100644 index 56893cd5f..000000000 --- a/packages/ui/src/Atoms/Icons/signature.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/square-behind-square-2.svg b/packages/ui/src/Atoms/Icons/square-behind-square-2.svg deleted file mode 100644 index 758f95a38..000000000 --- a/packages/ui/src/Atoms/Icons/square-behind-square-2.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/store.svg b/packages/ui/src/Atoms/Icons/store.svg deleted file mode 100644 index c15fa61e0..000000000 --- a/packages/ui/src/Atoms/Icons/store.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/ui/src/Atoms/Icons/sun.svg b/packages/ui/src/Atoms/Icons/sun.svg deleted file mode 100644 index 815ae9d39..000000000 --- a/packages/ui/src/Atoms/Icons/sun.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/packages/ui/src/Atoms/Icons/todo.svg b/packages/ui/src/Atoms/Icons/todo.svg deleted file mode 100644 index 4e9f88b64..000000000 --- a/packages/ui/src/Atoms/Icons/todo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ui/src/Atoms/Icons/todo2.svg b/packages/ui/src/Atoms/Icons/todo2.svg deleted file mode 100644 index 642d0e48b..000000000 --- a/packages/ui/src/Atoms/Icons/todo2.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/packages/ui/src/Atoms/Icons/trash-can-solid.svg b/packages/ui/src/Atoms/Icons/trash-can-solid.svg deleted file mode 100644 index 580f06ea5..000000000 --- a/packages/ui/src/Atoms/Icons/trash-can-solid.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/packages/ui/src/Atoms/Icons/trash-can.svg b/packages/ui/src/Atoms/Icons/trash-can.svg deleted file mode 100644 index b31f128d1..000000000 --- a/packages/ui/src/Atoms/Icons/trash-can.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/packages/ui/src/Atoms/Icons/upload.svg b/packages/ui/src/Atoms/Icons/upload.svg deleted file mode 100644 index 7fde469d4..000000000 --- a/packages/ui/src/Atoms/Icons/upload.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/ui/src/Atoms/Icons/warning.svg b/packages/ui/src/Atoms/Icons/warning.svg deleted file mode 100644 index d11e0f793..000000000 --- a/packages/ui/src/Atoms/Icons/warning.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/ui/src/Atoms/Logo/Logo.tsx b/packages/ui/src/Atoms/Logo/Logo.tsx index 74f4c892d..34cc65192 100644 --- a/packages/ui/src/Atoms/Logo/Logo.tsx +++ b/packages/ui/src/Atoms/Logo/Logo.tsx @@ -1,8 +1,73 @@ +import clsx from "clsx"; +import { useId } from "react"; + type Props = { className?: string; + withPicto?: boolean; }; -export function Logo({ className }: Props) { +export function Logo({ className, withPicto }: Props) { + const id = useId(); + if (withPicto) { + return ( + + + + + + + + + + + + + + + + + + + ); + } return ( ) { return ( - + {children} ); diff --git a/packages/ui/src/Molecules/Badge/FrameworkLogo.stories.tsx b/packages/ui/src/Molecules/Badge/FrameworkLogo.stories.tsx new file mode 100644 index 000000000..182660359 --- /dev/null +++ b/packages/ui/src/Molecules/Badge/FrameworkLogo.stories.tsx @@ -0,0 +1,17 @@ +import { FrameworkLogo } from "./FrameworkLogo"; +import type { Meta, StoryObj } from "@storybook/react"; + +export default { + title: "Molecules/Badges/FrameworkLogo", + component: FrameworkLogo, + argTypes: {}, +} satisfies Meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + name: "SOC 2", + className: "size-12", + }, +}; diff --git a/packages/ui/src/Molecules/Badge/FrameworkLogo.tsx b/packages/ui/src/Molecules/Badge/FrameworkLogo.tsx new file mode 100644 index 000000000..f3c7f711b --- /dev/null +++ b/packages/ui/src/Molecules/Badge/FrameworkLogo.tsx @@ -0,0 +1,23 @@ +import { availableFrameworks } from "@probo/helpers"; +import { Avatar } from "../../Atoms/Avatar/Avatar.tsx"; + +const availableLogos = new Map( + availableFrameworks.map((framework) => [framework.name, framework.logo]), +); + +export function FrameworkLogo({ + name, + alt, + className, +}: { + name: string; + alt?: string; + className?: string; +}) { + const logo = availableLogos.get(name); + return logo ? ( + {alt} + ) : ( + + ); +} diff --git a/packages/ui/src/Molecules/Dialog/Dialog.tsx b/packages/ui/src/Molecules/Dialog/Dialog.tsx index 4179c43c7..e4262ee37 100644 --- a/packages/ui/src/Molecules/Dialog/Dialog.tsx +++ b/packages/ui/src/Molecules/Dialog/Dialog.tsx @@ -91,11 +91,13 @@ export function Dialog({ } }; - const contentProps = closable ? {} : { - onEscapeKeyDown: (e: Event) => e.preventDefault(), - onPointerDownOutside: (e: Event) => e.preventDefault(), - onInteractOutside: (e: Event) => e.preventDefault(), - }; + const contentProps = closable + ? {} + : { + onEscapeKeyDown: (e: Event) => e.preventDefault(), + onPointerDownOutside: (e: Event) => e.preventDefault(), + onInteractOutside: (e: Event) => e.preventDefault(), + }; return ( @@ -142,14 +144,16 @@ export function Dialog({ export function DialogFooter({ children, exitLabel, + className, }: { children?: ReactNode; exitLabel?: string; + className?: string; }) { const { __ } = useTranslate(); const { footer } = dialog(); return ( -