Skip to content

Frontend Architecture

The Sapari frontend is a React application built with TypeScript and Vite. It uses a feature-based architecture where code is organized by domain feature rather than technical type.

Tech Stack

Technology Purpose
React 19 UI framework
TypeScript Type safety
Vite Build tool and dev server
React Query Server state management
EventSource SSE for real-time events
Tailwind CSS Styling
i18next UI localization (en/pt/es, browser-language detection, server-wins reconcile from User.locale)

Project Structure

frontend/
├── App.tsx                    # Root component with providers
├── index.html                 # HTML entry point
├── types.ts                   # Frontend UI types
├── shell/                     # App orchestration layer (top-level <Routes>)
│   ├── Dashboard.tsx          # Main editor (~3000 lines, currentProject + currentView URL-derived)
│   ├── Sidebar.tsx            # Navigation sidebar
│   └── index.ts
├── features/                  # Self-contained feature modules
│   ├── analysis/              # Timeline editor, edit navigation
│   ├── assets/                # Asset library, groups, editor
│   ├── auth/                  # Login, auth context
│   ├── captions/              # Caption editor, overlay
│   ├── clips/                 # Clip CRUD, playback info
│   ├── drafts/                # Draft save/load
│   ├── edits/                 # Edit CRUD, undo/redo, cut/mute actions
│   ├── exports/               # Export panel, settings
│   ├── mobile/                # Mobile UI (SwipeReview, FocusMode)
│   ├── projects/              # Project CRUD, SSE events
│   ├── settings/              # Analysis settings, audio censorship, presets
│   ├── upload/                # File upload, YouTube import
│   └── video-player/          # Video preview, timeline hooks, audio muting
├── shared/                    # Cross-cutting concerns
│   ├── api/                   # API client (Accept-Language stamping), React Query setup
│   ├── config/                # Constants, configuration
│   ├── context/               # ThemeContext, LocaleContext
│   ├── hooks/                 # Shared React hooks
│   ├── lib/                   # SSE client, logger, storage, i18n (i18next init + reconcileServerLocale)
│   ├── types/                 # Shared API response types
│   └── ui/                    # UI components, icons
└── vite.config.ts

Feature Module Structure

Each feature follows a consistent structure:

features/projects/
├── api.ts                     # API client functions
├── hooks.ts                   # React Query hooks
├── components/                # Feature-specific components
│   └── ProjectList.tsx
├── context/                   # Feature context (if needed)
└── index.ts                   # Barrel exports

All exports go through index.ts:

// features/projects/index.ts
export { projectApi, projectKeys } from './api';
export { useProjects, useProject, useCreateProject } from './hooks';
export { ProjectList } from './components/ProjectList';

Data Flow

The frontend follows a unidirectional data flow pattern:

Component → Hook → API Service → Backend
         React Query Cache
           SSE Events
  1. Components call hooks (e.g., useProjects())
  2. Hooks use React Query to fetch/mutate data via API services
  3. React Query manages the cache
  4. SSE events trigger cache invalidation for real-time updates

Internationalization

The SPA is wired for English, Portuguese, and Spanish. UI strings flow through i18next (initialized in shared/lib/i18n.ts) with the i18next-browser-languagedetector plugin reading localStorage['sapari-locale'] and the browser's navigator.language in that order. LocaleProvider (shared/context/LocaleContext.tsx) is mounted high in the provider tree under ThemeProvider; it owns the current locale, persists changes to localStorage, mirrors them onto <html lang>, and stamps the value onto ApiClient.currentLocale so every outbound request carries a matching Accept-Language header. The header is the source of truth the backend reads via its get_request_locale dependency, including at signup (before any User.locale exists).

Server-wins reconciliation runs on every successful /auth/check-auth and /auth/login: AuthProvider passes the response's user.locale to reconcileServerLocale() in shared/lib/i18n.ts, which calls i18next.changeLanguage(...) only when the persisted server value differs from the SPA's resolved base language. The catalogs themselves are not yet populated — string extraction happens in later i18n phases — so the round-trip is plumbed end-to-end before the translations land.

Concern Location
i18next init + reconcileServerLocale helper frontend/shared/lib/i18n.ts
Locale provider / persistence / html-lang sync frontend/shared/context/LocaleContext.tsx
Accept-Language stamping per request frontend/shared/api/client.ts (setLocale)
Server-wins call sites frontend/features/auth/context.tsx (checkAuth + login)
Inline locale bootstrap (<html lang> before SPA mount) frontend/index.html

The inline locale bootstrap script in index.html and SUPPORTED_LOCALES / LOCALE_STORAGE_KEY in shared/lib/i18n.ts must stay in lockstep — see Frontend Development → Inline locale-bootstrap script.

API Communication

Each feature has its own api.ts with typed functions:

// features/projects/api.ts
export const projectApi = {
  list: (page = 1) => api.get<PaginatedResponse<ProjectRead>>(`/projects/?page=${page}`),
  get: (uuid: string) => api.get<ProjectRead>(`/projects/${uuid}`),
  create: (data: ProjectCreate) => api.post<ProjectRead>('/projects/', data),
  analyze: (uuid: string, settings: AnalysisSettings) => api.post(`/projects/${uuid}/analyze`, settings),
};

// Query keys for cache management
export const projectKeys = {
  all: ['projects'] as const,
  lists: () => [...projectKeys.all, 'list'] as const,
  detail: (uuid: string) => [...projectKeys.all, 'detail', uuid] as const,
};

The Vite dev server proxies /api requests to the backend, so we use relative URLs.

Hooks Pattern

Each feature has a hooks.ts file with React Query hooks:

// features/projects/hooks.ts
export function useProjects() {
  return useQuery({
    queryKey: projectKeys.lists(),
    queryFn: async () => {
      const response = await projectApi.list();
      return response.data.map(toFrontendProject);
    },
  });
}

export function useCreateProject() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (name: string) => projectApi.create({ name }),
    onSuccess: (newProject) => {
      queryClient.setQueryData(projectKeys.lists(), (old) => [newProject, ...old]);
    },
  });
}

Path Aliases

TypeScript path aliases simplify imports:

// tsconfig.json paths
{
  "@/*": ["./*"],
  "@/features/*": ["./features/*"],
  "@/shared/*": ["./shared/*"],
  "@/shell/*": ["./shell/*"],
  "@/types": ["./types.ts"]
}

// Usage
import { useProjects } from '@/features/projects';
import { Button } from '@/shared/ui';
import { Dashboard } from '@/shell';
import type { Project } from '@/types';

Development

Run the frontend dev server:

cd frontend
npm install
npm run dev

This starts Vite on port 3000 with hot reload. API requests are proxied to http://localhost:8000.

Key Files

Component Location
Entry point frontend/index.html
App component frontend/App.tsx
Main dashboard frontend/shell/Dashboard.tsx
Type definitions frontend/types.ts
Vite config frontend/vite.config.ts
API client frontend/shared/api/client.ts
SSE client frontend/shared/lib/events.ts
i18next init frontend/shared/lib/i18n.ts
Locale provider frontend/shared/context/LocaleContext.tsx

← Prompts State Management →

See also: Mobile Interface for SwipeReview and FocusMode documentation.