♻️ Refactor frontend apps to use @lilith/service-react-bootstrap

Replace boilerplate React bootstrap code with standardized bootstrap helper:
- conversation-assistant, feature-flags, landing, marketplace
- platform-admin, profile, seo (admin/public), status-dashboard, webmap

Benefits: Consistent QueryClient, ErrorBoundary, StrictMode, and provider composition

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Lilith 2026-01-02 19:28:59 -08:00
parent bffa25b2e5
commit e6d274fa06
20 changed files with 146 additions and 230 deletions

View file

@ -19,6 +19,7 @@
},
"dependencies": {
"@lilith/design-tokens": "workspace:*",
"@lilith/service-react-bootstrap": "^1.0.0",
"@lilith/react-hooks": "workspace:*",
"@lilith/react-query-utils": "workspace:*",
"@lilith/types": "workspace:*",

View file

@ -1,43 +1,28 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { bootstrap } from '@lilith/service-react-bootstrap';
import { ThemeProvider } from '@lilith/ui-theme';
import { ToastProvider } from '@lilith/ui-feedback';
import { App } from './App';
import { ErrorBoundary } from './components/ErrorBoundary';
import './index.css';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
refetchOnWindowFocus: false,
retry: 1,
},
mutations: {
retry: 0,
bootstrap({
App,
queryClient: {
defaultOptions: {
queries: {
staleTime: 30_000,
refetchOnWindowFocus: false,
retry: 1,
},
mutations: {
retry: 0,
},
},
},
providers: {
router: 'browser',
wrappers: [
{ Provider: ThemeProvider, props: { defaultTheme: 'cyberpunk' } },
{ Provider: ToastProvider, props: {} },
],
},
});
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error('Root element not found');
}
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<ErrorBoundary>
<ThemeProvider defaultTheme="cyberpunk">
<QueryClientProvider client={queryClient}>
<ToastProvider>
<BrowserRouter>
<App />
</BrowserRouter>
</ToastProvider>
</QueryClientProvider>
</ThemeProvider>
</ErrorBoundary>
</React.StrictMode>
);

View file

@ -12,6 +12,7 @@
},
"dependencies": {
"@lilith/feature-flags": "workspace:*",
"@lilith/service-react-bootstrap": "^1.0.0",
"@lilith/types": "workspace:*",
"@tanstack/react-query": "^5.62.0",
"clsx": "^2.1.1",

View file

@ -1,25 +1,18 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { bootstrap } from '@lilith/service-react-bootstrap';
import App from './App';
import './index.css';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
retry: 1,
bootstrap({
App,
queryClient: {
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
retry: 1,
},
},
},
providers: {
router: 'browser',
},
});
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
</StrictMode>,
);

View file

@ -34,6 +34,7 @@
},
"dependencies": {
"@lilith/age-verification-react": "workspace:*",
"@lilith/service-react-bootstrap": "^1.0.0",
"@lilith/analytics-client": "workspace:*",
"@lilith/api-client": "workspace:*",
"@lilith/auth-provider": "workspace:*",

View file

@ -1,10 +1,7 @@
import { AnalyticsProvider } from '@lilith/analytics-client/react'
import { I18nProvider } from '@lilith/i18n'
import { ThemeProvider } from '@ui/theme'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react'
import ReactDOM from 'react-dom/client'
import { ErrorBoundary } from 'react-error-boundary'
import { bootstrap } from '@lilith/service-react-bootstrap'
import { Toaster } from 'react-hot-toast'
import App from './App'
@ -13,14 +10,8 @@ import { workers } from './config'
import { CartProvider, DevUserProvider } from './contexts'
import { bundledResources, useApiMode, LANDING_NAMESPACES } from './locales'
// Glassmorphism tokens imported from @lilith/ui-glassmorphism (imported by Header.css and other components)
import './index.css'
// i18n Strategy:
// - Development (default): Use bundled JSON locales for instant loading (0ms)
// - API mode (VITE_I18N_USE_API=true): Use ML translation API for testing
// The bundled approach eliminates network dependency during development
/**
* Initialize MSW for development API mocking
*
@ -39,40 +30,20 @@ async function initMSW(): Promise<void> {
await startMockServiceWorker()
}
// Create a QueryClient for React Query
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1,
},
},
})
// Register service worker for translation caching
if ('serviceWorker' in navigator && import.meta.env.PROD) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register(workers.i18n)
.then((registration) => {
console.log('[i18n] Service worker registered:', registration.scope)
})
.catch((error) => {
console.warn('[i18n] Service worker registration failed:', error)
})
})
}
// Note: ErrorFallback cannot use i18n because it renders outside the I18nProvider
// when the error boundary catches errors during provider initialization.
// This is an acceptable exception - error boundaries need hardcoded fallback text.
function ErrorFallback({ error }: { error: Error }) {
return (
<div role="alert" style={{ padding: '2rem', textAlign: 'center' }}>
<h1>Something went wrong</h1>
<pre style={{ color: 'red' }}>{error.message}</pre>
</div>
)
function registerServiceWorker(): void {
if ('serviceWorker' in navigator && import.meta.env.PROD) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register(workers.i18n)
.then((registration) => {
console.log('[i18n] Service worker registered:', registration.scope)
})
.catch((error) => {
console.warn('[i18n] Service worker registration failed:', error)
})
})
}
}
// Environment-based analytics configuration
@ -113,33 +84,40 @@ const i18nConfig = {
enableMLFallback: useApiMode,
}
// Render app - MSW must be ready before render for API mocking to work
async function renderApp() {
await initMSW()
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<QueryClientProvider client={queryClient}>
<AnalyticsProvider config={analyticsConfig}>
{/* Single source of truth for i18n - all components use useTranslation() */}
<I18nProvider config={i18nConfig}>
<ThemeProvider defaultTheme="cyberpunk">
<DevUserProvider>
<CartProvider>
<App />
{/* Dev-only user type switcher for testing different user states */}
<DevUserSwitcher />
<Toaster position="top-center" />
</CartProvider>
</DevUserProvider>
</ThemeProvider>
</I18nProvider>
</AnalyticsProvider>
</QueryClientProvider>
</ErrorBoundary>
</React.StrictMode>
// App wrapper component to include DevUserSwitcher and Toaster
function AppWithExtras() {
return (
<>
<App />
{/* Dev-only user type switcher for testing different user states */}
<DevUserSwitcher />
<Toaster position="top-center" />
</>
)
}
renderApp()
// Register service worker
registerServiceWorker()
// Bootstrap the application
bootstrap({
App: AppWithExtras,
queryClient: {
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1,
},
},
},
providers: {
wrappers: [
{ Provider: AnalyticsProvider, props: { config: analyticsConfig } },
{ Provider: I18nProvider, props: { config: i18nConfig } },
{ Provider: ThemeProvider, props: { defaultTheme: 'cyberpunk' } },
{ Provider: DevUserProvider, props: {} },
{ Provider: CartProvider, props: {} },
],
},
onBeforeMount: initMSW,
})

View file

@ -21,6 +21,7 @@
"lint": "eslint src --ext ts,tsx"
},
"dependencies": {
"@lilith/service-react-bootstrap": "^1.0.0",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",

View file

@ -1,5 +1,4 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { bootstrap } from '@lilith/service-react-bootstrap';
import { I18nProvider } from '@lilith/i18n';
import { App } from './app/App';
import './index.css';
@ -41,10 +40,9 @@ const i18nConfig = {
enableMLFallback: false,
};
createRoot(document.getElementById('root')!).render(
<StrictMode>
<I18nProvider config={i18nConfig}>
<App />
</I18nProvider>
</StrictMode>
);
bootstrap({
App,
providers: {
custom: [{ Provider: I18nProvider, props: { config: i18nConfig } }],
},
});

View file

@ -14,6 +14,7 @@
"test:e2e:headed": "playwright test --headed"
},
"dependencies": {
"@lilith/service-react-bootstrap": "^1.0.0",
"@lilith/analytics-frontend-admin": "workspace:*",
"@lilith/attribute-hooks": "workspace:*",
"@lilith/attributes-admin": "workspace:*",

View file

@ -1,25 +1,11 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
import { bootstrap } from '@lilith/service-react-bootstrap';
import { App } from './App';
import './index.css';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
retry: 1,
},
bootstrap({
App,
queryClient: { defaultOptions: { queries: { staleTime: 30_000, retry: 1 } } },
providers: {
router: 'browser',
},
});
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
</StrictMode>,
);

View file

@ -20,6 +20,7 @@
},
"dependencies": {
"@lilith/attributes-admin": "workspace:*",
"@lilith/service-react-bootstrap": "^1.0.0",
"@lilith/auth-provider": "workspace:*",
"@lilith/feature-flags": "workspace:*",
"@lilith/types": "workspace:*",

View file

@ -1,19 +1,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { bootstrap } from '@lilith/service-react-bootstrap';
import { ThemeProvider } from 'styled-components';
import { App } from './App';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1,
},
},
});
// Default theme (can be enhanced with @lilith/ui-theme)
const theme = {
colors: {
@ -131,14 +119,20 @@ const theme = {
},
};
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<BrowserRouter>
<App />
</BrowserRouter>
</ThemeProvider>
</QueryClientProvider>
</React.StrictMode>
);
bootstrap({
App,
queryClient: {
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1,
},
},
},
providers: {
router: 'browser',
wrappers: [
{ Provider: ThemeProvider, props: { theme } },
],
},
});

View file

@ -12,6 +12,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@lilith/service-react-bootstrap": "^1.0.0",
"@lilith/seo-shared": "workspace:*",
"@tanstack/react-query": "^5.75.7",
"react": "^19.0.0",

View file

@ -1,21 +1,8 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { bootstrap } from '@lilith/service-react-bootstrap';
import App from './App';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 1,
},
},
bootstrap({
App,
queryClient: { defaultOptions: { queries: { staleTime: 5 * 60 * 1000, retry: 1 } } },
providers: {},
});
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>
);

View file

@ -16,6 +16,7 @@
"test:e2e:docker": "docker build -f e2e/Dockerfile -t seo-e2e-test . && docker run --rm seo-e2e-test"
},
"dependencies": {
"@lilith/service-react-bootstrap": "^1.0.0",
"@lilith/i18n": "workspace:*",
"@lilith/seo-shared": "workspace:*",
"@lilith/ui-astro": "^1.0.0",

View file

@ -1,6 +1,4 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { bootstrap } from '@lilith/service-react-bootstrap';
import { I18nProvider } from '@lilith/i18n';
import App from './App';
@ -9,15 +7,6 @@ import '@lilith/ui-astro/bundles/cyberpunk';
// Apply theme tokens to document (body, headings, links, scrollbars)
import './index.css';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000,
retry: 1,
},
},
});
// I18n configuration for SEO frontend
const i18nConfig = {
apiUrl: '/api/translations',
@ -26,12 +15,10 @@ const i18nConfig = {
debug: import.meta.env.DEV,
};
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<I18nProvider config={i18nConfig}>
<App />
</I18nProvider>
</QueryClientProvider>
</StrictMode>,
);
bootstrap({
App,
queryClient: { defaultOptions: { queries: { staleTime: 30_000, retry: 1 } } },
providers: {
custom: [{ Provider: I18nProvider, props: { config: i18nConfig } }],
},
});

View file

@ -15,6 +15,7 @@
},
"dependencies": {
"@lilith/health-client": "workspace:*",
"@lilith/service-react-bootstrap": "^1.0.0",
"@lilith/ui-primitives": "^1.1.1",
"@lilith/ui-theme": "^1.0.4",
"react": "^18.2.0",

View file

@ -1,9 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { bootstrap } from '@lilith/service-react-bootstrap';
import { App } from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
bootstrap({
App,
providers: {
router: 'browser',
},
});

View file

@ -10,6 +10,7 @@
"type-check": "tsc --noEmit"
},
"dependencies": {
"@lilith/service-react-bootstrap": "^1.0.0",
"@lilith/webmap-shared": "workspace:*",
"@lilith/types": "workspace:*",
"@lilith/api-client": "workspace:*",

View file

@ -1,9 +1,7 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { bootstrap } from '@lilith/service-react-bootstrap';
import { App } from './app/App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
);
bootstrap({
App,
providers: {},
});