Frontend Architecture
The Financial Data Extractor frontend is built with Next.js 15, React 19, and TypeScript, providing a modern, type-safe, and performant user interface for viewing and managing financial data extraction.
Overview
The frontend serves as the primary interface for users to:
- Browse Companies: View all companies and navigate to financial statements
- View Financial Statements: Display 10-year compiled views of Income Statements, Balance Sheets, and Cash Flow Statements
- Monitor Documents: Browse scraped and classified PDF documents by company and fiscal year
- Trigger Extraction: Start and monitor Celery tasks for data extraction
- Real-time Updates: Monitor task progress with live status polling
Technology Stack
Core Framework
- Next.js 15 - React framework with App Router for Server Components and improved performance
- React 19 - Latest React features with improved hydration and concurrent rendering
- TypeScript - Strict type safety for better developer experience and fewer runtime errors
Styling & UI
- Tailwind CSS 4.0 - CSS-first configuration with
@import "tailwindcss"and@themedirective - shadcn/ui - Accessible component library built on Radix UI primitives
- Lucide React - Modern icon library
Data & API
- React Query (TanStack Query) - Data fetching, caching, and synchronization with automatic background updates
- Axios - HTTP client with interceptors for error handling
- React Query DevTools - Development tools for debugging queries and mutations
Architecture Diagram
graph TB
subgraph "Next.js App Router"
A[Root Layout]
B[Company Dashboard]
C[Statement Pages]
D[Extraction Page]
A --> B
A --> C
A --> D
end
subgraph "Component Layer"
E[Dashboard Components]
F[Statement Components]
G[Document Components]
H[Extraction Components]
I[UI Components - shadcn/ui]
B --> E
C --> F
C --> G
D --> H
E --> I
F --> I
G --> I
H --> I
end
subgraph "Data Fetching Layer"
J[React Query Provider]
K[Query Hooks]
L[Companies Hooks]
M[Statements Hooks]
N[Documents Hooks]
O[Tasks Hooks]
J --> K
K --> L
K --> M
K --> N
K --> O
end
subgraph "API Client Layer"
P[API Client - Axios]
Q[Companies API]
R[Statements API]
S[Documents API]
T[Tasks API]
P --> Q
P --> R
P --> S
P --> T
end
subgraph "Backend API"
U[FastAPI Backend<br/>:3030]
end
subgraph "Data Layer"
V[TypeScript Types]
W[Utility Functions]
end
E --> L
F --> M
G --> N
H --> O
L --> Q
M --> R
N --> S
O --> T
Q --> U
R --> U
S --> U
T --> U
Q --> V
R --> V
S --> V
T --> V
E --> W
F --> W
classDef nextjs fill:#000000,stroke:#fff,stroke-width:2px,color:#fff
classDef component fill:#61dafb,stroke:#20232a,stroke-width:2px
classDef datafetching fill:#ff6b6b,stroke:#fff,stroke-width:2px,color:#fff
classDef api fill:#0070f3,stroke:#fff,stroke-width:2px,color:#fff
classDef backend fill:#009688,stroke:#fff,stroke-width:2px,color:#fff
classDef data fill:#f7df1e,stroke:#323330,stroke-width:2px
class A,B,C,D nextjs
class E,F,G,H,I component
class J,K,L,M,N,O datafetching
class P,Q,R,S,T api
class U backend
class V,W data
Component Hierarchy
The following diagram shows the component tree structure and how components are organized and nested:
graph TD
Root[Root Layout<br/>layout.tsx] --> Navbar[Navbar<br/>layout/navbar.tsx]
Root --> Main[Main Content Area]
Main --> Dashboard[Home Page<br/>page.tsx]
Main --> Companies[Companies Detail<br/>companies/[id]/statements/[type]/page.tsx]
Main --> Documents[Documents Page<br/>Integrated into Statement Pages]
Main --> Extraction[Extraction Page<br/>extraction/page.tsx]
Dashboard --> CompanyList[CompanyList<br/>dashboard/company-list.tsx]
CompanyList --> CompanyCard[CompanyCard<br/>Individual Company Cards]
CompanyCard --> UI1[UI Components<br/>shadcn/ui]
Companies --> StatementView[StatementView<br/>statements/statement-view.tsx]
StatementView --> StatementNav[StatementTypeNav<br/>statements/statement-type-nav.tsx]
StatementView --> StatementTable[FinancialStatementTable<br/>statements/financial-statement-table.tsx]
StatementView --> DocumentList[DocumentList<br/>documents/document-list.tsx]
StatementView --> UI2[UI Components<br/>shadcn/ui]
StatementNav --> UI3[UI Components<br/>shadcn/ui]
StatementTable --> UI4[UI Components<br/>shadcn/ui]
DocumentList --> UI5[UI Components<br/>shadcn/ui]
Extraction --> ExtractionControls[ExtractionControls<br/>extraction/extraction-controls.tsx]
Extraction --> ExtractionContent[ExtractionPageContent<br/>extraction/extraction-page-content.tsx]
Extraction --> TaskMonitor[TaskStatusMonitor<br/>extraction/task-status-monitor.tsx]
ExtractionControls --> UI6[UI Components<br/>shadcn/ui]
ExtractionContent --> UI7[UI Components<br/>shadcn/ui]
TaskMonitor --> UI8[UI Components<br/>shadcn/ui]
CompanyList -.->|Uses| ReactQuery1[React Query Hooks<br/>Companies API]
StatementView -.->|Uses| ReactQuery2[React Query Hooks<br/>Statements API]
DocumentList -.->|Uses| ReactQuery3[React Query Hooks<br/>Documents API]
TaskMonitor -.->|Uses| ReactQuery4[React Query Hooks<br/>Tasks API]
style Root fill:#000000,stroke:#fff,stroke-width:2px,color:#fff
style Navbar fill:#61dafb,stroke:#20232a,stroke-width:2px
style Dashboard fill:#000000,stroke:#fff,stroke-width:2px,color:#fff
style Companies fill:#000000,stroke:#fff,stroke-width:2px,color:#fff
style Extraction fill:#000000,stroke:#fff,stroke-width:2px,color:#fff
style CompanyList fill:#61dafb,stroke:#20232a,stroke-width:2px
style StatementView fill:#61dafb,stroke:#20232a,stroke-width:2px
style DocumentList fill:#61dafb,stroke:#20232a,stroke-width:2px
style ExtractionControls fill:#61dafb,stroke:#20232a,stroke-width:2px
style TaskMonitor fill:#61dafb,stroke:#20232a,stroke-width:2px
style UI1 fill:#f7df1e,stroke:#323330,stroke-width:2px
style UI2 fill:#f7df1e,stroke:#323330,stroke-width:2px
style UI3 fill:#f7df1e,stroke:#323330,stroke-width:2px
style UI4 fill:#f7df1e,stroke:#323330,stroke-width:2px
style UI5 fill:#f7df1e,stroke:#323330,stroke-width:2px
style UI6 fill:#f7df1e,stroke:#323330,stroke-width:2px
style UI7 fill:#f7df1e,stroke:#323330,stroke-width:2px
style UI8 fill:#f7df1e,stroke:#323330,stroke-width:2px
style ReactQuery1 fill:#ff6b6b,stroke:#fff,stroke-width:2px,color:#fff
style ReactQuery2 fill:#ff6b6b,stroke:#fff,stroke-width:2px,color:#fff
style ReactQuery3 fill:#ff6b6b,stroke:#fff,stroke-width:2px,color:#fff
style ReactQuery4 fill:#ff6b6b,stroke:#fff,stroke-width:2px,color:#fff
Component Organization:
- Pages (Black): Next.js App Router pages that define routes
- Feature Components (Blue): Domain-specific components organized by feature
- UI Components (Yellow): Reusable shadcn/ui components
- Data Layer (Red): React Query hooks that fetch data from the API
- Dashed Lines: Data flow dependencies (components use React Query hooks)
Project Structure
frontend/
├── src/
│ ├── app/ # Next.js App Router pages
│ │ ├── layout.tsx # Root layout with favicon and global styles
│ │ ├── page.tsx # Home page - Company dashboard
│ │ ├── globals.css # Tailwind CSS and theme variables
│ │ ├── companies/
│ │ │ └── [id]/
│ │ │ └── statements/
│ │ │ └── [type]/
│ │ │ └── page.tsx # Statement detail page (SSR)
│ │ └── extraction/
│ │ └── page.tsx # Extraction management page
│ │
│ ├── components/
│ │ ├── ui/ # shadcn/ui base components
│ │ │ ├── button.tsx
│ │ │ ├── card.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── badge.tsx
│ │ │ └── table.tsx
│ │ │
│ │ ├── layout/
│ │ │ └── navbar.tsx # Navigation bar with logo
│ │ │
│ │ ├── dashboard/
│ │ │ └── company-list.tsx # Company grid with cards
│ │ │
│ │ ├── statements/
│ │ │ ├── statement-view.tsx # Main statement container
│ │ │ ├── financial-statement-table.tsx # 10-year data table
│ │ │ ├── statement-type-nav.tsx # Statement type tabs
│ │ │ └── statement-error.tsx # Error page component
│ │ │
│ │ ├── documents/
│ │ │ └── document-list.tsx # Document viewer grouped by year
│ │ │
│ │ └── extraction/
│ │ ├── extraction-controls.tsx # Trigger extraction tasks
│ │ ├── extraction-page-content.tsx # Extraction page layout
│ │ └── task-status-monitor.tsx # Real-time task polling
│ │
│ └── lib/
│ ├── api/ # API client modules
│ │ ├── client.ts # Axios instance with interceptors
│ │ ├── companies.ts # Company CRUD operations
│ │ ├── documents.ts # Document listing operations
│ │ ├── statements.ts # Compiled statement retrieval
│ │ └── tasks.ts # Task triggering and status
│ │
│ ├── hooks/ # React Query hooks
│ │ ├── use-companies.ts # Company query hooks
│ │ ├── use-documents.ts # Document query hooks
│ │ ├── use-statements.ts # Statement query hooks
│ │ ├── use-tasks.ts # Task mutation and query hooks
│ │ └── index.ts # Hooks exports
│ │
│ ├── providers/ # React context providers
│ │ └── query-provider.tsx # React Query provider with DevTools
│ │
│ ├── types/
│ │ └── index.ts # TypeScript types matching backend schemas
│ │
│ └── utils/
│ ├── utils.ts # cn() helper for Tailwind classes
│ └── formatters.ts # Currency, number, percent formatters
│
├── public/
│ └── images/
│ ├── favicon.ico
│ └── logo.png
│
├── package.json
├── tsconfig.json
├── tailwind.config.ts
├── next.config.js
└── postcss.config.mjs
Page Architecture
Server Components with Suspense
Next.js 15 uses Server Components with React Suspense for streaming and progressive rendering:
// app/companies/[id]/statements/[type]/page.tsx
export default async function StatementPage({ params }: PageProps) {
const { id, type } = await params;
return <StatementPageContent companyId={parseInt(id)} statementType={type} />;
}
// components/statements/statement-page-content.tsx
export function StatementPageContent({ companyId, statementType }) {
return (
<Suspense fallback={<StatementPageLoading />}>
<StatementDataLoader
companyId={companyId}
statementType={statementType}
/>
</Suspense>
);
}
Benefits:
- Smaller client bundle (component code doesn’t ship to browser)
- Streaming SSR with Suspense boundaries
- Progressive page rendering
- Better SEO (content rendered on server)
- Improved security (secrets can be used on server)
Client Components with React Query
Client Components use React Query hooks for data fetching:
// components/dashboard/company-list.tsx
"use client";
import { useCompanies } from "@/lib/hooks";
export function CompanyList() {
const { data: companies, isLoading, error } = useCompanies();
if (isLoading) return <LoadingSkeleton />;
if (error) return <ErrorDisplay error={error} />;
return <CompanyGrid companies={companies} />;
}
Benefits of React Query:
- Automatic caching and background refetching
- Built-in loading and error states
- Request deduplication
- Optimistic updates
- Automatic retry on failure
Client Components (When Needed)
Use "use client" directive for interactive components:
// components/dashboard/company-list.tsx
"use client";
import { useCompanies } from "@/lib/hooks";
export function CompanyList() {
const { data: companies, isLoading, error } = useCompanies();
// Client Component - uses React Query hooks
}
When to use:
- Component uses React hooks (
useState,useEffect, etc.) - Component handles user interactions (onClick, onChange)
- Component uses browser APIs (localStorage, window)
- Component needs event listeners
- Component uses React Query hooks (
useQuery,useMutation)
Component Architecture
Component Hierarchy
Root Layout
├── Navbar (Server)
└── Page Content
├── Company Dashboard (Client)
│ └── Company Cards (Client)
│ └── View Statements Button (Client)
│
├── Statement Page (Server)
│ └── Statement View (Client)
│ ├── Statement Type Nav (Client)
│ ├── Financial Statement Table (Client)
│ ├── Document List (Client)
│ └── Extraction Controls (Client)
│
└── Extraction Page (Server)
└── Extraction Page Content (Client)
├── Company Selector (Client)
└── Extraction Controls (Client)
└── Task Status Monitor (Client)
Key Components
Financial Statement Table
The FinancialStatementTable component displays 10-year compiled financial data:
Features:
- Sticky header row for horizontal scrolling
- Hierarchical line items with indentation levels
- Restated data indicators (badge + asterisk)
- Currency formatting (millions/billions)
- Responsive design with horizontal scroll
Data Structure:
interface CompiledStatement {
id: number;
company_id: number;
statement_type: string;
data: {
lineItems?: Array<{
name: string;
"2024": number;
"2023": number;
// ... more years
level?: number;
isTotal?: boolean;
}>;
// OR
columns?: string[];
rows?: Array<{ name: string; [year: string]: number }>;
};
}
Task Status Monitor
Real-time polling of Celery task status using React Query:
// lib/hooks/use-tasks.ts
export function useTaskStatus(taskId: string) {
return useQuery({
queryKey: ["tasks", "status", taskId],
queryFn: () => tasksApi.getTaskStatus(taskId),
refetchInterval: (query) => {
const status = query.state.data?.status;
// Poll every 2 seconds if task is still running
if (status === "PENDING" || status === "STARTED" || status === "RETRY") {
return 2000;
}
// Stop polling if task is complete
return false;
},
});
}
Status Handling:
- PENDING → Clock icon, yellow badge
- STARTED → Spinner, blue badge
- SUCCESS → Check icon, green badge with result
- FAILURE → X icon, red badge with error message
- RETRY → Spinner, orange badge
Benefits of React Query approach:
- Automatic polling management
- Cache invalidation on task completion
- Better error handling and retry logic
- Cleaner component code
Data Fetching Architecture
React Query Setup
React Query provides powerful data fetching, caching, and synchronization capabilities:
// lib/providers/query-provider.tsx
export function QueryProvider({ children }) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000, // Data fresh for 1 minute
gcTime: 5 * 60 * 1000, // Cache kept for 5 minutes
refetchOnWindowFocus: false,
retry: 1,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools /> {/* DevTools in development */}
</QueryClientProvider>
);
}
Query Hooks Pattern
Each resource has dedicated React Query hooks:
// lib/hooks/use-companies.ts
export function useCompanies() {
return useQuery({
queryKey: ["companies"],
queryFn: () => companiesApi.getAll(),
});
}
export function useCompany(id: number) {
return useQuery({
queryKey: ["companies", id],
queryFn: () => companiesApi.getById(id),
enabled: !!id,
});
}
export function useCreateCompany() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: companiesApi.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["companies"] });
},
});
}
Benefits:
- Automatic caching and background updates
- Built-in loading and error states
- Optimistic updates support
- Request deduplication
- Automatic retry logic
- Type-safe with TypeScript
API Client Layer
The API client uses Axios with interceptors for consistent error handling:
// lib/api/client.ts
export const apiClient: AxiosInstance = axios.create({
baseURL: `${
process.env.NEXT_PUBLIC_API_URL || "http://localhost:3030"
}/api/v1`,
headers: { "Content-Type": "application/json" },
});
// Error interceptor handles:
// - 404s (logged at debug level, not as errors)
// - Empty error responses ({})
// - Network errors
// - Status-specific error messages
API Modules
Each resource has a dedicated API module that React Query hooks call:
// lib/api/companies.ts
export const companiesApi = {
getAll: async (): Promise<Company[]> => {
const response = await apiClient.get<Company[]>("/companies");
return response.data;
},
getById: async (id: number): Promise<Company> => {
const response = await apiClient.get<Company>(`/companies/${id}`);
return response.data;
},
// ...
};
Styling Architecture
Tailwind CSS 4.0
CSS-First Configuration:
/* src/app/globals.css */
@import "tailwindcss";
@theme {
--radius: 0.5rem;
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
/* ... shadcn/ui CSS variables */
}
}
Key Features:
- No
tailwind.config.jsneeded (CSS-first) - Automatic content detection
- Built-in CSS nesting support
@themedirective for custom tokens
shadcn/ui Components
Components are installed directly into the codebase (not via npm):
// components/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
outline: "border border-input bg-background",
// ...
},
size: { default: "h-10 px-4", sm: "h-9", lg: "h-11" },
},
}
);
Benefits:
- Full control over component code
- Easy customization
- Type-safe variants with CVA
- Consistent with Tailwind utilities
Testing
The frontend uses Vitest and React Testing Library for comprehensive unit testing.
Testing Framework
- Vitest - Fast, Vite-native test runner
- React Testing Library - Component testing utilities
- jsdom - DOM environment for tests
- Coverage: 100% coverage for UI components
Test Organization
Tests are located in tests/ directory, mirroring src/ structure:
tests/
├── components/
│ ├── ui/ # UI component tests
│ └── layout/ # Layout component tests
├── vitest.config.mjs
└── vitest.setup.ts
Running Tests
# Run tests in watch mode
npm test
# Generate coverage report
npm run test:coverage
# Interactive test UI
npm run test:ui
Current Coverage
- UI Components: Button, Badge, Card, Table, Tabs (100%)
- Layout Components: Navbar (100%)
- Total: 65 tests, all passing
For detailed testing documentation, see Frontend Testing.
Error Handling
API Error Interceptor
// Handles empty responses, 404s, network errors
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 404) {
// Log at debug level, not as error
// 404s are expected for missing resources
}
// Extract meaningful error messages
// Attach status code for component handling
}
);
Error Pages
Statement Error Component handles three scenarios:
- Company Not Found (True 404): Company doesn’t exist in database
- Statements Not Generated: Company exists, but statements haven’t been extracted yet
- API Connection Issues: Backend server down or network problems
Smart Detection:
// If company found in list but API returns 404,
// treat as "statements not generated" not "company not found"
const statementsNotGenerated = is404 && companyName;