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

Project Structure

frontend/
├── App.tsx                    # Root component with providers
├── index.html                 # HTML entry point
├── types.ts                   # Frontend UI types
├── shell/                     # App orchestration layer
│   ├── Dashboard.tsx          # Main editor (~1400 lines)
│   ├── DashboardContext.tsx   # Dashboard state
│   ├── 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, React Query setup
│   ├── config/                # Constants, configuration
│   ├── context/               # ThemeContext
│   ├── hooks/                 # Shared React hooks
│   ├── lib/                   # SSE client, logger, storage
│   ├── 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

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

← Prompts State Management →

See also: Mobile Interface for SwipeReview and FocusMode documentation.