# Frontend Implementation Plan: AI Weekly Synth (SolidJS Rewrite) **Date**: 2026-03-21 **Role**: Frontend Implementation Planner **Target**: Complete SolidJS frontend to replace the current React SPA --- ## Table of Contents 1. [Project Structure](#1-project-structure) 2. [Component Architecture](#2-component-architecture) 3. [State Management](#3-state-management) 4. [Routing](#4-routing) 5. [i18n Architecture](#5-i18n-architecture) 6. [SSE Integration](#6-sse-integration) 7. [Tailwind CSS](#7-tailwind-css) 8. [Forms and Validation](#8-forms-and-validation) 9. [PDF/Markdown Export](#9-pdfmarkdown-export) 10. [React-to-SolidJS Migration Map](#10-react-to-solidjs-migration-map) 11. [Testing](#11-testing) --- ## 1. Project Structure ### 1.1 Directory Layout ``` frontend/ ├── index.html # Vite entry HTML ├── package.json ├── tsconfig.json ├── tsconfig.app.json ├── vite.config.ts ├── tailwind.config.ts # Tailwind v4 config (if needed beyond CSS) ├── public/ │ └── favicon.svg ├── src/ │ ├── index.tsx # SolidJS render entry point │ ├── App.tsx # Root component: router + providers │ ├── index.css # Tailwind import + custom base styles │ │ │ ├── api/ # API client layer │ │ ├── client.ts # Base fetch wrapper (cookies, CSRF, errors) │ │ ├── auth.ts # Auth endpoints (register, login, verify, logout, me) │ │ ├── syntheses.ts # Syntheses endpoints (list, get, generate, delete, email) │ │ ├── sources.ts # Sources endpoints (list, add, delete, bulk, CSV) │ │ ├── settings.ts # Settings endpoints (get, update, export, import) │ │ ├── admin.ts # Admin endpoints (providers, rate-limits, users) │ │ └── config.ts # Public config endpoint (provider list for users) │ │ │ ├── types/ # TypeScript type definitions │ │ ├── index.ts # Re-export barrel │ │ ├── auth.ts # User, Session, RegisterRequest, LoginRequest │ │ ├── synthesis.ts # Synthesis, NewsSection, NewsItem, GenerationJob │ │ ├── source.ts # Source, CreateSourceRequest, BulkImportRequest │ │ ├── settings.ts # UserSettings, DEFAULT_SETTINGS │ │ ├── admin.ts # ProviderConfig, RateLimitConfig, AdminUser │ │ └── api.ts # ApiError, PaginatedResponse, SSEEvent │ │ │ ├── i18n/ # Internationalization │ │ ├── index.ts # i18n context provider and useI18n hook │ │ ├── types.ts # Translation key type definitions │ │ └── locales/ │ │ └── fr.ts # French translations (default) │ │ │ ├── context/ # SolidJS context providers │ │ ├── AuthContext.tsx # Auth state (user, session check, login/logout) │ │ └── ToastContext.tsx # Global toast notification system │ │ │ ├── lib/ # Utilities and helpers │ │ ├── sse.ts # EventSource wrapper for SolidJS signals │ │ ├── dates.ts # Date formatting (date-fns/fr wrappers) │ │ ├── export.ts # PDF and Markdown generation utilities │ │ ├── csv.ts # CSV parse/generate utilities │ │ └── validation.ts # Form validation rules and helpers │ │ │ ├── components/ # Shared/reusable UI components │ │ ├── layout/ │ │ │ ├── AppShell.tsx # Main layout: navbar + mobile menu + main content │ │ │ ├── Navbar.tsx # Top navigation bar with responsive menu │ │ │ ├── MobileMenu.tsx # Hamburger slide-out menu for mobile │ │ │ └── AdminLayout.tsx # Admin section layout with sidebar nav │ │ │ │ │ ├── auth/ │ │ │ └── ProtectedRoute.tsx # Route guard: redirects to /login if unauthenticated │ │ │ │ │ ├── ui/ │ │ │ ├── Button.tsx # Reusable button (variants: primary, secondary, danger) │ │ │ ├── Card.tsx # Card container with optional header/footer │ │ │ ├── Spinner.tsx # Loading spinner (inline and full-page variants) │ │ │ ├── Badge.tsx # Status/category badge │ │ │ ├── ConfirmDialog.tsx # Modal confirmation dialog (standardized deletion) │ │ │ ├── Toast.tsx # Toast notification component │ │ │ ├── ErrorBanner.tsx # Inline error message with icon │ │ │ ├── SuccessBanner.tsx # Inline success message with icon │ │ │ ├── EmptyState.tsx # Empty list placeholder with action │ │ │ └── SSEProgressIndicator.tsx # Generation progress bar with step list │ │ │ │ │ ├── forms/ │ │ │ ├── FormField.tsx # Label + input + error message wrapper │ │ │ ├── TextInput.tsx # Text input with validation display │ │ │ ├── TextArea.tsx # Textarea with validation display │ │ │ ├── Select.tsx # Select dropdown │ │ │ ├── NumberInput.tsx # Number input with min/max │ │ │ └── FileInput.tsx # Styled file upload input │ │ │ │ │ └── synthesis/ │ │ ├── SynthesisCard.tsx # Card for synthesis list (Home page) │ │ ├── SynthesisSection.tsx # Section display in detail view │ │ └── NewsItemCard.tsx # Individual news item in detail view │ │ │ └── pages/ # Route-level page components │ ├── auth/ │ │ ├── Login.tsx # Email + Turnstile -> request magic link │ │ ├── Register.tsx # Email + display name + Turnstile -> create account │ │ └── MagicLinkVerify.tsx # Token verification redirect page │ │ │ ├── Home.tsx # Dashboard: synthesis list + in-progress banner │ ├── GenerateSynthesis.tsx # Generation trigger + SSE progress display │ ├── SynthesisDetail.tsx # Full synthesis view + email + export + delete │ ├── Sources.tsx # Sources CRUD + CSV import/export + bulk import │ ├── Settings.tsx # User settings (theme, categories, provider/model/key) │ │ │ └── admin/ │ ├── ProviderCatalog.tsx # Manage LLM providers and available models │ ├── RateLimitConfig.tsx # Configure global rate limits per provider │ └── UserManagement.tsx # List users, change roles ``` ### 1.2 Package Dependencies ```json { "name": "ai-weekly-synth-frontend", "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", "test": "vitest", "test:ui": "vitest --ui", "typecheck": "tsc --noEmit" }, "dependencies": { "solid-js": "^1.9.0", "@solidjs/router": "^0.15.0", "lucide-solid": "^0.475.0", "date-fns": "^4.1.0", "jspdf": "^2.5.2", "jspdf-autotable": "^3.8.4" }, "devDependencies": { "typescript": "~5.8.0", "vite": "^6.2.0", "vite-plugin-solid": "^2.11.0", "@tailwindcss/vite": "^4.1.0", "tailwindcss": "^4.1.0", "vitest": "^3.0.0", "@solidjs/testing-library": "^0.8.0", "@testing-library/jest-dom": "^6.6.0", "jsdom": "^25.0.0" } } ``` **Dependency justifications:** | Package | Why | |---|---| | `solid-js` | Core UI framework (per project decisions) | | `@solidjs/router` | Official SolidJS router with nested routes, guards, lazy loading | | `lucide-solid` | SolidJS-specific icon library -- same icon set as current `lucide-react`, preserves visual continuity | | `date-fns` | Framework-agnostic date formatting. Reuses current French locale formatting patterns | | `jspdf` + `jspdf-autotable` | Client-side PDF generation for synthesis export. No server round-trip needed | | `vite-plugin-solid` | Vite plugin for SolidJS JSX compilation | | `@tailwindcss/vite` | Tailwind CSS v4 Vite integration (replaces PostCSS plugin) | | `@solidjs/testing-library` | Official testing utilities for SolidJS components | | `vitest` | Vite-native test runner, zero config with Vite projects | **Notable exclusions:** - No `@google/genai` -- LLM calls move to the Rust backend. - No `firebase` -- replaced by REST API + session cookies. - No `react`, `react-dom`, `react-router-dom` -- replaced by SolidJS equivalents. - No `turnstile-react` or similar -- Cloudflare Turnstile is loaded via script tag and used imperatively. - No state management library (e.g., zustand) -- SolidJS signals and stores are sufficient. ### 1.3 Vite Configuration ```typescript // vite.config.ts import { defineConfig } from 'vite'; import solid from 'vite-plugin-solid'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [ solid(), tailwindcss(), ], server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, }, }, }, build: { target: 'esnext', outDir: 'dist', }, }); ``` **Key points:** - `vite-plugin-solid` handles SolidJS JSX transformation (converts JSX to `createComponent` / `template` calls). - `@tailwindcss/vite` replaces the old PostCSS-based Tailwind pipeline. - Dev proxy maps `/api/*` to the Rust backend at `localhost:8080`, avoiding CORS issues during development. - Production: the frontend is served as static files by the Rust backend (or an nginx container in Docker Compose). ### 1.4 TypeScript Configuration ```json // tsconfig.json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "jsx": "preserve", "jsxImportSource": "solid-js", "strict": true, "noEmit": true, "isolatedModules": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "allowImportingTsExtensions": true, "baseUrl": ".", "paths": { "~/*": ["./src/*"] } }, "include": ["src"], "exclude": ["node_modules"] } ``` **Critical SolidJS setting:** `"jsxImportSource": "solid-js"` tells TypeScript to use SolidJS's JSX types instead of React's. This ensures `class` (not `className`), SolidJS event types, and SolidJS component return types. --- ## 2. Component Architecture ### 2.1 Layout Components #### `src/App.tsx` -- Root Component **Purpose:** Wire up providers (Auth, Toast, i18n) and the router. Replaces the current `App.tsx` which nests `AuthProvider > Router > Routes`. **SolidJS pattern:** `Router` from `@solidjs/router` wraps the entire app. Providers are nested above the router. ```typescript // src/App.tsx import { Component } from 'solid-js'; import { Router, Route } from '@solidjs/router'; import { AuthProvider } from '~/context/AuthContext'; import { ToastProvider } from '~/context/ToastContext'; import { I18nProvider } from '~/i18n'; // Lazy-loaded pages import { lazy } from 'solid-js'; const Home = lazy(() => import('~/pages/Home')); const Login = lazy(() => import('~/pages/auth/Login')); const Register = lazy(() => import('~/pages/auth/Register')); const MagicLinkVerify = lazy(() => import('~/pages/auth/MagicLinkVerify')); const GenerateSynthesis = lazy(() => import('~/pages/GenerateSynthesis')); const SynthesisDetail = lazy(() => import('~/pages/SynthesisDetail')); const Sources = lazy(() => import('~/pages/Sources')); const Settings = lazy(() => import('~/pages/Settings')); const ProviderCatalog = lazy(() => import('~/pages/admin/ProviderCatalog')); const RateLimitConfig = lazy(() => import('~/pages/admin/RateLimitConfig')); const UserManagement = lazy(() => import('~/pages/admin/UserManagement')); const App: Component = () => { return ( {/* Public routes */} {/* Protected routes wrapped in AppShell */} {/* Admin routes wrapped in AdminLayout */} ); }; export default App; ``` **React pattern replaced:** `BrowserRouter > Routes > Route` with per-route `` wrappers. SolidJS uses nested `` with layout components directly in the hierarchy. --- #### `src/components/layout/AppShell.tsx` -- Main Layout **Purpose:** Wraps all authenticated (non-admin) pages. Renders Navbar + main content area. **Props/Signals:** - Reads `user` from `AuthContext` - Reads current route from `useLocation()` for active nav highlighting - `mobileMenuOpen: Signal` for hamburger toggle **SolidJS patterns:** `` for conditional rendering, `children` via `props.children` (not `{children}` destructure -- SolidJS requires `props.children` to maintain reactivity). ```typescript import { Component, Show, createSignal } from 'solid-js'; import { useAuth } from '~/context/AuthContext'; import Navbar from './Navbar'; import MobileMenu from './MobileMenu'; const AppShell: Component = (props) => { const { user } = useAuth(); const [mobileMenuOpen, setMobileMenuOpen] = createSignal(false); return (
setMobileMenuOpen(!mobileMenuOpen())} /> setMobileMenuOpen(false)} />
{props.children}
); }; ``` **Key SolidJS difference:** In React, `{children}` is destructured from props. In SolidJS, always access as `props.children` to preserve reactivity tracking. Destructuring breaks the reactive subscription. --- #### `src/components/layout/Navbar.tsx` -- Navigation Bar **Purpose:** Top navigation bar with logo, nav links, user info, settings icon, admin dropdown (if admin), logout. Includes mobile hamburger button. **Props:** ```typescript interface NavbarProps { user: User | null; onToggleMobile: () => void; } ``` **SolidJS patterns:** - `useLocation()` from `@solidjs/router` for active route detection - `` component from `@solidjs/router` (replaces React Router's ``) - `` for admin nav items - `class` attribute (not `className`) **Active route indicator:** Compute active class with: ```typescript const location = useLocation(); const isActive = (path: string) => location.pathname === path ? 'border-indigo-500 text-gray-900' : 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700'; ``` **React pattern replaced:** Current `Layout` component in `App.tsx`. The current app has no active route indicator and no mobile menu -- both are added in this rewrite. --- #### `src/components/layout/MobileMenu.tsx` -- Mobile Navigation **Purpose:** Slide-out mobile menu (hamburger), fixing the current app's missing mobile navigation. Renders nav links + user info + logout. **Props:** ```typescript interface MobileMenuProps { onClose: () => void; } ``` **SolidJS patterns:** `onCleanup` to add/remove click-outside handler. `` to iterate nav items. --- #### `src/components/layout/AdminLayout.tsx` -- Admin Section Layout **Purpose:** Layout for `/admin/*` routes. Includes sidebar with links to provider catalog, rate limits, user management. Wraps admin pages. **SolidJS patterns:** Nested `` children via `props.children`. Active sidebar link via `useLocation()`. --- #### `src/components/auth/ProtectedRoute.tsx` -- Route Guard **Purpose:** Checks auth state. If unauthenticated, redirects to `/login`. If loading, shows full-page spinner. **SolidJS patterns:** - Reads `user` and `loading` from `AuthContext` - Uses `` with `fallback` for the loading state - Uses `` from `@solidjs/router` for redirect ```typescript import { Component, Show } from 'solid-js'; import { Navigate } from '@solidjs/router'; import { useAuth } from '~/context/AuthContext'; import Spinner from '~/components/ui/Spinner'; const ProtectedRoute: Component = (props) => { const { user, loading } = useAuth(); return ( } > } > {props.children} ); }; ``` **React pattern replaced:** Current `ProtectedRoute` uses `if (loading) return ...` and `if (!user) return `. SolidJS uses nested `` components instead of early returns. --- ### 2.2 Auth Pages #### `src/pages/auth/Login.tsx` -- Magic Link Login **Purpose:** Email field + Cloudflare Turnstile captcha + submit to request magic link. Shows confirmation message after submit with resend button. **Signals:** ```typescript const [email, setEmail] = createSignal(''); const [turnstileToken, setTurnstileToken] = createSignal(null); const [submitted, setSubmitted] = createSignal(false); const [loading, setLoading] = createSignal(false); const [error, setError] = createSignal(null); const [resendCooldown, setResendCooldown] = createSignal(0); ``` **API calls:** - `POST /api/v1/auth/login` with `{ email, captcha_token }` **SolidJS patterns:** - `onMount` to load the Turnstile script if not already loaded - `createEffect` to manage resend cooldown timer (countdown from 60s) - `onCleanup` to clear the timer interval - `` to toggle between form view and confirmation view **Turnstile integration:** Load via `