-
-
Notifications
You must be signed in to change notification settings - Fork 1
Overview
Thrive is a modern job application tracking system built with React 19, TypeScript, Bun, and Vite. This documentation provides technical details for developers working on or extending Thrive.
- Technology Stack
- Project Structure
- Setup & Installation
- Architecture
- State Management
- Routing
- UI Components
- Data Models
- Utilities & Helpers
- Testing
- Build & Deployment
- Contributing
- API Reference
- React 19: Latest React with concurrent features
- TypeScript 5.6: Strict mode enabled
- Bun 1.x: JavaScript runtime and package manager
- Vite 5.x: Build tool and dev server
- TanStack Router 1.x: Type-safe routing
- Tailwind CSS 3.x: Utility-first CSS framework
- shadcn/ui: Accessible component library
- Radix UI: Headless UI primitives
- Lucide Icons: Icon library
- Zustand 4.x: Lightweight state management
- Zustand Persist: State persistence middleware
- Recharts: Charting library for analytics
- Biome: Linting and formatting
- TypeScript ESLint: Additional TypeScript rules
- Vitest: Unit testing framework
- Testing Library: Component testing utilities
- Chrome 90+
- Firefox 88+
- Safari 14+
- Edge 90+
thrive/
โโโ docs/ # Documentation
โ โโโ USER_GUIDE.md # User documentation
โ โโโ DEVELOPER_GUIDE.md # This file
โ โโโ API_REFERENCE.md # API documentation
โ โโโ DEPLOYMENT.md # Deployment guide
โ โโโ accessibility-checklist.md
โ โโโ cross-browser-testing-checklist.md
โ โโโ phase-*.md # Phase implementation summaries
โ
โโโ public/ # Static assets
โ โโโ vite.svg
โ
โโโ src/
โ โโโ components/ # React components
โ โ โโโ a11y/ # Accessibility components
โ โ โ โโโ FocusTrap.tsx
โ โ โ โโโ LiveRegion.tsx
โ โ โ โโโ SkipNav.tsx
โ โ โ โโโ VisuallyHidden.tsx
โ โ โ
โ โ โโโ applications/ # Application management
โ โ โ โโโ ApplicationBoard.tsx
โ โ โ โโโ ApplicationCard.tsx
โ โ โ โโโ ApplicationDetails.tsx
โ โ โ โโโ ApplicationFilters.tsx
โ โ โ โโโ ApplicationForm.tsx
โ โ โ โโโ ApplicationList.tsx
โ โ โ โโโ ApplicationSearch.tsx
โ โ โ โโโ StatusBadge.tsx
โ โ โ
โ โ โโโ analytics/ # Analytics components
โ โ โ โโโ AnalyticsDashboard.tsx
โ โ โ โโโ ApplicationFunnel.tsx
โ โ โ โโโ ApplicationsOverTime.tsx
โ โ โ โโโ InterviewSuccessRate.tsx
โ โ โ โโโ MetricCard.tsx
โ โ โ โโโ ResponseRateByCompany.tsx
โ โ โ โโโ StatusDistribution.tsx
โ โ โ
โ โ โโโ companies/ # Company research
โ โ โ โโโ CompanyCard.tsx
โ โ โ โโโ CompanyDetails.tsx
โ โ โ โโโ CompanyForm.tsx
โ โ โ โโโ CompanyList.tsx
โ โ โ โโโ CompanySearch.tsx
โ โ โ
โ โ โโโ dashboard/ # Dashboard components
โ โ โ โโโ ActivityTimeline.tsx
โ โ โ โโโ QuickActions.tsx
โ โ โ โโโ QuickStats.tsx
โ โ โ โโโ RecentApplications.tsx
โ โ โ โโโ StatsCard.tsx
โ โ โ โโโ UpcomingInterviews.tsx
โ โ โ
โ โ โโโ documents/ # Document management
โ โ โ โโโ DocumentCard.tsx
โ โ โ โโโ DocumentList.tsx
โ โ โ โโโ DocumentUpload.tsx
โ โ โ โโโ DocumentViewer.tsx
โ โ โ
โ โ โโโ export/ # Data export
โ โ โ โโโ ExportOptions.tsx
โ โ โ โโโ ExportPreview.tsx
โ โ โ โโโ ExportProgress.tsx
โ โ โ
โ โ โโโ interviews/ # Interview management
โ โ โ โโโ InterviewCalendar.tsx
โ โ โ โโโ InterviewCard.tsx
โ โ โ โโโ InterviewDetails.tsx
โ โ โ โโโ InterviewForm.tsx
โ โ โ โโโ InterviewList.tsx
โ โ โ
โ โ โโโ layout/ # Layout components
โ โ โ โโโ Header.tsx
โ โ โ โโโ MainLayout.tsx
โ โ โ โโโ Navigation.tsx
โ โ โ โโโ Sidebar.tsx
โ โ โ
โ โ โโโ prep/ # Interview prep
โ โ โ โโโ QuestionBank.tsx
โ โ โ โโโ QuestionCard.tsx
โ โ โ โโโ QuestionCategories.tsx
โ โ โ โโโ QuestionSearch.tsx
โ โ โ
โ โ โโโ settings/ # Settings
โ โ โ โโโ AppearanceSettings.tsx
โ โ โ โโโ DataSettings.tsx
โ โ โ โโโ NotificationSettings.tsx
โ โ โ โโโ ProfileSettings.tsx
โ โ โ
โ โ โโโ ui/ # Shared UI components (shadcn/ui)
โ โ โโโ alert.tsx
โ โ โโโ avatar.tsx
โ โ โโโ badge.tsx
โ โ โโโ button.tsx
โ โ โโโ card.tsx
โ โ โโโ checkbox.tsx
โ โ โโโ dialog.tsx
โ โ โโโ dropdown-menu.tsx
โ โ โโโ empty-state.tsx
โ โ โโโ error-state.tsx
โ โ โโโ input.tsx
โ โ โโโ label.tsx
โ โ โโโ loading-skeletons.tsx
โ โ โโโ select.tsx
โ โ โโโ separator.tsx
โ โ โโโ sheet.tsx
โ โ โโโ skeleton.tsx
โ โ โโโ switch.tsx
โ โ โโโ table.tsx
โ โ โโโ tabs.tsx
โ โ โโโ textarea.tsx
โ โ โโโ toast.tsx
โ โ
โ โโโ hooks/ # Custom React hooks
โ โ โโโ useApplications.ts
โ โ โโโ useCompanies.ts
โ โ โโโ useDocuments.ts
โ โ โโโ useInterviews.ts
โ โ โโโ useLocalStorage.ts
โ โ โโโ useNotifications.ts
โ โ โโโ useTheme.ts
โ โ
โ โโโ lib/ # Utility libraries
โ โ โโโ accessibility.ts # Accessibility helpers
โ โ โโโ browser-detection.ts # Browser detection
โ โ โโโ constants.ts # App constants
โ โ โโโ export.ts # Data export utilities
โ โ โโโ keyboard.ts # Keyboard navigation
โ โ โโโ responsive-testing.ts # Responsive utilities
โ โ โโโ utils.ts # General utilities
โ โ
โ โโโ routes/ # Route components
โ โ โโโ __root.tsx # Root layout
โ โ โโโ analytics.tsx # Analytics page
โ โ โโโ applications.tsx # Applications page
โ โ โโโ companies.tsx # Companies page
โ โ โโโ dashboard.tsx # Dashboard page
โ โ โโโ documents.tsx # Documents page
โ โ โโโ export.tsx # Export page
โ โ โโโ index.tsx # Home page
โ โ โโโ interviews.tsx # Interviews page
โ โ โโโ prep.tsx # Interview prep page
โ โ โโโ settings.tsx # Settings page
โ โ
โ โโโ store/ # Zustand stores
โ โ โโโ applicationStore.ts # Application state
โ โ โโโ companyStore.ts # Company state
โ โ โโโ documentStore.ts # Document state
โ โ โโโ interviewStore.ts # Interview state
โ โ โโโ notificationStore.ts # Notification state
โ โ โโโ settingsStore.ts # Settings state
โ โ
โ โโโ styles/ # Global styles
โ โ โโโ browser-fixes.css # Browser-specific fixes
โ โ โโโ globals.css # Global CSS + Tailwind
โ โ
โ โโโ types/ # TypeScript type definitions
โ โ โโโ application.ts # Application types
โ โ โโโ company.ts # Company types
โ โ โโโ document.ts # Document types
โ โ โโโ interview.ts # Interview types
โ โ โโโ interviewPrep.ts # Interview prep types
โ โ โโโ notification.ts # Notification types
โ โ
โ โโโ main.tsx # App entry point
โ โโโ routeTree.gen.ts # Generated route tree
โ
โโโ .gitignore
โโโ biome.json # Biome configuration
โโโ components.json # shadcn/ui configuration
โโโ index.html
โโโ package.json
โโโ postcss.config.js # PostCSS configuration
โโโ README.md
โโโ tailwind.config.js # Tailwind configuration
โโโ tsconfig.json # TypeScript configuration
โโโ tsconfig.node.json
โโโ vite.config.ts # Vite configuration
- Bun: v1.0.0 or higher
- Node.js: v18.0.0 or higher (for compatibility)
- Git: For version control
# Clone the repository
git clone https://github.com/yourusername/thrive.git
cd thrive
# Install dependencies
bun install
# Start development server
bun run dev
# Open in browser
# Navigate to http://localhost:5173# Development
bun run dev # Start dev server with hot reload
bun run build # Build for production
bun run preview # Preview production build
bun run lint # Run Biome linter
bun run format # Format code with Biome
bun run type-check # TypeScript type checking
# Testing (when implemented)
bun test # Run unit tests
bun test:watch # Run tests in watch mode
bun test:coverage # Generate coverage reportCreate a .env file in the root directory:
# API Configuration (if backend is added)
VITE_API_URL=http://localhost:3000/api
# Feature Flags
VITE_ENABLE_ANALYTICS=true
VITE_ENABLE_NOTIFICATIONS=true
# Build Configuration
VITE_APP_VERSION=1.0.0- Component-Based: Modular, reusable components
- Type-Safe: Strict TypeScript throughout
- Accessible: WCAG 2.1 AA compliance
- Performant: Code splitting, lazy loading, memoization
- Maintainable: Clear structure, documentation, conventions
User Action
โ
Component Event Handler
โ
Zustand Store Action
โ
State Update
โ
Component Re-render
โ
LocalStorage Persistence (via middleware)
Components are organized by feature (applications, interviews, etc.) rather than by type (containers, presentational). This makes it easier to find related code and manage features independently.
Routes are automatically code-split by TanStack Router. Additional components can be lazy-loaded:
import { lazy } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));All stores follow a consistent pattern:
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface StoreState {
// State
items: Item[];
// Actions
addItem: (item: Item) => void;
updateItem: (id: string, updates: Partial<Item>) => void;
deleteItem: (id: string) => void;
}
export const useStore = create<StoreState>()(
persist(
(set, get) => ({
// Initial state
items: [],
// Actions
addItem: (item) =>
set((state) => ({ items: [...state.items, item] })),
updateItem: (id, updates) =>
set((state) => ({
items: state.items.map((item) =>
item.id === id ? { ...item, ...updates } : item
),
})),
deleteItem: (id) =>
set((state) => ({
items: state.items.filter((item) => item.id !== id),
})),
}),
{
name: 'store-name',
version: 1,
}
)
);interface ApplicationStore {
applications: Application[];
addApplication: (app: Application) => void;
updateApplication: (id: string, updates: Partial<Application>) => void;
deleteApplication: (id: string) => void;
getApplicationById: (id: string) => Application | undefined;
getApplicationsByStatus: (status: ApplicationStatus) => Application[];
}interface InterviewStore {
interviews: Interview[];
addInterview: (interview: Interview) => void;
updateInterview: (id: string, updates: Partial<Interview>) => void;
deleteInterview: (id: string) => void;
getInterviewsByApplication: (appId: string) => Interview[];
getUpcomingInterviews: () => Interview[];
}interface CompanyStore {
companies: Company[];
addCompany: (company: Company) => void;
updateCompany: (id: string, updates: Partial<Company>) => void;
deleteCompany: (id: string) => void;
getCompanyById: (id: string) => Company | undefined;
}function MyComponent() {
// Select specific state
const applications = useApplicationStore((state) => state.applications);
const addApplication = useApplicationStore((state) => state.addApplication);
// Or select multiple
const { applications, addApplication } = useApplicationStore();
// Derived state (memoized automatically)
const activeApps = useApplicationStore((state) =>
state.applications.filter((app) => app.status === 'active')
);
return (
<button onClick={() => addApplication(newApp)}>
Add Application
</button>
);
}Thrive uses file-based routing with TanStack Router for type-safe navigation.
routes/
โโโ __root.tsx # Root layout (wraps all routes)
โโโ index.tsx # / (home/dashboard)
โโโ applications.tsx # /applications
โโโ interviews.tsx # /interviews
โโโ companies.tsx # /companies
โโโ documents.tsx # /documents
โโโ analytics.tsx # /analytics
โโโ prep.tsx # /prep
โโโ export.tsx # /export
โโโ settings.tsx # /settings
-
Create route file in
src/routes/:
// src/routes/new-page.tsx
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/new-page')({
component: NewPageComponent,
});
function NewPageComponent() {
return (
<div>
<h1>New Page</h1>
</div>
);
}-
Router automatically picks it up (no manual registration needed)
-
Navigate to
/new-page
import { Link, useNavigate } from '@tanstack/react-router';
function MyComponent() {
const navigate = useNavigate();
return (
<>
{/* Declarative navigation */}
<Link to="/applications">Applications</Link>
{/* Programmatic navigation */}
<button onClick={() => navigate({ to: '/interviews' })}>
Go to Interviews
</button>
</>
);
}// Route with parameter
export const Route = createFileRoute('/applications/$appId')({
component: ApplicationDetailsComponent,
});
function ApplicationDetailsComponent() {
const { appId } = Route.useParams();
return <div>Application ID: {appId}</div>;
}export const Route = createFileRoute('/applications')({
component: ApplicationsComponent,
validateSearch: (search) => ({
status: search.status as string | undefined,
page: Number(search.page ?? 1),
}),
});
function ApplicationsComponent() {
const { status, page } = Route.useSearch();
return <div>Status: {status}, Page: {page}</div>;
}Thrive uses shadcn/ui components, which are customizable and accessible. Components are copied into src/components/ui/ for full control.
# Add a component
bunx shadcn@latest add button
# Add multiple components
bunx shadcn@latest add card dialog input// Card with subcomponents
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
function MyCard() {
return (
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
</CardHeader>
<CardContent>Content</CardContent>
</Card>
);
}// Controlled
function ControlledInput() {
const [value, setValue] = useState('');
return (
<Input
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
}
// Uncontrolled (with ref)
function UncontrolledInput() {
const inputRef = useRef<HTMLInputElement>(null);
const handleSubmit = () => {
console.log(inputRef.current?.value);
};
return <Input ref={inputRef} />;
}// Compose smaller components
function ApplicationCard({ application }: Props) {
return (
<Card>
<CardHeader>
<StatusBadge status={application.status} />
<CardTitle>{application.position}</CardTitle>
</CardHeader>
<CardContent>
<CompanyInfo company={application.company} />
<DateInfo date={application.appliedDate} />
</CardContent>
</Card>
);
}Create reusable logic with custom hooks:
// useApplications.ts
export function useApplications() {
const applications = useApplicationStore((state) => state.applications);
const addApplication = useApplicationStore((state) => state.addApplication);
const activeApplications = useMemo(
() => applications.filter((app) => app.status !== 'rejected'),
[applications]
);
return {
applications,
activeApplications,
addApplication,
};
}
// Usage in component
function MyComponent() {
const { activeApplications, addApplication } = useApplications();
return <div>{activeApplications.length} active</div>;
}interface Application {
id: string;
position: string;
company: string;
status: ApplicationStatus;
appliedDate: string;
location?: string;
jobType?: 'remote' | 'hybrid' | 'onsite';
salaryRange?: {
min: number;
max: number;
currency: string;
};
description?: string;
url?: string;
notes?: Note[];
documents?: string[]; // Document IDs
contacts?: Contact[];
createdAt: string;
updatedAt: string;
}
type ApplicationStatus =
| 'wishlist'
| 'applied'
| 'screening'
| 'phone-interview'
| 'interview'
| 'assessment'
| 'offer'
| 'accepted'
| 'rejected'
| 'withdrawn';interface Interview {
id: string;
applicationId: string;
type: InterviewType;
date: string;
duration: number; // minutes
location?: string;
isRemote: boolean;
meetingLink?: string;
interviewers?: Interviewer[];
notes?: string;
preparation?: string;
followUpDate?: string;
status: 'scheduled' | 'completed' | 'cancelled';
createdAt: string;
updatedAt: string;
}
type InterviewType =
| 'phone-screen'
| 'video-call'
| 'onsite'
| 'technical'
| 'behavioral'
| 'panel'
| 'final';interface Company {
id: string;
name: string;
website?: string;
industry?: string;
size?: string;
location?: string;
description?: string;
culture?: string;
benefits?: string[];
techStack?: string[];
interviewProcess?: string;
notes?: string;
rating?: number;
createdAt: string;
updatedAt: string;
}interface Document {
id: string;
name: string;
type: DocumentType;
fileType: string;
size: number;
url: string;
applicationIds?: string[];
tags?: string[];
version: number;
uploadedAt: string;
updatedAt: string;
}
type DocumentType =
| 'resume'
| 'cover-letter'
| 'portfolio'
| 'certification'
| 'reference'
| 'other';// Class name utility (for Tailwind)
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
// Date formatting
export function formatDate(date: string | Date): string {
return new Date(date).toLocaleDateString();
}
// ID generation
export function generateId(): string {
return crypto.randomUUID();
}
// Debounce
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}// Screen reader announcements
export function announceToScreenReader(message: string): void {
const announcement = document.createElement('div');
announcement.setAttribute('role', 'status');
announcement.setAttribute('aria-live', 'polite');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
setTimeout(() => document.body.removeChild(announcement), 1000);
}
// Generate accessible labels
export function getStatusAriaLabel(status: ApplicationStatus): string {
const labels: Record<ApplicationStatus, string> = {
wishlist: 'On wishlist',
applied: 'Application submitted',
screening: 'In screening process',
// ... etc
};
return labels[status];
}// Export to CSV
export function exportToCSV(data: Application[]): void {
const csv = convertToCSV(data);
downloadFile(csv, 'applications.csv', 'text/csv');
}
// Export to JSON
export function exportToJSON(data: any): void {
const json = JSON.stringify(data, null, 2);
downloadFile(json, 'applications.json', 'application/json');
}
// Export to PDF
export function exportToPDF(data: Application[]): Promise<void> {
// PDF generation logic
}// Detect browser
export function detectBrowser(): BrowserInfo {
// Returns browser name, version, OS, device type
}
// Check if browser is supported
export function isBrowserSupported(): boolean {
const browser = detectBrowser();
const minVersions = {
chrome: 90,
firefox: 88,
safari: 14,
edge: 90,
};
return browser.majorVersion >= minVersions[browser.name];
}# Install testing dependencies (if not already installed)
bun add -d vitest @testing-library/react @testing-library/jest-dom
bun add -d @testing-library/user-event happy-dom// ApplicationCard.test.tsx
import { render, screen } from '@testing-library/react';
import { ApplicationCard } from './ApplicationCard';
describe('ApplicationCard', () => {
const mockApplication = {
id: '1',
position: 'Software Engineer',
company: 'Test Company',
status: 'applied',
appliedDate: '2025-10-01',
};
it('renders application details', () => {
render(<ApplicationCard application={mockApplication} />);
expect(screen.getByText('Software Engineer')).toBeInTheDocument();
expect(screen.getByText('Test Company')).toBeInTheDocument();
});
it('shows correct status badge', () => {
render(<ApplicationCard application={mockApplication} />);
expect(screen.getByText('Applied')).toBeInTheDocument();
});
});// ApplicationList.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ApplicationList } from './ApplicationList';
describe('ApplicationList', () => {
it('filters applications by status', async () => {
const user = userEvent.setup();
render(<ApplicationList />);
const filterButton = screen.getByRole('button', { name: /filter/i });
await user.click(filterButton);
const appliedFilter = screen.getByRole('checkbox', { name: /applied/i });
await user.click(appliedFilter);
// Assert filtered results
});
});// applicationStore.test.ts
import { renderHook, act } from '@testing-library/react';
import { useApplicationStore } from './applicationStore';
describe('Application Store', () => {
it('adds an application', () => {
const { result } = renderHook(() => useApplicationStore());
const newApp = {
id: '1',
position: 'Developer',
company: 'Test Corp',
status: 'applied',
appliedDate: '2025-10-01',
};
act(() => {
result.current.addApplication(newApp);
});
expect(result.current.applications).toHaveLength(1);
expect(result.current.applications[0]).toEqual(newApp);
});
});# Build for production
bun run build
# Preview production build locally
bun run previewdist/
โโโ assets/
โ โโโ index-[hash].js
โ โโโ index-[hash].css
โ โโโ [other chunks]
โโโ index.html
โโโ vite.svg
Automatic Optimizations:
- Code splitting by route
- Tree shaking
- Minification
- Asset optimization
- CSS purging (unused Tailwind classes)
Manual Optimizations:
- Lazy load heavy components
- Use React.memo for expensive renders
- Implement virtualization for long lists
- Optimize images (WebP format)
See DEPLOYMENT.md for detailed deployment instructions.
Quick Deploy to Vercel:
# Install Vercel CLI
bun add -g vercel
# Deploy
vercel- TypeScript: Strict mode, explicit types
- Formatting: Biome (runs on commit)
- Naming: camelCase for variables, PascalCase for components
- Files: kebab-case for files, PascalCase for components
# Create feature branch
git checkout -b feature/my-feature
# Make changes and commit
git add .
git commit -m "feat: add new feature"
# Push and create PR
git push origin feature/my-featureFollow Conventional Commits:
-
feat:New feature -
fix:Bug fix -
docs:Documentation changes -
style:Code style changes (formatting) -
refactor:Code refactoring -
test:Adding tests -
chore:Build process or auxiliary tools
- Create a feature branch
- Make your changes
- Add tests if applicable
- Update documentation
- Run
bun run lintandbun run type-check - Create PR with clear description
- Wait for review and approval
See API_REFERENCE.md for detailed API documentation of all components, hooks, stores, and utilities.
- Memoization:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);- Callback Memoization:
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);- Component Memoization:
const MemoizedComponent = React.memo(MyComponent);- Virtualization (for long lists):
import { useVirtualizer } from '@tanstack/react-virtual';- Code Splitting:
const HeavyComponent = lazy(() => import('./HeavyComponent'));Use React DevTools Profiler to identify performance bottlenecks:
- Open React DevTools
- Go to Profiler tab
- Start recording
- Perform actions
- Stop recording
- Analyze render times
Build fails with TypeScript errors:
- Run
bun run type-checkto see errors - Fix type errors before building
Hot reload not working:
- Check Vite config
- Restart dev server
- Clear
.vitecache
Store not persisting:
- Check localStorage is enabled
- Check store configuration
- Verify persist middleware setup
Route not found:
- Ensure route file is in
src/routes/ - Check route file naming
- Restart dev server
Last Updated: October 18, 2025 Version: 1.0.0