Frontend Development
This guide covers the frontend architecture and development practices for the SEO Platform.
Project Structure
frontend/
├── src/
│ ├── api/
│ │ └── client.ts # API client configuration
│ ├── components/
│ │ ├── ui/ # Base UI components
│ │ │ ├── Button.tsx
│ │ │ ├── Card.tsx
│ │ │ └── ...
│ │ ├── TaxonomyTree.tsx # Feature components
│ │ └── ...
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utilities
│ ├── pages/ # Route components
│ │ ├── LoginPage.tsx
│ │ ├── SignupPage.tsx
│ │ ├── JobsPage.tsx
│ │ ├── JobDetailPage.tsx
│ │ ├── DedupeReviewPage.tsx
│ │ ├── PromptConfigsPage.tsx
│ │ └── UsagePage.tsx
│ ├── App.tsx # Root component
│ └── main.tsx # Entry point
├── public/ # Static assets
├── package.json
└── vite.config.ts
Pages Overview
Authentication Pages
| Page | Component | APIs |
|---|---|---|
| Login | LoginPage.tsx | POST /api/v1/auth/login |
| Signup | SignupPage.tsx | POST /api/v1/users/signup |
Main Application Pages
| Page | Component | APIs |
|---|---|---|
| Jobs List | JobsPage.tsx | GET /api/v1/jobs |
| Job Detail | JobDetailPage.tsx | GET /api/v1/jobs/{id}, POST /api/v1/jobs/{id}/classify, GET /api/v1/jobs/{id}/export |
| Dedupe Review | DedupeReviewPage.tsx | GET /api/v1/dedupe/queue, POST /api/v1/dedupe/queue/bulk/* |
| Prompt Configs | PromptConfigsPage.tsx | GET /api/v1/prompt-configs |
| Usage | UsagePage.tsx | GET /api/v1/usage/budget, GET /api/v1/usage/ledger |
Running the Frontend
Development Server
cd frontend
npm install
npm run dev
The frontend runs at http://localhost:5173 by default.
Production Build
npm run build
npm run preview # Preview production build
API Client
Configuration
The API client is configured in frontend/src/api/client.ts:
import { QueryClient } from '@tanstack/react-query';
export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 1,
},
},
});
export async function apiClient<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const token = localStorage.getItem('access_token');
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options?.headers,
},
});
if (!response.ok) {
throw new Error(`API Error: ${response.status}`);
}
return response.json();
}
Using TanStack Query
import { useQuery, useMutation } from '@tanstack/react-query';
import { apiClient } from '../api/client';
// Fetching data
export function useJobs() {
return useQuery({
queryKey: ['jobs'],
queryFn: () => apiClient<Job[]>('/api/v1/jobs'),
});
}
// Mutations
export function useCreateJob() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (file: File) => {
const formData = new FormData();
formData.append('file', file);
return apiClient('/api/v1/jobs/upload', {
method: 'POST',
body: formData,
headers: {}, // Remove Content-Type for FormData
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['jobs'] });
},
});
}
State Conventions
Loading States
Always handle loading, error, and empty states explicitly:
function JobsPage() {
const { data: jobs, isLoading, error } = useJobs();
if (isLoading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorMessage error={error} />;
}
if (!jobs?.length) {
return <EmptyState message="No jobs found" />;
}
return (
<JobsList jobs={jobs} />
);
}
Cache Key Conventions
Align cache keys with endpoint paths:
// GET /api/v1/jobs
queryKey: ['jobs']
// GET /api/v1/jobs/{id}
queryKey: ['jobs', jobId]
// GET /api/v1/taxonomies
queryKey: ['taxonomies']
// GET /api/v1/usage/budget
queryKey: ['usage', 'budget']
Component Development
UI Components
Base components are in frontend/src/components/ui/:
// Button.tsx
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
}
export function Button({
variant = 'primary',
size = 'md',
loading,
children,
...props
}: ButtonProps) {
return (
<button
className={cn(
'rounded font-medium transition-colors',
variants[variant],
sizes[size],
loading && 'opacity-50 cursor-not-allowed'
)}
disabled={loading}
{...props}
>
{loading ? <Spinner /> : children}
</button>
);
}
Feature Components
Feature-specific components live in frontend/src/components/:
// TaxonomyTree.tsx
interface TaxonomyTreeProps {
taxonomy: Taxonomy;
onCategorySelect?: (category: Category) => void;
}
export function TaxonomyTree({ taxonomy, onCategorySelect }: TaxonomyTreeProps) {
// Implementation
}
Styling with Tailwind
Configuration
Tailwind is configured in tailwind.config.js:
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
// ... color scale
900: '#1e3a8a',
},
},
},
},
plugins: [],
};
Usage
function Card({ children }: { children: React.ReactNode }) {
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
{children}
</div>
);
}
Environment Variables
Required variables (.env):
VITE_API_URL=http://localhost:8000
VITE_DOCS_URL=https://docs.seo-platform.com
Testing
Running Tests
# Unit tests
npm run test
# Watch mode
npm run test:watch
# Coverage
npm run test:coverage
Test Example
import { render, screen } from '@testing-library/react';
import { JobsPage } from './JobsPage';
describe('JobsPage', () => {
it('renders jobs list', async () => {
render(<JobsPage />);
expect(await screen.findByText('My Jobs')).toBeInTheDocument();
});
});
Code Quality
Linting
# Run ESLint
npm run lint
# Auto-fix
npm run lint:fix
Type Checking
npm run typecheck
Related Documentation
- Architecture Overview - System architecture
- API Reference - Backend API documentation
- Testing Guide - Testing practices