Frontend Architecture
The Vulcan web frontend is built with React 19, TypeScript, and Vite, following a feature-based architecture.
Technology Stack
| Technology | Purpose |
|---|---|
| React 19 | UI framework |
| TypeScript | Type safety |
| Vite | Build tooling |
| TanStack Query | Server state management |
| Redux Toolkit | Client state management |
| MUI (Material UI) | Component library |
| React Router | Navigation |
| Vitest | Unit testing |
| Playwright | E2E testing |
Directory Structure
vulcan-web/
├── src/
│ ├── api/ # Generated API clients
│ ├── components/ # Shared UI components
│ │ ├── common/ # Buttons, Cards, Dialogs
│ │ ├── layout/ # AppShell, Navigation
│ │ └── feedback/ # Toast, Loading, Error
│ ├── features/ # Feature modules
│ │ ├── dashboard/
│ │ ├── leads/
│ │ ├── quotation/
│ │ └── ...
│ ├── hooks/ # Shared custom hooks
│ ├── lib/ # Pure utility functions
│ ├── pages/ # Route entry points
│ ├── routes/ # Route configuration
│ ├── store/ # Redux store
│ ├── styles/ # Global styles + theme
│ └── types/ # Shared TypeScript types
├── tests/ # E2E tests
├── vite.config.ts
└── package.jsonFeature Module Structure
Each feature is a self-contained module with colocated components, hooks, and API calls.
features/leads/
├── components/
│ ├── LeadList.tsx
│ ├── LeadDetail.tsx
│ ├── LeadForm.tsx
│ └── index.ts
├── hooks/
│ ├── useLeads.ts
│ └── useCreateLead.ts
├── api/
│ └── leadsApi.ts
├── types.ts
└── index.tsState Management
Server State (TanStack Query)
For data fetched from the API.
typescript
// features/leads/hooks/useLeads.ts
export function useLeads() {
return useQuery({
queryKey: ['leads'],
queryFn: () => leadsApi.getAll(),
});
}
export function useCreateLead() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateLeadRequest) => leadsApi.create(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['leads'] });
},
});
}Client State (Redux Toolkit)
For UI state that doesn't come from the server.
typescript
// store/slices/uiSlice.ts
const uiSlice = createSlice({
name: 'ui',
initialState: {
sidebarOpen: true,
theme: 'light',
},
reducers: {
toggleSidebar: (state) => {
state.sidebarOpen = !state.sidebarOpen;
},
},
});Component Patterns
Props Interface Above Component
typescript
interface LeadCardProps {
lead: Lead;
onEdit: (id: string) => void;
onDelete: (id: string) => void;
}
export function LeadCard({ lead, onEdit, onDelete }: LeadCardProps) {
// Component implementation
}Hooks at Top
typescript
export function LeadDetail({ id }: { id: string }) {
// Hooks first
const { data: lead, isLoading } = useLead(id);
const { mutate: deleteLead } = useDeleteLead();
// Early returns for loading/error
if (isLoading) return <LoadingSpinner />;
if (!lead) return <NotFound />;
// Main render
return (
<Paper>
{/* ... */}
</Paper>
);
}API Integration
Generated Client
API clients are generated from OpenAPI spec using openapi-generator.
bash
npm run generate:clientTanStack Query Hooks
Wrap generated client calls in TanStack Query hooks.
typescript
// features/leads/api/leadsApi.ts
import { LeadsApi } from '@/api/generated';
const api = new LeadsApi();
export const leadsApi = {
getAll: () => api.getLeads(),
getById: (id: string) => api.getLead({ id }),
create: (data: CreateLeadRequest) => api.createLead({ createLeadRequest: data }),
update: (id: string, data: UpdateLeadRequest) => api.updateLead({ id, updateLeadRequest: data }),
delete: (id: string) => api.deleteLead({ id }),
};Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Components | PascalCase | LeadCard.tsx |
| Hooks | camelCase with use | useLeads.ts |
| Utilities | camelCase | formatDate.ts |
| Types | PascalCase | Lead, CreateLeadRequest |
| Constants | SCREAMING_SNAKE | API_BASE_URL |
Testing
Unit Tests (Vitest)
typescript
// features/leads/hooks/useLeads.test.ts
describe('useLeads', () => {
it('should fetch leads', async () => {
// Test implementation
});
});E2E Tests (Playwright)
typescript
// tests/leads.spec.ts
test('should create a new lead', async ({ page }) => {
await page.goto('/leads');
await page.click('button:has-text("New Lead")');
// ...
});Commands
bash
npm run dev # Start dev server
npm run build # Production build
npm run test # Run unit tests
npm run test:e2e # Run E2E tests
npm run lint # Lint code
npm run type-check # TypeScript check
npm run generate:client # Generate API client