Skip to main content

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

PageComponentAPIs
LoginLoginPage.tsxPOST /api/v1/auth/login
SignupSignupPage.tsxPOST /api/v1/users/signup

Main Application Pages

PageComponentAPIs
Jobs ListJobsPage.tsxGET /api/v1/jobs
Job DetailJobDetailPage.tsxGET /api/v1/jobs/{id}, POST /api/v1/jobs/{id}/classify, GET /api/v1/jobs/{id}/export
Dedupe ReviewDedupeReviewPage.tsxGET /api/v1/dedupe/queue, POST /api/v1/dedupe/queue/bulk/*
Prompt ConfigsPromptConfigsPage.tsxGET /api/v1/prompt-configs
UsageUsagePage.tsxGET /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