diff --git a/apps/trust/eslint.config.js b/apps/trust/eslint.config.js deleted file mode 100644 index 092408a9f..000000000 --- a/apps/trust/eslint.config.js +++ /dev/null @@ -1,28 +0,0 @@ -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/eslint.config.mjs b/apps/trust/eslint.config.mjs new file mode 100644 index 000000000..80a1e4de5 --- /dev/null +++ b/apps/trust/eslint.config.mjs @@ -0,0 +1,27 @@ +// @ts-check + +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +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, + parserOptions: { + tsconfigRootDir: import.meta.dirname, + }, + }, + plugins: { + "react-hooks": reactHooks, + }, + rules: { + ...reactHooks.configs.recommended.rules, + }, + } +); diff --git a/apps/trust/package.json b/apps/trust/package.json index 078eaad74..554f9e68f 100644 --- a/apps/trust/package.json +++ b/apps/trust/package.json @@ -16,6 +16,7 @@ "@probo/helpers": "1.0.0", "@probo/hooks": "1.0.0", "@probo/i18n": "1.0.0", + "@probo/routes": "^1.0.0", "@probo/ui": "1.0.0", "clsx": "^2.1.1", "react": "^19.1.0", @@ -41,7 +42,6 @@ "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", diff --git a/apps/trust/src/hooks/useDelayedEffect.ts b/apps/trust/src/hooks/useDelayedEffect.ts deleted file mode 100644 index ee421db8a..000000000 --- a/apps/trust/src/hooks/useDelayedEffect.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/useMutationWithToast.ts b/apps/trust/src/hooks/useMutationWithToast.ts index acaff1096..f047a9ad4 100644 --- a/apps/trust/src/hooks/useMutationWithToast.ts +++ b/apps/trust/src/hooks/useMutationWithToast.ts @@ -56,7 +56,7 @@ export function useMutationWithToasts( }) ); }, - [mutate] + [mutate, toast, __, baseOptions] ); return [mutateWithToast, isLoading] as const; diff --git a/apps/trust/src/pages/OverviewPage.tsx b/apps/trust/src/pages/OverviewPage.tsx index ca09a645a..7296d0de2 100644 --- a/apps/trust/src/pages/OverviewPage.tsx +++ b/apps/trust/src/pages/OverviewPage.tsx @@ -175,7 +175,7 @@ function Subprocessors({ } const hasAnyCountries = vendors.some((vendor) => { - const vendorData = vendor.node as any; + const vendorData = vendor.node; return vendorData.countries && vendorData.countries.length > 0; }); diff --git a/apps/trust/src/routes.tsx b/apps/trust/src/routes.tsx index 75f88a074..39ef6d5b5 100644 --- a/apps/trust/src/routes.tsx +++ b/apps/trust/src/routes.tsx @@ -2,18 +2,15 @@ import { createBrowserRouter, Navigate, redirect, - type RouteObject, - useLoaderData, useRouteError, } from "react-router"; -import { type ComponentType, Fragment, Suspense } from "react"; +import { Fragment } 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"; +} from "./providers/RelayProviders.tsx"; +import { loadQuery } from "react-relay"; +import { PageError } from "./components/PageError.tsx"; import { MainLayout } from "/layouts/MainLayout"; import { currentTrustGraphQuery, @@ -24,15 +21,9 @@ 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; -}; +import { TabSkeleton } from "./components/Skeletons/TabSkeleton.tsx"; +import { MainSkeleton } from "./components/Skeletons/MainSkeleton.tsx"; +import { loaderFromQueryLoader, routeFromAppRoute, withQueryRef, type AppRoute } from "@probo/routes"; /** * Top level error boundary @@ -59,47 +50,45 @@ const routes = [ // Custom domain routes (subdomain-based) { path: "/overview", - queryLoader: () => loadQuery(relayEnvironment, currentTrustGraphQuery, {}), - Component: MainLayout, - fallback: MainSkeleton, + loader: loaderFromQueryLoader(() => loadQuery(relayEnvironment, currentTrustGraphQuery, {})), + Component: withQueryRef(MainLayout), + Fallback: MainSkeleton, ErrorBoundary: ErrorBoundary, children: [ { path: "", - fallback: TabSkeleton, + Fallback: TabSkeleton, Component: OverviewPage, }, ], }, { path: "/documents", - queryLoader: () => loadQuery(relayEnvironment, currentTrustGraphQuery, {}), - Component: MainLayout, - fallback: MainSkeleton, + loader: loaderFromQueryLoader(() => loadQuery(relayEnvironment, currentTrustGraphQuery, {})), + Component: withQueryRef(MainLayout), + Fallback: MainSkeleton, ErrorBoundary: ErrorBoundary, children: [ { path: "", - queryLoader: () => - loadQuery(relayEnvironment, currentTrustDocumentsQuery, {}), - fallback: TabSkeleton, - Component: DocumentsPage, + loader: loaderFromQueryLoader(() => loadQuery(relayEnvironment, currentTrustDocumentsQuery, {})), + Fallback: TabSkeleton, + Component: withQueryRef(DocumentsPage), }, ], }, { path: "/subprocessors", - queryLoader: () => loadQuery(relayEnvironment, currentTrustGraphQuery, {}), - Component: MainLayout, - fallback: MainSkeleton, + loader: loaderFromQueryLoader(() => loadQuery(relayEnvironment, currentTrustGraphQuery, {})), + Component: withQueryRef(MainLayout), + Fallback: MainSkeleton, ErrorBoundary: ErrorBoundary, children: [ { path: "", - queryLoader: () => - loadQuery(relayEnvironment, currentTrustVendorsQuery, {}), - fallback: TabSkeleton, - Component: SubprocessorsPage, + loader: loaderFromQueryLoader(() => loadQuery(relayEnvironment, currentTrustVendorsQuery, {})), + Fallback: TabSkeleton, + Component: withQueryRef(SubprocessorsPage), }, ], }, @@ -115,56 +104,6 @@ const routes = [ }, ] 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; -} - // Detect basename from current URL path // If URL starts with /trust/{slug}, extract that as the basename // Otherwise, use "/" for custom domains @@ -174,6 +113,6 @@ function getBasename(): string { return trustMatch ? trustMatch[0] : "/"; } -export const router = createBrowserRouter(routes.map(routeTransformer), { +export const router = createBrowserRouter(routes.map(routeFromAppRoute), { basename: getBasename(), }); diff --git a/package-lock.json b/package-lock.json index 675a2f99b..86fd1916f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,10 +22,11 @@ "version": "0.0.0", "dependencies": { "@hookform/resolvers": "^5.0.1", - "@probo/helpers": "1.0.0", + "@probo/helpers": "^1.0.0", "@probo/hooks": "1.0.0", "@probo/i18n": "1.0.0", "@probo/react-lazy": "1.0.0", + "@probo/routes": "^1.0.0", "@probo/ui": "1.0.0", "@probo/vendors": "0.0.1", "@tanstack/react-query": "^5.76.1", @@ -72,6 +73,7 @@ "@probo/helpers": "1.0.0", "@probo/hooks": "1.0.0", "@probo/i18n": "1.0.0", + "@probo/routes": "^1.0.0", "@probo/ui": "1.0.0", "clsx": "^2.1.1", "react": "^19.1.0", diff --git a/packages/helpers/src/routes.tsx b/packages/helpers/src/routes.tsx deleted file mode 100644 index 29ad91e38..000000000 --- a/packages/helpers/src/routes.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { ComponentType, LazyExoticComponent } from "react"; -import { EnvironmentProviderOptions, PreloadedQuery } from "react-relay"; -import { LoaderFunction, LoaderFunctionArgs, RouteObject, useLoaderData } from "react-router"; -import { OperationType } from "relay-runtime"; -import { useCleanup } from "./useDelayedEffect"; - -export function withQueryRef< - TQuery extends OperationType, - TEnvironmentProviderOptions = EnvironmentProviderOptions ->( - Component: LazyExoticComponent }>>, -) { - return () => { - const { queryRef, dispose } = useLoaderData(); - - useCleanup(dispose, 1000); - - return - } -} - -export function loaderFromQueryLoader< - TQuery extends OperationType, - TEnvironmentProviderOptions = EnvironmentProviderOptions ->( - queryLoader: (params: Record) => PreloadedQuery -): LoaderFunction { - return ({ params }: LoaderFunctionArgs) => { - const query = queryLoader(params as Record); - return { - queryRef: query, - dispose: query.dispose, - }; - } -} - -export type AppRoute = Omit & { - children?: AppRoute[]; - fallback?: ComponentType; -} diff --git a/packages/helpers/src/useDelayedEffect.ts b/packages/helpers/src/useDelayedEffect.ts deleted file mode 100644 index ee421db8a..000000000 --- a/packages/helpers/src/useDelayedEffect.ts +++ /dev/null @@ -1,19 +0,0 @@ -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/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index bc44b993c..107936e98 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -3,3 +3,4 @@ export { useToggle } from "./useToggle"; export { useRefSync } from "./useRefSync"; export { useList } from "./useList"; export { useStateWithRef } from "./useStateWithRef"; +export { useCleanup } from "./useCleanup";