diff --git a/apps/legacy/package.json b/apps/legacy/package.json index 1272c5a4..034bf240 100644 --- a/apps/legacy/package.json +++ b/apps/legacy/package.json @@ -35,7 +35,6 @@ "@types/react": "^19.2.13", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", - "msw": "^2.12.14", "tailwindcss": "^4.0.0", "vite": "^8.0.2", "vite-tsconfig-paths": "^6.1.0" diff --git a/apps/legacy/tsconfig.json b/apps/legacy/tsconfig.json index 653831ac..43afaf76 100644 --- a/apps/legacy/tsconfig.json +++ b/apps/legacy/tsconfig.json @@ -3,7 +3,6 @@ "include": ["src/**/*"], "compilerOptions": { "types": ["node", "vite/client"], - "baseUrl": ".", "paths": { "~/*": ["./src/*"] } diff --git a/apps/smartem/.env.example b/apps/smartem/.env.example new file mode 100644 index 00000000..75534e0a --- /dev/null +++ b/apps/smartem/.env.example @@ -0,0 +1,4 @@ +VITE_KEYCLOAK_URL=https://identity.diamond.ac.uk +VITE_KEYCLOAK_REALM=master +VITE_KEYCLOAK_CLIENT_ID=smartem-frontend +VITE_AUTH_ENABLED=false diff --git a/apps/smartem/package.json b/apps/smartem/package.json index ffb4b74b..355a570a 100644 --- a/apps/smartem/package.json +++ b/apps/smartem/package.json @@ -15,7 +15,9 @@ "@smartem/ui": "*", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.3.9", "@mui/material": "^7.0.2", + "keycloak-js": "^26.1.0", "@tanstack/react-query": "^5.95.2", "@tanstack/react-router": "^1.168.3", "@tanstack/router-devtools": "^1.166.11", @@ -30,7 +32,6 @@ "@types/react": "^19.2.13", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", - "msw": "^2.12.14", "tailwindcss": "^4.0.0", "vite": "^8.0.2", "vite-tsconfig-paths": "^6.1.0" diff --git a/apps/smartem/src/auth/AuthGate.tsx b/apps/smartem/src/auth/AuthGate.tsx new file mode 100644 index 00000000..fe5089ae --- /dev/null +++ b/apps/smartem/src/auth/AuthGate.tsx @@ -0,0 +1,12 @@ +import { setAuthToken } from '@smartem/api' +import type { PropsWithChildren } from 'react' +import { AuthProvider } from './AuthProvider' +import { isAuthEnabled } from './config' + +export const AuthGate = ({ children }: PropsWithChildren) => { + if (!isAuthEnabled()) { + return <>{children} + } + + return {children} +} diff --git a/apps/smartem/src/auth/AuthProvider.tsx b/apps/smartem/src/auth/AuthProvider.tsx new file mode 100644 index 00000000..2218ce0b --- /dev/null +++ b/apps/smartem/src/auth/AuthProvider.tsx @@ -0,0 +1,123 @@ +import Keycloak, { type KeycloakError } from 'keycloak-js' +import { + createContext, + type PropsWithChildren, + useContext, + useEffect, + useRef, + useState, +} from 'react' +import { keycloakConfig } from './config' +import type { Auth, AuthUser } from './types' + +const MIN_SECONDS_BEFORE_EXPIRY = 10 +const MIN_REFRESH_INTERVAL_SECONDS = 5 + +const defaultAuth: Auth = { + initialised: false, + authenticated: false, + login: () => {}, + logout: () => {}, + getToken: () => '', +} + +const AuthContext = createContext(defaultAuth) + +export const useAuth = () => useContext(AuthContext) + +interface AuthProviderProps extends PropsWithChildren { + onTokenChange?: (token: string) => void +} + +function buildAuth(keycloak: Keycloak): Auth { + const auth: Auth = { + initialised: keycloak.didInitialize, + authenticated: keycloak.authenticated ?? false, + login: () => void keycloak.login({}), + logout: () => void keycloak.logout({}), + getToken: () => keycloak.token ?? '', + } + + if (keycloak.authenticated && keycloak.idTokenParsed) { + auth.user = { + name: keycloak.idTokenParsed.name ?? '', + givenName: keycloak.idTokenParsed.given_name ?? '', + familyName: keycloak.idTokenParsed.family_name ?? '', + fedId: keycloak.idTokenParsed.fedId ?? '', + email: keycloak.idTokenParsed.email ?? '', + } satisfies AuthUser + } + + return auth +} + +export const AuthProvider = ({ children, onTokenChange }: AuthProviderProps) => { + const [auth, setAuth] = useState(defaultAuth) + const refreshTimer = useRef>(undefined) + const onTokenChangeRef = useRef(onTokenChange) + onTokenChangeRef.current = onTokenChange + + useEffect(() => { + const keycloak = new Keycloak(keycloakConfig) + + const emitToken = () => { + const token = keycloak.authenticated && keycloak.token ? keycloak.token : '' + onTokenChangeRef.current?.(token) + } + + const scheduleRefresh = () => { + clearTimeout(refreshTimer.current) + + if (!keycloak.idTokenParsed?.exp || !keycloak.idTokenParsed?.iat) return + + const tokenLifetime = keycloak.idTokenParsed.exp - keycloak.idTokenParsed.iat + let refreshIn = tokenLifetime - MIN_SECONDS_BEFORE_EXPIRY + if (refreshIn < MIN_REFRESH_INTERVAL_SECONDS) { + refreshIn = MIN_REFRESH_INTERVAL_SECONDS + } + + refreshTimer.current = setTimeout(() => { + keycloak.updateToken(-1).catch((err: KeycloakError) => { + console.error('Token refresh failed:', err) + setAuth((prev) => ({ ...prev, error: 'Token refresh failed' })) + }) + }, refreshIn * 1000) + } + + keycloak.onAuthSuccess = () => { + setAuth(buildAuth(keycloak)) + emitToken() + scheduleRefresh() + } + + keycloak.onAuthRefreshSuccess = () => { + emitToken() + scheduleRefresh() + } + + keycloak.onAuthLogout = () => { + clearTimeout(refreshTimer.current) + setAuth(buildAuth(keycloak)) + emitToken() + } + + keycloak.onAuthError = (error: KeycloakError) => { + console.error('Keycloak auth error:', error) + setAuth((prev) => ({ ...prev, error: `Auth error: ${error.error}` })) + } + + keycloak + .init({ onLoad: 'check-sso' }) + .then(() => setAuth(buildAuth(keycloak))) + .catch((err) => { + console.error('Keycloak init failed:', err) + setAuth({ ...defaultAuth, initialised: true, error: 'Failed to connect to Keycloak' }) + }) + + return () => { + clearTimeout(refreshTimer.current) + } + }, []) + + return {children} +} diff --git a/apps/smartem/src/auth/config.ts b/apps/smartem/src/auth/config.ts new file mode 100644 index 00000000..785c52e0 --- /dev/null +++ b/apps/smartem/src/auth/config.ts @@ -0,0 +1,14 @@ +import type { KeycloakServerConfig } from 'keycloak-js' + +export const isAuthEnabled = (): boolean => { + if (import.meta.env.VITE_ENABLE_MOCKS === 'true') { + return import.meta.env.VITE_AUTH_ENABLED === 'true' + } + return import.meta.env.VITE_AUTH_ENABLED !== 'false' +} + +export const keycloakConfig: KeycloakServerConfig = { + url: import.meta.env.VITE_KEYCLOAK_URL || 'https://identity.diamond.ac.uk', + realm: import.meta.env.VITE_KEYCLOAK_REALM || 'master', + clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID || 'smartem-frontend', +} diff --git a/apps/smartem/src/auth/index.ts b/apps/smartem/src/auth/index.ts new file mode 100644 index 00000000..c5bdb5dc --- /dev/null +++ b/apps/smartem/src/auth/index.ts @@ -0,0 +1,3 @@ +export { AuthGate } from './AuthGate' +export { AuthProvider, useAuth } from './AuthProvider' +export type { Auth, AuthUser } from './types' diff --git a/apps/smartem/src/auth/types.ts b/apps/smartem/src/auth/types.ts new file mode 100644 index 00000000..4f290028 --- /dev/null +++ b/apps/smartem/src/auth/types.ts @@ -0,0 +1,17 @@ +export type AuthUser = { + name: string + givenName: string + familyName: string + fedId: string + email: string +} + +export interface Auth { + initialised: boolean + authenticated: boolean + user?: AuthUser + login: () => void + logout: () => void + getToken: () => string + error?: string +} diff --git a/apps/smartem/src/main.tsx b/apps/smartem/src/main.tsx index b53f9c6a..d8d10fa0 100644 --- a/apps/smartem/src/main.tsx +++ b/apps/smartem/src/main.tsx @@ -1,6 +1,7 @@ import { createRouter, RouterProvider } from '@tanstack/react-router' import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' +import { AuthGate } from './auth' import { routeTree } from './routeTree.gen' import './app.css' @@ -30,7 +31,9 @@ if (rootElement && !rootElement.innerHTML) { const root = createRoot(rootElement) root.render( - + + + ) }) diff --git a/apps/smartem/src/mocks/browser.ts b/apps/smartem/src/mocks/browser.ts index 7fd3db43..e5c3e280 100644 --- a/apps/smartem/src/mocks/browser.ts +++ b/apps/smartem/src/mocks/browser.ts @@ -1,10 +1,4 @@ import { mswHandlers } from '@smartem/api' -import type { RequestHandler } from 'msw' import { setupWorker } from 'msw/browser' -const generatedHandlers = Object.values(mswHandlers) - .filter((value) => typeof value === 'function') - .filter((fn) => fn.name.includes('MockHandler')) - .map((handlerFn) => (handlerFn as () => RequestHandler)()) - -export const worker = setupWorker(...generatedHandlers) +export const worker = setupWorker(...mswHandlers.getDefaultMock()) diff --git a/apps/smartem/src/routes/__root.tsx b/apps/smartem/src/routes/__root.tsx index fb3fdca7..b82198b4 100644 --- a/apps/smartem/src/routes/__root.tsx +++ b/apps/smartem/src/routes/__root.tsx @@ -1,7 +1,10 @@ -import { Box, Typography } from '@mui/material' +import { AccountCircle, Login } from '@mui/icons-material' +import { Box, IconButton, Menu, MenuItem, Typography } from '@mui/material' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createRootRoute, Outlet } from '@tanstack/react-router' import { TanStackRouterDevtools } from '@tanstack/router-devtools' +import { useState } from 'react' +import { useAuth } from '../auth' const queryClient = new QueryClient({ defaultOptions: { @@ -16,6 +19,40 @@ export const Route = createRootRoute({ component: RootComponent, }) +function AuthControls() { + const auth = useAuth() + const [anchorEl, setAnchorEl] = useState(null) + + if (!auth.initialised) return null + + if (!auth.authenticated) { + return ( + auth.login()}> + + + ) + } + + return ( + <> + setAnchorEl(e.currentTarget)}> + + + setAnchorEl(null)}> + {auth.user?.name || auth.user?.email} + { + setAnchorEl(null) + auth.logout() + }} + > + Logout + + + + ) +} + function RootComponent() { return ( @@ -27,11 +64,15 @@ function RootComponent() { py: 2, borderBottom: 1, borderColor: 'divider', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', }} > SmartEM + diff --git a/apps/smartem/tsconfig.json b/apps/smartem/tsconfig.json index 653831ac..43afaf76 100644 --- a/apps/smartem/tsconfig.json +++ b/apps/smartem/tsconfig.json @@ -3,7 +3,6 @@ "include": ["src/**/*"], "compilerOptions": { "types": ["node", "vite/client"], - "baseUrl": ".", "paths": { "~/*": ["./src/*"] } diff --git a/enhancements.md b/enhancements.md deleted file mode 100644 index 2a2ed090..00000000 --- a/enhancements.md +++ /dev/null @@ -1,2202 +0,0 @@ -# SmartEM Frontend - Comprehensive Improvement Analysis - -**Analysis Date:** 2025-10-07 -**Framework:** React 19 + React Router 7 + TypeScript -**Current State:** Production-ready application with modern tooling - ---- - -## Executive Summary - -The SmartEM frontend is a well-architected React application using modern technologies including React 19, React Router 7, TanStack Query, and Material-UI. The codebase demonstrates good practices in several areas, particularly in API client generation and type safety. However, there are significant opportunities for improvement in code organization, component architecture, testing, and developer tooling. - -**Overall Assessment:** 7/10 - Solid foundation with room for modernization and refinement. - ---- - -## 1. Project Structure and Organization - -### Current State - -``` -app/ -├── api/ -│ ├── generated/ # Auto-generated (300+ files) -│ ├── mutator.ts -│ ├── version-check.ts -│ └── openapi.json -├── components/ # 6 components, mixed purposes -├── hooks/ # Empty directory -├── mocks/ # MSW setup (2 files) -├── routes/ # 10 route components (94-432 lines each) -├── utils/ # Empty directory -├── routes.ts -├── root.tsx -└── entry.client.tsx -``` - -### Issues Identified - -1. **Flat component structure** - All components in single directory without categorization -2. **Large route components** - Some routes have 400+ lines (atlas.tsx: 432 lines, squareLR.tsx: 396 lines) -3. **Empty directories** - `hooks/` and `utils/` directories exist but are empty -4. **Mixed responsibilities** - Components contain business logic, API calls, and presentation -5. **No feature-based organization** - Everything organized by technical type rather than domain - -### Recommended Improvements - -#### 1.1 Adopt Feature-Based Structure - -**Effort:** Medium | **Priority:** High | **Impact:** High - -Reorganize code by domain features: - -``` -app/ -├── features/ -│ ├── acquisitions/ -│ │ ├── components/ -│ │ ├── hooks/ -│ │ ├── routes/ -│ │ └── types.ts -│ ├── atlas/ -│ │ ├── components/ -│ │ ├── hooks/ -│ │ └── routes/ -│ ├── grids/ -│ └── predictions/ -├── shared/ -│ ├── components/ # Navbar, ApiVersionCheck, etc. -│ ├── hooks/ -│ ├── utils/ -│ └── theme/ -├── api/ -└── core/ # Root-level app setup -``` - -**Benefits:** - -- Better code discoverability -- Easier to understand feature scope -- Reduced coupling between features -- Easier onboarding for new developers -- Clearer ownership boundaries - -#### 1.2 Split Large Route Components - -**Effort:** Medium | **Priority:** High | **Impact:** High - -Break down large route files (atlas.tsx, squareLR.tsx, grid.tsx) into smaller, focused components: - -**Current:** `app/routes/atlas.tsx` (432 lines) -**Proposed:** - -``` -app/features/atlas/ -├── routes/ -│ └── AtlasRoute.tsx (50-100 lines) -├── components/ -│ ├── AtlasViewer.tsx -│ ├── LatentSpacePanel.tsx -│ ├── PredictionControls.tsx -│ └── GridSquareOverlay.tsx -├── hooks/ -│ ├── useAtlasData.ts -│ ├── usePredictions.ts -│ └── useLatentRepresentation.ts -└── types.ts -``` - -**Benefits:** - -- Improved testability -- Better code reuse -- Easier to understand and maintain -- Clearer separation of concerns - -#### 1.3 Create Proper Barrel Exports - -**Effort:** Low | **Priority:** Medium | **Impact:** Medium - -Add index.ts files for cleaner imports: - -```typescript -// app/features/atlas/index.ts -export { AtlasRoute } from './routes/AtlasRoute' -export * from './types' - -// Usage -import { AtlasRoute } from '~/features/atlas' -``` - -**Benefits:** - -- Cleaner import statements -- Better encapsulation -- Easier refactoring - ---- - -## 2. TypeScript Usage and Type Safety - -### Current State - -**Strengths:** - -- TypeScript strict mode enabled (`tsconfig.json`: `"strict": true`) -- Auto-generated types from OpenAPI spec -- Comprehensive API type coverage -- Proper type exports from generated code - -**Issues:** - -1. **4 instances of `@ts-ignore` or `any` types** in application code -2. **Inconsistent type imports** - Mix of `import type` and regular imports -3. **Missing custom types** - Domain types not formalized -4. **Inline type definitions** - Types defined in components instead of separate files - -### Recommended Improvements - -#### 2.1 Remove All Type Suppressions - -**Effort:** Low | **Priority:** High | **Impact:** Medium - -Current issues in `/home/vredchenko/dev/DLS/smartem-frontend/app/api/mutator.ts`: - -```typescript -// @ts-ignore -promise.cancel = () => { - source.cancel('Query was cancelled') -} -``` - -**Solution:** - -```typescript -type CancellablePromise = Promise & { - cancel: () => void -} - -export const customInstance = ( - config: AxiosRequestConfig, - options?: AxiosRequestConfig -): CancellablePromise => { - const source = Axios.CancelToken.source() - const promise = AXIOS_INSTANCE({ - ...config, - ...options, - cancelToken: source.token, - }).then(({ data }) => data) as CancellablePromise - - promise.cancel = () => { - source.cancel('Query was cancelled') - } - - return promise -} -``` - -**Benefits:** - -- Full type safety -- Better IDE autocomplete -- Catch errors at compile time - -#### 2.2 Consolidate Types into Domain Files - -**Effort:** Medium | **Priority:** Medium | **Impact:** Medium - -**Current:** Types scattered across route files - -```typescript -// In atlas.tsx -type GridSquare = GridSquareResponse -type PredictionModel = QualityPredictionModelResponse -type Coords = { x: number; y: number; index: number } -``` - -**Proposed:** - -```typescript -// app/features/atlas/types.ts -export type GridSquare = GridSquareResponse -export type PredictionModel = QualityPredictionModelResponse -export type Coords = { x: number; y: number; index: number } -export type AtlasViewState = { - selectedSquare: string - showPredictions: boolean - selectionFrozen: boolean -} -``` - -**Benefits:** - -- Single source of truth for types -- Easier to share types across files -- Better type documentation - -#### 2.3 Use Consistent Type Import Syntax - -**Effort:** Low | **Priority:** Low | **Impact:** Low - -Standardize on `import type` for all type-only imports: - -```typescript -// Before (inconsistent) -import { GridSquareResponse } from '../api/generated/models' -import type { SelectChangeEvent } from '@mui/material' - -// After (consistent) -import type { GridSquareResponse } from '../api/generated/models' -import type { SelectChangeEvent } from '@mui/material' -``` - -**Benefits:** - -- Clearer intent -- Better tree-shaking -- Faster compilation - -#### 2.4 Enable Additional TypeScript Strict Checks - -**Effort:** Low | **Priority:** Medium | **Impact:** Medium - -Update `tsconfig.json`: - -```json -{ - "compilerOptions": { - "strict": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - "exactOptionalPropertyTypes": true - } -} -``` - -**Benefits:** - -- Catch more potential runtime errors -- Better null/undefined handling -- Stronger type guarantees - ---- - -## 3. React Patterns and Best Practices - -### Current State - -**Good Practices:** - -- Using React 19 features -- Functional components throughout -- React.StrictMode enabled -- Using hooks appropriately - -**Issues Identified:** - -1. **72 instances of `React.useState`, `React.useEffect`** - Unnecessary namespace usage -2. **No custom hooks** - Business logic embedded in components -3. **Prop drilling** - No context usage for shared state -4. **Mixed concerns** - Components handle data fetching, state, and UI -5. **Inline styles** - Heavy use of `style={}` props (78 instances) -6. **No component composition patterns** - Monolithic components -7. **Missing error boundaries** - Only root-level error boundary -8. **No loading states abstraction** - Repeated loading/error patterns - -### Recommended Improvements - -#### 3.1 Remove React Namespace from Hooks - -**Effort:** Low | **Priority:** Medium | **Impact:** Low - -**Current:** - -```typescript -import React from 'react' -const [state, setState] = React.useState(false) -``` - -**Proposed:** - -```typescript -import { useState } from 'react' -const [state, setState] = useState(false) -``` - -**Benefits:** - -- Cleaner code -- Modern React conventions -- Smaller bundle (tree-shaking) - -#### 3.2 Extract Custom Hooks - -**Effort:** Medium | **Priority:** High | **Impact:** High - -**Current:** Business logic in components - -```typescript -// In atlas.tsx (432 lines) -export default function Atlas({ loaderData, params }: Route.ComponentProps) { - const [predictions, setPredictions] = React.useState>() - const [latentRep, setLatentRep] = React.useState>() - - const handleChange = async (event: SelectChangeEvent) => { - setPredictionModel(event.target.value) - const preds = await getPredictions(event.target.value, params.gridId) - setPredictions(preds) - } - // ... 400 more lines -} -``` - -**Proposed:** - -```typescript -// app/features/atlas/hooks/usePredictions.ts -export function usePredictions(gridId: string) { - const [predictions, setPredictions] = useState>() - const [model, setModel] = useState('') - const [isLoading, setIsLoading] = useState(false) - - const loadPredictions = useCallback( - async (modelName: string) => { - setIsLoading(true) - setModel(modelName) - try { - const preds = await getPredictions(modelName, gridId) - setPredictions(preds) - } finally { - setIsLoading(false) - } - }, - [gridId] - ) - - return { predictions, model, isLoading, loadPredictions } -} - -// app/features/atlas/routes/AtlasRoute.tsx -export default function Atlas({ loaderData, params }: Route.ComponentProps) { - const { predictions, loadPredictions } = usePredictions(params.gridId) - const { latentRep, loadLatentRep } = useLatentRepresentation(params.gridId) - // Component now 100 lines, focused on UI -} -``` - -**Benefits:** - -- Testable business logic -- Reusable across components -- Easier to understand component purpose -- Better separation of concerns - -#### 3.3 Create Shared UI Component Abstractions - -**Effort:** Medium | **Priority:** High | **Impact:** High - -**Pattern identified:** Repeated loading/error/data pattern - -```typescript -// Appears in multiple routes -{isLoading ? ( - - - -) : error ? ( - Error: {error.message} -) : ( - // render data -)} -``` - -**Proposed:** - -```typescript -// app/shared/components/QueryStateHandler.tsx -export function QueryStateHandler({ - isLoading, - error, - data, - children, - loadingMessage = 'Loading...', - emptyMessage = 'No data found' -}: QueryStateHandlerProps) { - if (isLoading) return - if (error) return - if (!data || (Array.isArray(data) && data.length === 0)) { - return - } - return <>{children(data)} -} - -// Usage - - {(data) => } - -``` - -**Benefits:** - -- DRY principle -- Consistent UX -- Easier to update loading/error states globally - -#### 3.4 Replace Inline Styles with Styled Components or Theme - -**Effort:** High | **Priority:** Medium | **Impact:** Medium - -**Current:** 78 instances of inline styles - -```typescript - - -``` - -**Proposed Option 1 - MUI sx prop:** - -```typescript - - -``` - -**Proposed Option 2 - styled components:** - -```typescript -const StyledContainer = styled(Container)(({ theme }) => ({ - width: '100%', - paddingTop: theme.spacing(6.25), -})) -``` - -**Benefits:** - -- Theme-aware styling -- Type-safe style props -- Better performance (no style recalculation) -- Easier to maintain - -#### 3.5 Implement Compound Components Pattern - -**Effort:** Medium | **Priority:** Medium | **Impact:** Medium - -For complex components like Atlas viewer: - -```typescript -// app/features/atlas/components/AtlasViewer/index.tsx -export const AtlasViewer = { - Root: AtlasViewerRoot, - Image: AtlasImage, - Overlay: GridSquareOverlay, - Controls: AtlasControls, - LatentPanel: LatentSpacePanel -} - -// Usage - - - - - - - - {showLatent && } - -``` - -**Benefits:** - -- Better component composition -- Clearer component hierarchy -- More flexible layouts -- Self-documenting API - -#### 3.6 Add Error Boundaries per Feature - -**Effort:** Low | **Priority:** Medium | **Impact:** Medium - -**Current:** Only root error boundary - -**Proposed:** - -```typescript -// app/shared/components/ErrorBoundary.tsx -export class FeatureErrorBoundary extends Component { - static getDerivedStateFromError(error: Error) { - return { hasError: true, error } - } - - componentDidCatch(error: Error, errorInfo: ErrorInfo) { - logErrorToService(error, errorInfo) - } - - render() { - if (this.state.hasError) { - return this.props.fallback || - } - return this.props.children - } -} - -// Usage in routes - - - -``` - -**Benefits:** - -- Graceful degradation -- Better error isolation -- Improved UX -- Error tracking - ---- - -## 4. Build Tooling and Configuration - -### Current State - -**Configuration Files:** - -- `vite.config.ts` - Minimal config (18 lines) -- `react-router.config.ts` - Very basic (7 lines) -- `tsconfig.json` - Good strict settings -- `orval.config.ts` - Well configured - -**Strengths:** - -- Modern Vite setup -- Fast HMR -- Good proxy configuration -- TypeScript path aliases configured - -**Issues:** - -1. **No bundle analysis** - Can't analyze bundle size -2. **No environment validation** - No schema for env vars -3. **No source maps configuration** - Debug experience could be better -4. **No build optimization config** - Using defaults -5. **Missing performance budgets** - -### Recommended Improvements - -#### 4.1 Add Bundle Analysis - -**Effort:** Low | **Priority:** Medium | **Impact:** Medium - -```typescript -// vite.config.ts -import { visualizer } from 'rollup-plugin-visualizer' - -export default defineConfig(({ mode }) => ({ - plugins: [ - tailwindcss(), - reactRouter(), - tsconfigPaths(), - mode === 'analyze' && - visualizer({ - open: true, - filename: 'bundle-analysis.html', - gzipSize: true, - brotliSize: true, - }), - ].filter(Boolean), -})) -``` - -Add script to `package.json`: - -```json -{ - "scripts": { - "analyze": "vite build --mode analyze" - } -} -``` - -**Benefits:** - -- Identify large dependencies -- Optimize bundle size -- Find code splitting opportunities - -#### 4.2 Add Environment Variable Validation - -**Effort:** Low | **Priority:** High | **Impact:** Medium - -```typescript -// app/config/env.ts -import { z } from 'zod' - -const envSchema = z.object({ - VITE_API_ENDPOINT: z.string().url().optional(), - VITE_ENABLE_MOCKS: z - .string() - .transform((val) => val === 'true') - .optional(), - MODE: z.enum(['development', 'production', 'test']), - DEV: z.boolean(), - PROD: z.boolean(), -}) - -export const env = envSchema.parse({ - VITE_API_ENDPOINT: import.meta.env.VITE_API_ENDPOINT, - VITE_ENABLE_MOCKS: import.meta.env.VITE_ENABLE_MOCKS, - MODE: import.meta.env.MODE, - DEV: import.meta.env.DEV, - PROD: import.meta.env.PROD, -}) - -// Usage -import { env } from '~/config/env' -const apiUrl = env.VITE_API_ENDPOINT || 'http://localhost:8000' -``` - -**Benefits:** - -- Type-safe environment variables -- Fail fast on misconfiguration -- Clear documentation of required env vars - -#### 4.3 Optimize Build Configuration - -**Effort:** Medium | **Priority:** Medium | **Impact:** Medium - -```typescript -// vite.config.ts -export default defineConfig({ - plugins: [tailwindcss(), reactRouter(), tsconfigPaths()], - - build: { - target: 'es2022', - minify: 'terser', - terserOptions: { - compress: { - drop_console: true, // Remove console.logs in production - drop_debugger: true, - }, - }, - rollupOptions: { - output: { - manualChunks: { - 'vendor-react': ['react', 'react-dom', 'react-router'], - 'vendor-mui': ['@mui/material', '@mui/icons-material'], - 'vendor-query': ['@tanstack/react-query'], - 'vendor-charts': ['@mui/x-charts', 'd3-array'], - }, - }, - }, - sourcemap: true, - chunkSizeWarningLimit: 1000, - }, - - server: { - proxy: { - '/api': { - target: 'http://localhost:8000', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, ''), - }, - }, - }, -}) -``` - -**Benefits:** - -- Better caching -- Faster initial load -- Smaller bundle sizes -- Better debugging - -#### 4.4 Add Performance Budgets - -**Effort:** Low | **Priority:** Low | **Impact:** Low - -```json -// package.json -{ - "budgets": { - "client/index.html": "500kb", - "client/assets/*.js": "200kb", - "client/assets/*.css": "50kb" - } -} -``` - -**Benefits:** - -- Prevent bundle bloat -- Maintain performance standards -- Early warning system - ---- - -## 5. Testing Setup and Coverage - -### Current State - -**Issues:** - -- **NO testing framework configured** (no vitest, jest, or playwright) -- **NO test files** found in the codebase -- **NO test scripts** in package.json -- **0% test coverage** -- NO CI/CD pipeline detected - -This is the **most critical gap** in the codebase. - -### Recommended Improvements - -#### 5.1 Set Up Vitest for Unit/Integration Tests - -**Effort:** Medium | **Priority:** CRITICAL | **Impact:** High - -**Install dependencies:** - -```json -{ - "devDependencies": { - "@testing-library/react": "^16.0.0", - "@testing-library/jest-dom": "^6.1.5", - "@testing-library/user-event": "^14.5.1", - "@vitest/ui": "^2.0.0", - "vitest": "^2.0.0", - "happy-dom": "^12.10.3" - } -} -``` - -**Create config:** - -```typescript -// vitest.config.ts -import { defineConfig } from 'vitest/config' -import react from '@vitejs/plugin-react' -import tsconfigPaths from 'vite-tsconfig-paths' - -export default defineConfig({ - plugins: [react(), tsconfigPaths()], - test: { - globals: true, - environment: 'happy-dom', - setupFiles: ['./app/test/setup.ts'], - coverage: { - provider: 'v8', - reporter: ['text', 'html', 'json'], - exclude: ['app/api/generated/**', '**/*.config.{ts,js}', '**/types.ts'], - thresholds: { - lines: 70, - functions: 70, - branches: 70, - statements: 70, - }, - }, - }, -}) -``` - -**Add scripts:** - -```json -{ - "scripts": { - "test": "vitest", - "test:ui": "vitest --ui", - "test:coverage": "vitest --coverage" - } -} -``` - -**Benefits:** - -- Catch bugs early -- Safer refactoring -- Documentation through tests -- Better code quality - -#### 5.2 Create Test Structure - -**Effort:** Low | **Priority:** High | **Impact:** Medium - -``` -app/ -├── test/ -│ ├── setup.ts -│ ├── utils/ -│ │ ├── test-utils.tsx # Custom render with providers -│ │ ├── mocks/ # Mock data factories -│ │ └── fixtures.ts -│ └── __mocks__/ # Global mocks -└── features/ - └── atlas/ - ├── components/ - │ ├── AtlasViewer.tsx - │ └── __tests__/ - │ └── AtlasViewer.test.tsx - └── hooks/ - ├── usePredictions.ts - └── __tests__/ - └── usePredictions.test.ts -``` - -#### 5.3 Write Tests for Critical Paths - -**Effort:** High | **Priority:** High | **Impact:** High - -**Example hook test:** - -```typescript -// app/features/atlas/hooks/__tests__/usePredictions.test.ts -import { renderHook, waitFor } from '@testing-library/react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { usePredictions } from '../usePredictions' - -describe('usePredictions', () => { - it('should load predictions when model changes', async () => { - const { result } = renderHook(() => usePredictions('grid-123'), { - wrapper: createQueryWrapper(), - }) - - expect(result.current.predictions).toBeUndefined() - - await act(() => result.current.loadPredictions('model-1')) - - await waitFor(() => { - expect(result.current.predictions).toBeDefined() - expect(result.current.isLoading).toBe(false) - }) - }) -}) -``` - -**Example component test:** - -```typescript -// app/shared/components/__tests__/QueryStateHandler.test.tsx -import { render, screen } from '@testing-library/react' -import { QueryStateHandler } from '../QueryStateHandler' - -describe('QueryStateHandler', () => { - it('shows loading state', () => { - render( - - {() =>
Data
} -
- ) - expect(screen.getByRole('progressbar')).toBeInTheDocument() - }) - - it('shows error state', () => { - const error = new Error('Failed to load') - render( - - {() =>
Data
} -
- ) - expect(screen.getByText(/failed to load/i)).toBeInTheDocument() - }) -}) -``` - -**Priority test coverage:** - -1. Custom hooks (100% coverage goal) -2. Shared components (90% coverage goal) -3. API utilities (80% coverage goal) -4. Route components (60% coverage goal - mainly integration tests) - -#### 5.4 Add E2E Testing with Playwright - -**Effort:** Medium | **Priority:** Medium | **Impact:** High - -```typescript -// playwright.config.ts -import { defineConfig } from '@playwright/test' - -export default defineConfig({ - testDir: './e2e', - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - use: { - baseURL: 'http://localhost:5174', - trace: 'on-first-retry', - }, - webServer: { - command: 'npm run dev', - url: 'http://localhost:5174', - reuseExistingServer: !process.env.CI, - }, -}) -``` - -**Example E2E test:** - -```typescript -// e2e/acquisition-flow.spec.ts -import { test, expect } from '@playwright/test' - -test('user can view and select acquisition', async ({ page }) => { - await page.goto('/') - - // Should show acquisitions table - await expect(page.getByRole('table')).toBeVisible() - - // Click on first acquisition - await page.getByRole('row').first().click() - - // Should navigate to grids page - await expect(page).toHaveURL(/\/acquisitions\/.*/) - await expect(page.getByRole('heading', { name: /grids/i })).toBeVisible() -}) -``` - -**Benefits:** - -- Catch UI regressions -- Test user workflows -- Confidence in deployments - ---- - -## 6. Code Quality Tools - -### Current State - -**Configured:** - -- Prettier (`.prettierrc`) - Basic configuration -- TypeScript (strict mode enabled) - -**Missing:** - -- ESLint -- Husky (git hooks) -- lint-staged -- Commitlint -- Import sorting - -### Recommended Improvements - -#### 6.1 Add ESLint Configuration - -**Effort:** Low | **Priority:** High | **Impact:** Medium - -```javascript -// eslint.config.js -import js from '@eslint/js' -import typescript from '@typescript-eslint/eslint-plugin' -import tsParser from '@typescript-eslint/parser' -import react from 'eslint-plugin-react' -import reactHooks from 'eslint-plugin-react-hooks' -import jsxA11y from 'eslint-plugin-jsx-a11y' -import importPlugin from 'eslint-plugin-import' - -export default [ - js.configs.recommended, - { - files: ['**/*.{ts,tsx}'], - plugins: { - '@typescript-eslint': typescript, - react: react, - 'react-hooks': reactHooks, - 'jsx-a11y': jsxA11y, - import: importPlugin, - }, - languageOptions: { - parser: tsParser, - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - ecmaFeatures: { jsx: true }, - }, - }, - rules: { - // TypeScript - '@typescript-eslint/no-explicit-any': 'error', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - }, - ], - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/consistent-type-imports': 'error', - - // React - 'react/react-in-jsx-scope': 'off', // Not needed in React 19 - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn', - 'react/prop-types': 'off', // Using TypeScript - - // Accessibility - 'jsx-a11y/alt-text': 'error', - 'jsx-a11y/aria-props': 'error', - - // Imports - 'import/order': [ - 'error', - { - groups: [ - 'builtin', - 'external', - 'internal', - 'parent', - 'sibling', - 'index', - ], - 'newlines-between': 'always', - alphabetize: { order: 'asc' }, - }, - ], - 'import/no-duplicates': 'error', - }, - }, -] -``` - -**Add scripts:** - -```json -{ - "scripts": { - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "eslint . --ext .ts,.tsx --fix" - } -} -``` - -**Benefits:** - -- Catch common errors -- Enforce code style -- Improve code quality -- Better team consistency - -#### 6.2 Set Up Git Hooks with Husky - -**Effort:** Low | **Priority:** Medium | **Impact:** High - -```bash -npm install -D husky lint-staged @commitlint/cli @commitlint/config-conventional -npx husky init -``` - -```json -// package.json -{ - "lint-staged": { - "*.{ts,tsx}": ["eslint --fix", "prettier --write", "vitest related --run"], - "*.{json,md,css}": "prettier --write" - } -} -``` - -```javascript -// .husky/pre-commit -npm run lint-staged -``` - -```javascript -// .husky/commit-msg -npx --no -- commitlint --edit $1 -``` - -```javascript -// commitlint.config.js -export default { - extends: ['@commitlint/config-conventional'], - rules: { - 'type-enum': [ - 2, - 'always', - [ - 'feat', - 'fix', - 'docs', - 'style', - 'refactor', - 'test', - 'chore', - 'perf', - 'ci', - 'build', - ], - ], - }, -} -``` - -**Benefits:** - -- Prevent bad commits -- Enforce code quality -- Consistent commit messages -- Automated quality checks - -#### 6.3 Add Import Sorting and Organization - -**Effort:** Low | **Priority:** Low | **Impact:** Low - -```json -// .prettierrc -{ - "trailingComma": "es5", - "tabWidth": 2, - "semi": false, - "singleQuote": true, - "importOrder": ["^react", "^@?\\w", "^~/", "^[./]"], - "importOrderSeparation": true, - "importOrderSortSpecifiers": true, - "plugins": ["@trivago/prettier-plugin-sort-imports"] -} -``` - -**Benefits:** - -- Cleaner imports -- Easier to scan -- Prevent merge conflicts - ---- - -## 7. Dependencies and Package Management - -### Current State - -**Dependencies (16):** - -- React 19.1.1 -- React Router 7.9.2 -- TanStack Query 5.90.2 -- Material-UI 7.3.2 -- Axios 1.12.2 -- TypeScript 5.9.2 - -**DevDependencies (13):** - -- Vite 5.4.20 -- Orval 7.13.0 -- MSW 2.11.3 -- Tailwind CSS 4.1.13 - -**Issues:** - -1. **No lockfile validation** in CI -2. **No dependency update automation** -3. **No security audit automation** -4. **Missing peer dependencies warnings** -5. **No bundle size tracking** - -### Recommended Improvements - -#### 7.1 Add Dependency Update Automation - -**Effort:** Low | **Priority:** Medium | **Impact:** Medium - -**Create Renovate config:** - -```json -// renovate.json -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base"], - "packageRules": [ - { - "matchUpdateTypes": ["minor", "patch"], - "matchCurrentVersion": "!/^0/", - "automerge": true - }, - { - "groupName": "React ecosystem", - "matchPackagePatterns": ["^react", "^@types/react"] - }, - { - "groupName": "MUI ecosystem", - "matchPackagePatterns": ["^@mui/"] - }, - { - "matchDepTypes": ["devDependencies"], - "automerge": true - } - ], - "vulnerabilityAlerts": { - "enabled": true - } -} -``` - -**Benefits:** - -- Keep dependencies updated -- Automatic security patches -- Reduce maintenance burden - -#### 7.2 Add Security Audit Script - -**Effort:** Low | **Priority:** High | **Impact:** Medium - -```json -{ - "scripts": { - "audit": "npm audit --audit-level=moderate", - "audit:fix": "npm audit fix" - } -} -``` - -**Add to CI:** - -```yaml -# .github/workflows/security.yml -name: Security Audit -on: - schedule: - - cron: '0 0 * * 0' # Weekly - pull_request: - -jobs: - audit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - run: npm audit --audit-level=moderate -``` - -**Benefits:** - -- Early security vulnerability detection -- Automated fixes -- Compliance - -#### 7.3 Evaluate Heavy Dependencies - -**Effort:** Medium | **Priority:** Medium | **Impact:** Medium - -**Current bundle (estimated):** - -- node_modules: 662MB -- Largest dependencies: MUI, React Router, TanStack Query - -**Recommendations:** - -1. **Consider lighter alternatives:** - - Material-UI (large) → Consider Radix UI + custom styling - - d3-array → Consider native Array methods where possible - -2. **Use selective imports:** - -```typescript -// Before -import { Box, Container, Stack } from '@mui/material' - -// After (if needed for bundle optimization) -import Box from '@mui/material/Box' -import Container from '@mui/material/Container' -import Stack from '@mui/material/Stack' -``` - -3. **Lazy load heavy features:** - -```typescript -const ChartsPanel = lazy(() => import('./components/ChartsPanel')) -``` - -**Benefits:** - -- Smaller bundle size -- Faster initial load -- Better performance - -#### 7.4 Add Package Size Limit - -**Effort:** Low | **Priority:** Low | **Impact:** Low - -```json -{ - "devDependencies": { - "size-limit": "^11.0.0", - "@size-limit/preset-app": "^11.0.0" - }, - "scripts": { - "size": "size-limit" - } -} -``` - -```javascript -// .size-limit.js -export default [ - { - name: 'Client bundle', - path: 'build/client/**/*.js', - limit: '500 KB', - }, -] -``` - -**Benefits:** - -- Prevent bundle bloat -- Performance budget enforcement - ---- - -## 8. Performance Optimizations - -### Current State - -**Strengths:** - -- React Router 7 with SSR enabled -- TanStack Query for caching (5-minute stale time) -- Vite for fast builds - -**Issues:** - -1. **No code splitting** beyond route-level -2. **No lazy loading** for heavy components -3. **No image optimization** -4. **No memoization** in expensive computations -5. **Fetch waterfalls** in some routes -6. **No prefetching** of likely next pages - -### Recommended Improvements - -#### 8.1 Implement Component Code Splitting - -**Effort:** Medium | **Priority:** High | **Impact:** High - -```typescript -// app/routes/atlas.tsx -import { lazy, Suspense } from 'react' - -const LatentSpacePanel = lazy(() => - import('../features/atlas/components/LatentSpacePanel') -) -const PredictionControls = lazy(() => - import('../features/atlas/components/PredictionControls') -) - -export default function Atlas() { - return ( - - - - }> - {showLatent && } - - - ) -} -``` - -**Benefits:** - -- Smaller initial bundle -- Faster initial load -- Better caching - -#### 8.2 Add React.memo for Expensive Components - -**Effort:** Low | **Priority:** Medium | **Impact:** Medium - -```typescript -// app/features/atlas/components/GridSquareOverlay.tsx -import { memo } from 'react' - -export const GridSquareOverlay = memo(({ - squares, - predictions, - onSquareClick -}: Props) => { - return ( - - {squares.map(square => ( - - ))} - - ) -}, (prev, next) => { - // Custom comparison - return prev.squares === next.squares && - prev.predictions === next.predictions -}) -``` - -**Benefits:** - -- Reduce unnecessary re-renders -- Better performance with large lists -- Smoother interactions - -#### 8.3 Implement Virtual Scrolling for Large Lists - -**Effort:** Medium | **Priority:** Medium | **Impact:** High - -For grid squares or large tables: - -```typescript -import { useVirtualizer } from '@tanstack/react-virtual' - -function GridSquareList({ squares }: Props) { - const parentRef = useRef(null) - - const virtualizer = useVirtualizer({ - count: squares.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 50, - overscan: 10 - }) - - return ( -
-
- {virtualizer.getVirtualItems().map(virtualRow => ( - - ))} -
-
- ) -} -``` - -**Benefits:** - -- Handle thousands of items -- Constant performance regardless of list size -- Better UX - -#### 8.4 Optimize TanStack Query Configuration - -**Effort:** Low | **Priority:** Medium | **Impact:** Medium - -```typescript -// app/root.tsx -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 5, // 5 minutes - gcTime: 1000 * 60 * 10, // 10 minutes (was cacheTime) - retry: 1, - refetchOnWindowFocus: false, // Prevent unnecessary refetches - refetchOnReconnect: true, - }, - mutations: { - retry: 0, - onError: (error) => { - // Global error handling - console.error('Mutation error:', error) - }, - }, - }, -}) -``` - -**Add prefetching:** - -```typescript -// app/routes/home.tsx -import { useQueryClient } from '@tanstack/react-query' - -export default function Home() { - const queryClient = useQueryClient() - const { data: acquisitions } = useGetAcquisitionsAcquisitionsGet() - - const handleRowHover = (acqId: string) => { - // Prefetch grids data - queryClient.prefetchQuery({ - queryKey: getGetAcquisitionGridsQueryKey(acqId), - queryFn: () => getAcquisitionGrids(acqId) - }) - } - - return ( - handleRowHover(acq.uuid)}> - {/* ... */} - - ) -} -``` - -**Benefits:** - -- Better caching strategy -- Faster perceived performance -- Reduced API calls - -#### 8.5 Implement Image Optimization - -**Effort:** Medium | **Priority:** Medium | **Impact:** Medium - -```typescript -// app/components/OptimizedImage.tsx -export function OptimizedImage({ - src, - alt, - width, - height -}: Props) { - return ( - {alt} { - e.currentTarget.src = '/placeholder.png' - }} - /> - ) -} - -// Usage in atlas - -``` - -**Benefits:** - -- Faster page loads -- Better UX -- Reduced bandwidth - ---- - -## 9. Developer Experience Improvements - -### Current State - -**Strengths:** - -- Good README documentation -- Mock API mode for development -- Fast HMR with Vite -- TypeScript for better DX - -**Issues:** - -1. **No component documentation** (Storybook, etc.) -2. **No dev tools extensions** configured -3. **No debugging configuration** -4. **Limited error messages** -5. **No development utilities** - -### Recommended Improvements - -#### 9.1 Add Storybook for Component Documentation - -**Effort:** Medium | **Priority:** Medium | **Impact:** High - -```bash -npx storybook@latest init -``` - -```typescript -// .storybook/main.ts -export default { - stories: ['../app/**/*.stories.@(ts|tsx)'], - addons: [ - '@storybook/addon-essentials', - '@storybook/addon-interactions', - '@storybook/addon-a11y', - ], - framework: '@storybook/react-vite', -} -``` - -**Example story:** - -```typescript -// app/shared/components/QueryStateHandler.stories.tsx -import type { Meta, StoryObj } from '@storybook/react' -import { QueryStateHandler } from './QueryStateHandler' - -const meta = { - title: 'Shared/QueryStateHandler', - component: QueryStateHandler, - parameters: { - layout: 'centered', - }, - tags: ['autodocs'], -} satisfies Meta - -export default meta -type Story = StoryObj - -export const Loading: Story = { - args: { - isLoading: true, - error: null, - data: null, - children: () =>
Data
- } -} - -export const Error: Story = { - args: { - isLoading: false, - error: new Error('Failed to load'), - data: null, - children: () =>
Data
- } -} -``` - -**Benefits:** - -- Component documentation -- Visual testing -- Isolated development -- Better collaboration - -#### 9.2 Add React Query DevTools - -**Effort:** Low | **Priority:** Low | **Impact:** Medium - -```typescript -// app/root.tsx -import { ReactQueryDevtools } from '@tanstack/react-query-devtools' - -export default function App() { - return ( - - - - {import.meta.env.DEV && ( - - )} - - ) -} -``` - -**Benefits:** - -- Debug query state -- Inspect cache -- Better development experience - -#### 9.3 Add VS Code Debug Configuration - -**Effort:** Low | **Priority:** Low | **Impact:** Low - -```json -// .vscode/launch.json -{ - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Launch Chrome against localhost", - "url": "http://localhost:5174", - "webRoot": "${workspaceFolder}" - }, - { - "type": "node", - "request": "launch", - "name": "Vitest: Current File", - "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", - "args": ["--run", "${relativeFile}"], - "smartStep": true, - "console": "integratedTerminal" - } - ] -} -``` - -**Benefits:** - -- Better debugging -- Faster development -- Easier troubleshooting - -#### 9.4 Add Development Utilities - -**Effort:** Low | **Priority:** Low | **Impact:** Medium - -```typescript -// app/utils/dev.ts -export const devLog = { - info: (...args: any[]) => { - if (import.meta.env.DEV) { - console.log('[INFO]', ...args) - } - }, - warn: (...args: any[]) => { - if (import.meta.env.DEV) { - console.warn('[WARN]', ...args) - } - }, - error: (...args: any[]) => { - console.error('[ERROR]', ...args) - }, -} - -export const devAssert = (condition: boolean, message: string) => { - if (import.meta.env.DEV && !condition) { - console.error(`Assertion failed: ${message}`) - } -} -``` - -**Benefits:** - -- Better logging -- Easier debugging -- Development-only code - ---- - -## 10. Modern Web Standards and Patterns - -### Current State - -**Strengths:** - -- Using React 19 (latest) -- ES2022 target -- Modern bundler (Vite) - -**Issues:** - -1. **No Web Vitals tracking** -2. **No PWA support** -3. **No service worker** (except MSW for mocks) -4. **No dark mode implementation** (despite theme setup) -5. **Limited accessibility features** - -### Recommended Improvements - -#### 10.1 Add Web Vitals Monitoring - -**Effort:** Low | **Priority:** Medium | **Impact:** Medium - -```typescript -// app/utils/vitals.ts -import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals' - -export function reportWebVitals(onPerfEntry?: (metric: any) => void) { - if (onPerfEntry && onPerfEntry instanceof Function) { - onCLS(onPerfEntry) - onFID(onPerfEntry) - onLCP(onPerfEntry) - onFCP(onPerfEntry) - onTTFB(onPerfEntry) - } -} - -// app/entry.client.tsx -import { reportWebVitals } from './utils/vitals' - -reportWebVitals((metric) => { - // Send to analytics - console.log(metric) -}) -``` - -**Benefits:** - -- Track real user performance -- Identify performance issues -- Data-driven optimizations - -#### 10.2 Implement Dark Mode Properly - -**Effort:** Medium | **Priority:** Low | **Impact:** Medium - -```typescript -// app/hooks/useTheme.ts -import { useState, useEffect } from 'react' - -export function useThemeMode() { - const [mode, setMode] = useState<'light' | 'dark'>(() => { - if (typeof window === 'undefined') return 'dark' - const stored = localStorage.getItem('theme-mode') - if (stored) return stored as 'light' | 'dark' - return window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light' - }) - - useEffect(() => { - localStorage.setItem('theme-mode', mode) - document.documentElement.classList.toggle('dark', mode === 'dark') - }, [mode]) - - const toggleMode = () => setMode((m) => (m === 'light' ? 'dark' : 'light')) - - return { mode, toggleMode } -} - -// Update theme.tsx -export function useAppTheme() { - const { mode } = useThemeMode() - - return createTheme({ - palette: { - mode, - // ... rest of theme - }, - }) -} -``` - -**Benefits:** - -- Better UX -- Reduced eye strain -- Modern user expectation - -#### 10.3 Add Accessibility Improvements - -**Effort:** Medium | **Priority:** High | **Impact:** High - -**Current issues:** - -- Missing aria labels -- Keyboard navigation not always clear -- Focus management issues - -**Improvements:** - -```typescript -// app/features/atlas/components/GridSquareOverlay.tsx - { - if (e.key === 'Enter' || e.key === ' ') { - handleSelectionClick(gridSquare.uuid) - } - }} - onClick={() => handleSelectionClick(gridSquare.uuid)} -/> -``` - -**Add skip links:** - -```typescript -// app/root.tsx - - - Skip to main content - - {children} - -``` - -**Benefits:** - -- Better accessibility -- Legal compliance -- Wider user reach - -#### 10.4 Add Offline Support - -**Effort:** High | **Priority:** Low | **Impact:** Medium - -```typescript -// vite.config.ts -import { VitePWA } from 'vite-plugin-pwa' - -export default defineConfig({ - plugins: [ - tailwindcss(), - reactRouter(), - tsconfigPaths(), - VitePWA({ - registerType: 'autoUpdate', - workbox: { - globPatterns: ['**/*.{js,css,html,ico,png,svg}'], - runtimeCaching: [ - { - urlPattern: /^https:\/\/api\./, - handler: 'NetworkFirst', - options: { - cacheName: 'api-cache', - expiration: { - maxEntries: 100, - maxAgeSeconds: 60 * 60 * 24, // 1 day - }, - }, - }, - ], - }, - }), - ], -}) -``` - -**Benefits:** - -- Work offline -- Better reliability -- Faster loads on repeat visits - ---- - -## 11. Additional Recommendations - -### 11.1 API Client Improvements - -**Current:** Good setup with Orval - -**Enhancements:** - -1. **Add request/response logging:** - -```typescript -// app/api/mutator.ts -if (import.meta.env.DEV) { - AXIOS_INSTANCE.interceptors.request.use((request) => { - console.log('API Request:', request.method?.toUpperCase(), request.url) - return request - }) -} -``` - -2. **Add retry logic:** - -```typescript -import axiosRetry from 'axios-retry' - -axiosRetry(AXIOS_INSTANCE, { - retries: 3, - retryDelay: axiosRetry.exponentialDelay, - retryCondition: (error) => { - return ( - axiosRetry.isNetworkOrIdempotentRequestError(error) || - error.response?.status === 429 - ) - }, -}) -``` - -3. **Add request cancellation:** - -```typescript -export function useApiQuery(queryKey: QueryKey, queryFn: () => Promise) { - return useQuery({ - queryKey, - queryFn: ({ signal }) => { - // Orval already handles this with CancelToken - return queryFn() - }, - }) -} -``` - -### 11.2 State Management Considerations - -**Current:** TanStack Query for server state only - -**Potential additions:** - -For complex client state, consider: - -- Zustand (lightweight) -- Jotai (atomic state) -- React Context + useReducer (built-in) - -**Not needed yet**, but worth considering if: - -- Sharing state across many components -- Complex state interactions -- Need for state persistence - -### 11.3 Form Management - -**Current:** Manual form handling - -**Recommendation:** Add React Hook Form for complex forms - -```typescript -import { useForm } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' - -const schema = z.object({ - name: z.string().min(1), - description: z.string() -}) - -function CreateModelForm() { - const { register, handleSubmit, formState } = useForm({ - resolver: zodResolver(schema) - }) - - return ( -
- - {formState.errors.name && {formState.errors.name.message}} -
- ) -} -``` - -**Benefits:** - -- Better validation -- Less boilerplate -- Type-safe forms - -### 11.4 Documentation Improvements - -**Add:** - -1. Architecture Decision Records (ADRs) -2. Component usage examples in README -3. API integration guide -4. Contributing guidelines -5. Deployment guide - ---- - -## 12. Priority Implementation Roadmap - -### Phase 1: Critical (Week 1-2) - -**Focus: Testing & Quality** - -1. Set up Vitest + Testing Library -2. Add ESLint configuration -3. Write tests for critical paths (hooks, shared components) -4. Set up git hooks (Husky + lint-staged) -5. Add environment variable validation - -**Effort:** ~40 hours -**Impact:** Prevents bugs, improves code quality - -### Phase 2: High Priority (Week 3-4) - -**Focus: Code Organization & Architecture** - -1. Extract custom hooks from route components -2. Create shared UI component abstractions -3. Implement feature-based folder structure -4. Split large route components -5. Remove React namespace from hooks -6. Fix TypeScript `@ts-ignore` issues - -**Effort:** ~60 hours -**Impact:** Better maintainability, easier development - -### Phase 3: Medium Priority (Week 5-6) - -**Focus: Performance & DX** - -1. Implement code splitting -2. Add React.memo for expensive components -3. Optimize TanStack Query configuration -4. Add Storybook -5. Set up bundle analysis -6. Add React Query DevTools - -**Effort:** ~40 hours -**Impact:** Better performance, better DX - -### Phase 4: Enhancement (Week 7-8) - -**Focus: Polish & Modern Features** - -1. Implement dark mode properly -2. Add accessibility improvements -3. Set up E2E testing with Playwright -4. Add Web Vitals monitoring -5. Improve error boundaries -6. Add dependency update automation - -**Effort:** ~30 hours -**Impact:** Better UX, modern standards - ---- - -## 13. Metrics and Success Criteria - -### Code Quality Metrics - -| Metric | Current | Target | Timeline | -| ---------------------- | -------- | ------ | -------- | -| Test Coverage | 0% | 70% | 4 weeks | -| ESLint Errors | Unknown | 0 | 2 weeks | -| TypeScript `any` usage | 4 | 0 | 2 weeks | -| Bundle Size | ~1.3MB | <800KB | 6 weeks | -| Lighthouse Score | Unknown | >90 | 8 weeks | -| Component Count | 6 | 30+ | 6 weeks | -| Hook Count | 0 custom | 15+ | 4 weeks | - -### Developer Experience Metrics - -| Metric | Current | Target | Timeline | -| ------------------------------ | ------- | ------ | -------- | -| Build Time | ~5s | <3s | 4 weeks | -| HMR Speed | Fast | Faster | 4 weeks | -| Time to First Meaningful Paint | Unknown | <1.5s | 6 weeks | -| Storybook Stories | 0 | 25+ | 6 weeks | - ---- - -## 14. Conclusion - -The SmartEM frontend is a solid React application with good foundations. The most critical gaps are: - -1. **Testing** (0% coverage) - Highest priority -2. **Component organization** - Large, monolithic components -3. **Code quality tooling** - Missing ESLint -4. **Type safety** - Some type suppressions - -The project would benefit most from: - -- Immediate focus on testing infrastructure -- Gradual refactoring to extract hooks and split components -- Adding ESLint and git hooks -- Improving component reusability - -Overall, with the recommended improvements, this codebase can evolve from a good application to an excellent, maintainable, and scalable frontend architecture. - ---- - -## Appendix A: Useful Resources - -- [React Router 7 Documentation](https://reactrouter.com/) -- [TanStack Query Best Practices](https://tanstack.com/query/latest/docs/react/guides/best-practices) -- [Testing Library Best Practices](https://testing-library.com/docs/guiding-principles/) -- [Web Vitals](https://web.dev/vitals/) -- [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/) - ---- - -**Generated:** 2025-10-07 -**Analyzer:** Claude Code -**Next Review:** 2025-11-07 diff --git a/package-lock.json b/package-lock.json index caa89b14..c67d39b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "devDependencies": { "@biomejs/biome": "2.4.8", "lefthook": "^2.1.4", + "msw": "^2.12.14", "prettier": "^3.5.3", "typescript": "^6.0.2" }, @@ -45,56 +46,24 @@ "@types/react": "^19.2.13", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", - "msw": "^2.12.14", "tailwindcss": "^4.0.0", "vite": "^8.0.2", "vite-tsconfig-paths": "^6.1.0" } }, - "apps/legacy/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.7", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", - "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", - "dev": true, - "license": "MIT" - }, - "apps/legacy/node_modules/@vitejs/plugin-react": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", - "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.7" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } - } - }, "apps/smartem": { "name": "@smartem/app", "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", + "@mui/icons-material": "^7.3.9", "@mui/material": "^7.0.2", "@smartem/api": "*", "@smartem/ui": "*", "@tanstack/react-query": "^5.95.2", "@tanstack/react-router": "^1.168.3", "@tanstack/router-devtools": "^1.166.11", + "keycloak-js": "^26.1.0", "react": "^19.2.4", "react-dom": "^19.2.4" }, @@ -106,45 +75,11 @@ "@types/react": "^19.2.13", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", - "msw": "^2.12.14", "tailwindcss": "^4.0.0", "vite": "^8.0.2", "vite-tsconfig-paths": "^6.1.0" } }, - "apps/smartem/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.7", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", - "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", - "dev": true, - "license": "MIT" - }, - "apps/smartem/node_modules/@vitejs/plugin-react": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", - "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.7" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } - } - }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -319,23 +254,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -380,9 +315,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", - "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -787,9 +722,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", "cpu": [ "ppc64" ], @@ -804,9 +739,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", "cpu": [ "arm" ], @@ -821,9 +756,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", "cpu": [ "arm64" ], @@ -838,9 +773,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", "cpu": [ "x64" ], @@ -855,9 +790,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", "cpu": [ "arm64" ], @@ -872,9 +807,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", "cpu": [ "x64" ], @@ -889,9 +824,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", "cpu": [ "arm64" ], @@ -906,9 +841,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", "cpu": [ "x64" ], @@ -923,9 +858,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", "cpu": [ "arm" ], @@ -940,9 +875,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", "cpu": [ "arm64" ], @@ -957,9 +892,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", "cpu": [ "ia32" ], @@ -974,9 +909,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", "cpu": [ "loong64" ], @@ -991,9 +926,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", "cpu": [ "mips64el" ], @@ -1008,9 +943,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", "cpu": [ "ppc64" ], @@ -1025,9 +960,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", "cpu": [ "riscv64" ], @@ -1042,9 +977,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", "cpu": [ "s390x" ], @@ -1059,9 +994,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", "cpu": [ "x64" ], @@ -1076,9 +1011,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", "cpu": [ "arm64" ], @@ -1093,9 +1028,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", "cpu": [ "x64" ], @@ -1110,9 +1045,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", "cpu": [ "arm64" ], @@ -1127,9 +1062,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", "cpu": [ "x64" ], @@ -1144,9 +1079,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", "cpu": [ "arm64" ], @@ -1161,9 +1096,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", "cpu": [ "x64" ], @@ -1178,9 +1113,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", "cpu": [ "arm64" ], @@ -1195,9 +1130,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", "cpu": [ "ia32" ], @@ -1212,9 +1147,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", "cpu": [ "x64" ], @@ -1394,9 +1329,9 @@ } }, "node_modules/@mswjs/interceptors": { - "version": "0.41.2", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.2.tgz", - "integrity": "sha512-7G0Uf0yK3f2bjElBLGHIQzgRgMESczOMyYVasq1XK8P5HaXtlW4eQhz9MBL+TQILZLaruq+ClGId+hH0w4jvWw==", + "version": "0.41.3", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", + "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==", "dev": true, "license": "MIT", "dependencies": { @@ -2239,9 +2174,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.11", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz", - "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==", + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", "dev": true, "license": "MIT" }, @@ -2682,70 +2617,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.8.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.8.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", @@ -2857,6 +2728,33 @@ "react-dom": ">=18.0.0 || >=19.0.0" } }, + "node_modules/@tanstack/react-router-devtools": { + "version": "1.166.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.166.11.tgz", + "integrity": "sha512-WYR3q4Xui5yPT/5PXtQh8i03iUA7q8dONBjWpV3nsGdM8Cs1FxpfhLstW0wZO1dOvSyElscwTRCJ6nO5N8r3Lg==", + "license": "MIT", + "dependencies": { + "@tanstack/router-devtools-core": "1.167.1" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.168.2", + "@tanstack/router-core": "^1.168.2", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + }, + "peerDependenciesMeta": { + "@tanstack/router-core": { + "optional": true + } + } + }, "node_modules/@tanstack/react-store": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.2.tgz", @@ -2948,34 +2846,7 @@ } } }, - "node_modules/@tanstack/router-devtools/node_modules/@tanstack/react-router-devtools": { - "version": "1.166.11", - "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.166.11.tgz", - "integrity": "sha512-WYR3q4Xui5yPT/5PXtQh8i03iUA7q8dONBjWpV3nsGdM8Cs1FxpfhLstW0wZO1dOvSyElscwTRCJ6nO5N8r3Lg==", - "license": "MIT", - "dependencies": { - "@tanstack/router-devtools-core": "1.167.1" - }, - "engines": { - "node": ">=20.19" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/react-router": "^1.168.2", - "@tanstack/router-core": "^1.168.2", - "react": ">=18.0.0 || >=19.0.0", - "react-dom": ">=18.0.0 || >=19.0.0" - }, - "peerDependenciesMeta": { - "@tanstack/router-core": { - "optional": true - } - } - }, - "node_modules/@tanstack/router-devtools/node_modules/@tanstack/router-devtools-core": { + "node_modules/@tanstack/router-devtools-core": { "version": "1.167.1", "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.167.1.tgz", "integrity": "sha512-ECMM47J4KmifUvJguGituSiBpfN8SyCUEoxQks5RY09hpIBfR2eswCv2e6cJimjkKwBQXOVTPkTUk/yRvER+9w==", @@ -3314,10 +3185,36 @@ "dev": true, "license": "MIT" }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -3513,13 +3410,16 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bezier-easing": { @@ -3634,9 +3534,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", "dev": true, "funding": [ { @@ -3823,9 +3723,9 @@ } }, "node_modules/cosmiconfig/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "license": "ISC", "engines": { "node": ">= 6" @@ -4001,9 +3901,9 @@ } }, "node_modules/diff": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -4035,9 +3935,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "version": "1.5.325", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.325.tgz", + "integrity": "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA==", "dev": true, "license": "ISC" }, @@ -4144,9 +4044,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4157,32 +4057,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" } }, "node_modules/escalade": { @@ -4517,9 +4417,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4612,9 +4512,9 @@ "license": "ISC" }, "node_modules/graphql": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "version": "16.13.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", "dev": true, "license": "MIT", "engines": { @@ -4864,9 +4764,9 @@ } }, "node_modules/isbot": { - "version": "5.1.35", - "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.35.tgz", - "integrity": "sha512-waFfC72ZNfwLLuJ2iLaoVaqcNo+CAaLR7xCpAn0Y5WfGzkNHv7ZN39Vbi1y+kb+Zs46XHOX3tZNExroFUPX+Kg==", + "version": "5.1.36", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.36.tgz", + "integrity": "sha512-C/ZtXyJqDPZ7G7JPr06ApWyYoHjYexQbS6hPYD4WYCzpv2Qes6Z+CCEfTX4Owzf+1EJ933PoI2p+B9v7wpGZBQ==", "license": "Unlicense", "engines": { "node": ">=18" @@ -4969,6 +4869,15 @@ "node": ">=0.10.0" } }, + "node_modules/keycloak-js": { + "version": "26.2.3", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.2.3.tgz", + "integrity": "sha512-widjzw/9T6bHRgEp6H/Se3NCCarU7u5CwFKBcwtu7xfA1IfdZb+7Q7/KGusAnBo34Vtls8Oz9vzSqkQvQ7+b4Q==", + "license": "Apache-2.0", + "workspaces": [ + "test" + ] + }, "node_modules/lefthook": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/lefthook/-/lefthook-2.1.4.tgz", @@ -5653,9 +5562,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, @@ -5811,6 +5720,22 @@ } } }, + "node_modules/orval/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/outvariant": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", @@ -5939,9 +5864,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -6276,6 +6201,13 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11" } }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.11", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz", + "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -6317,18 +6249,18 @@ } }, "node_modules/seroval": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.0.tgz", - "integrity": "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.1.tgz", + "integrity": "sha512-OwrZRZAfhHww0WEnKHDY8OM0U/Qs8OTfIDWhUD4BLpNJUfXK4cGmjiagGze086m+mhI+V2nD0gfbHEnJjb9STA==", "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/seroval-plugins": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.0.tgz", - "integrity": "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.1.tgz", + "integrity": "sha512-4FbuZ/TMl02sqv0RTFexu0SP6V+ywaIe5bAWCCEik0fk17BhALgwvUDVF7e3Uvf9pxmwCEJsRPmlkUE6HdzLAw==", "license": "MIT", "engines": { "node": ">=10" @@ -6568,9 +6500,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -6581,22 +6513,22 @@ } }, "node_modules/tldts": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", - "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.23" + "tldts-core": "^7.0.27" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", - "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", "dev": true, "license": "MIT" }, @@ -6614,9 +6546,9 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -6654,9 +6586,9 @@ } }, "node_modules/type-fest": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.4.4.tgz", - "integrity": "sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", "dev": true, "license": "(MIT OR CC0-1.0)", "dependencies": { @@ -6963,6 +6895,22 @@ } } }, + "node_modules/vite-tsconfig-paths/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/vite/node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -7032,9 +6980,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { @@ -7133,7 +7081,6 @@ }, "devDependencies": { "@faker-js/faker": "^10.4.0", - "msw": "^2.12.14", "orval": "^8.6.2", "vite": "^8.0.2" }, diff --git a/package.json b/package.json index bc5dfb66..80a4f51b 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "devDependencies": { "@biomejs/biome": "2.4.8", "lefthook": "^2.1.4", + "msw": "^2.12.14", "prettier": "^3.5.3", "typescript": "^6.0.2" }, diff --git a/packages/api/package.json b/packages/api/package.json index a8e61cd6..123904fd 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -15,7 +15,6 @@ }, "devDependencies": { "@faker-js/faker": "^10.4.0", - "msw": "^2.12.14", "orval": "^8.6.2", "vite": "^8.0.2" }, diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index b4cd7479..779b2038 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -9,7 +9,7 @@ export * as mswHandlers from './generated/default/default.msw' // Generated TypeScript models/types export * from './generated/models' // Axios mutator and helpers -export { ApiError, AXIOS_INSTANCE, apiUrl, customInstance } from './mutator' +export { ApiError, AXIOS_INSTANCE, apiUrl, customInstance, setAuthToken } from './mutator' // Stub implementations for not-yet-implemented endpoints export { getLatentRepPredictionModelPredictionModelNameGridGridUuidLatentRepresentationGet, diff --git a/packages/api/src/mutator.ts b/packages/api/src/mutator.ts index f30bc085..52f5a765 100644 --- a/packages/api/src/mutator.ts +++ b/packages/api/src/mutator.ts @@ -14,6 +14,19 @@ export const AXIOS_INSTANCE = Axios.create({ baseURL: apiUrl(), }) +let authToken: string | null = null + +export const setAuthToken = (token: string | null) => { + authToken = token || null +} + +AXIOS_INSTANCE.interceptors.request.use((config) => { + if (authToken) { + config.headers.Authorization = `Bearer ${authToken}` + } + return config +}) + export class ApiError extends Error { constructor( message: string,