The frontend is a Next.js 16 application with App Router, using static export for deployment flexibility.
- Root providers (Theme, Clerk, tRPC, Toast)
- Global styles and fonts
- HTML structure
- Single page app with hash router
- Routes:
/,/login,/resume/:id
Custom hash-based routing for static export:
// Routes are defined in src/components/app-router.tsx
<HashRouter>
<Route path="/" component={Dashboard} />
<Route path="/login" component={Login} />
<Route path="/resume/:id" component={ResumeEditor} />
</HashRouter>import { useHashNavigate } from '@/lib/hash-router';
const navigate = useHashNavigate();
navigate('/resume/123');Primary data fetching layer:
// Query
const { data, isLoading } = trpc.resume.list.useQuery();
// Mutation
const mutation = trpc.resume.create.useMutation();
await mutation.mutateAsync({ name: 'My Resume' });Local UI state:
auth.store.ts- Auth state (synced with Clerk)resume.store.ts- Current resume editing state
shadcn/ui primitives:
button.tsx,input.tsx,card.tsxaccordion.tsx,dialog.tsx,select.tsxform-save-bar.tsx- Unsaved changes indicator
Resume section editors:
personal-info-form.tsx- Name, contact, linksexperience-form.tsx- Work historyeducation-form.tsx- Academic backgroundskills-form.tsx- Skill categoriesprojects-form.tsx- Project showcase- Plus: certifications, awards, volunteer, publications, references, languages
function ExperienceForm({ data, onChange }) {
// Local state for editing
const [localData, setLocalData] = useState(data);
// Track unsaved changes
const hasChanges = JSON.stringify(localData) !== JSON.stringify(data);
// Save handler
const handleSave = () => onChange(localData);
return (
<div>
<FormSaveBar hasChanges={hasChanges} onSave={handleSave} />
{/* Form fields */}
</div>
);
}<Accordion>
{items.map((item, index) => (
<AccordionItem key={item.id}>
<div className="flex items-center">
<AccordionTrigger>
{/* Item summary */}
</AccordionTrigger>
{/* Action buttons OUTSIDE trigger (no nested buttons!) */}
<div className="flex gap-1">
<Button onClick={() => moveItem(index, 'up')}>↑</Button>
<Button onClick={() => removeItem(item.id)}>🗑</Button>
</div>
</div>
<AccordionContent>
{/* Form fields */}
</AccordionContent>
</AccordionItem>
))}
</Accordion>preview.tsx- Live PDF previewsections-manager.tsx- Add/remove/reorder sectionstemplate-selector.tsx- Choose resume templateformatting-toolbar.tsx- Style controls
pdf-document.tsx- React PDF templateexport-buttons.tsx- Download PDF/DOCX
- Lists user's resumes
- Create new resume
- Resume cards with quick actions
Main editing interface:
function ResumeEditor() {
const { id } = useParams();
const { data: resume } = trpc.resume.getById.useQuery({ id });
// Section form rendering based on type
const renderSectionForm = (section) => {
switch (section.type) {
case 'experience': return <ExperienceForm data={section.content} />;
case 'education': return <EducationForm data={section.content} />;
// ...
}
};
}- Clerk SignIn component
- Redirect after auth
// Configures tRPC client with:
// - API URL from env
// - Auth headers from Clerk
// - React Query integration// Wraps app with Clerk auth context
// Handles sign-in redirects// Dark/light mode support
// Persists to localStorageConfirmation dialog hook:
const confirm = useConfirm();
const confirmed = await confirm('Delete?', 'Are you sure?');
if (confirmed) doDelete();TypeScript types for resume data:
interface Resume {
id: string;
name: string;
metadata: ResumeMetadata;
sections: Section[];
}
interface ResumeMetadata {
personalInfo: PersonalInfo;
settings: ResumeSettings;
}Includes factory functions:
createDefaultExperience()
createDefaultEducation()
createDefaultSkillCategory()
// etc.- Config in
tailwind.config.js - Global styles in
src/styles/globals.css - CSS variables for theming
import { cn } from '@/lib/utils';
<div className={cn('base-class', conditional && 'extra-class')} />NEXT_PUBLIC_API_URL=http://localhost:4000
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
NEXT_PUBLIC_DEV_MODE=true
# Development
npm run dev
# Build static export
npm run build
# Output in /out directory