Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,277 changes: 1,191 additions & 86 deletions frontend/package-lock.json

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,22 @@
"lint": "next lint"
},
"dependencies": {
"@clerk/nextjs": "^7.2.5",
"@radix-ui/react-dialog": "^1.1.4",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.3",
"@tanstack/react-query": "^5.74.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.503.0",
"next": "^15.5.15",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"tailwind-merge": "^3.3.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
Expand Down
188 changes: 188 additions & 0 deletions frontend/src/app/(app)/applications/[id]/application-detail-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
"use client";

import Link from "next/link";
import { useParams } from "next/navigation";
import { ArrowLeft, Download } from "lucide-react";

import { MatchBadge } from "@/components/match-badge";
import { StatusBadge } from "@/components/status-badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useApplication, useResume } from "@/lib/hooks/use-api";

export default function ApplicationDetailClient() {
const params = useParams<{ id: string }>();
const id = params?.id;

const { data: application, isLoading } = useApplication(id);
const { data: resume } = useResume(application?.resumeId);

if (isLoading) {
return (
<div className="space-y-4">
<Skeleton className="h-10 w-64" />
<Skeleton className="h-64 w-full" />
</div>
);
}

if (!application) {
return (
<div className="rounded-lg border p-8 text-center">
<p className="text-muted-foreground">Application not found.</p>
<Button asChild className="mt-4" variant="outline">
<Link href="/applications">Back to applications</Link>
</Button>
</div>
);
}

return (
<div className="space-y-6">
<div>
<Button asChild variant="ghost" size="sm" className="-ml-3">
<Link href="/applications">
<ArrowLeft className="mr-1 h-4 w-4" />
Back to applications
</Link>
</Button>
</div>

<div className="flex flex-col justify-between gap-4 md:flex-row md:items-start">
<div>
<h1 className="text-3xl font-bold tracking-tight">
{application.position}
</h1>
<p className="mt-1 text-muted-foreground">{application.company}</p>
{application.jobUrl ? (
<a
href={application.jobUrl}
target="_blank"
rel="noreferrer"
className="mt-1 inline-block text-sm text-primary underline-offset-4 hover:underline"
>
View original posting
</a>
) : null}
</div>
<div className="flex items-center gap-2">
<MatchBadge score={application.matchScore} />
<StatusBadge status={application.status} />
</div>
</div>

<Tabs defaultValue="job" className="space-y-4">
<TabsList>
<TabsTrigger value="job">Job description</TabsTrigger>
<TabsTrigger value="resume">Tailored resume</TabsTrigger>
<TabsTrigger value="cover">Cover letter</TabsTrigger>
<TabsTrigger value="gaps">Gap analysis</TabsTrigger>
</TabsList>

<TabsContent value="job">
<Card>
<CardHeader>
<CardTitle>Job description</CardTitle>
</CardHeader>
<CardContent>
<pre className="whitespace-pre-wrap rounded-md bg-muted p-4 text-sm">
{application.jobDescription}
</pre>
</CardContent>
</Card>
</TabsContent>

<TabsContent value="resume">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Tailored resume</CardTitle>
{resume ? (
<Button variant="outline" size="sm">
<Download className="mr-2 h-4 w-4" />
Download PDF
</Button>
) : null}
</CardHeader>
<CardContent>
{resume ? (
<pre className="whitespace-pre-wrap rounded-md bg-muted p-4 font-mono text-sm">
{resume.content}
</pre>
) : (
<p className="text-sm text-muted-foreground">
No tailored resume yet.
</p>
)}
</CardContent>
</Card>
</TabsContent>

<TabsContent value="cover">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Cover letter</CardTitle>
{application.coverLetter ? (
<Button variant="outline" size="sm">
<Download className="mr-2 h-4 w-4" />
Download PDF
</Button>
) : null}
</CardHeader>
<CardContent>
{application.coverLetter ? (
<pre className="whitespace-pre-wrap rounded-md bg-muted p-4 text-sm">
{application.coverLetter}
</pre>
) : (
<p className="text-sm text-muted-foreground">
No cover letter generated yet.
</p>
)}
</CardContent>
</Card>
</TabsContent>

<TabsContent value="gaps">
<Card>
<CardHeader>
<CardTitle>Gap analysis</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{(application.gaps ?? []).length === 0 ? (
<p className="text-sm text-muted-foreground">
No gaps detected — your profile aligns well with this role.
</p>
) : (
(application.gaps ?? []).map((gap) => (
<div
key={gap.skill}
className="flex items-start justify-between gap-4 rounded-md border p-3"
>
<div>
<p className="font-medium">{gap.skill}</p>
{gap.note ? (
<p className="text-sm text-muted-foreground">
{gap.note}
</p>
) : null}
</div>
<span className="text-xs uppercase tracking-wide text-muted-foreground">
{gap.severity}
</span>
</div>
))
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}
9 changes: 9 additions & 0 deletions frontend/src/app/(app)/applications/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import ApplicationDetailClient from "./application-detail-client";

export function generateStaticParams() {
return [];
}

export default function ApplicationDetailPage() {
return <ApplicationDetailClient />;
}
79 changes: 79 additions & 0 deletions frontend/src/app/(app)/applications/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"use client";

import Link from "next/link";
import { Plus } from "lucide-react";

import { MatchBadge } from "@/components/match-badge";
import { StatusBadge } from "@/components/status-badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Skeleton } from "@/components/ui/skeleton";
import { useApplications } from "@/lib/hooks/use-api";

export default function ApplicationsPage() {
const { data: applications, isLoading } = useApplications();

return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Applications</h1>
<p className="mt-1 text-muted-foreground">
Track every job you&apos;ve tailored an application for.
</p>
</div>
<Button asChild>
<Link href="/apply">
<Plus className="mr-2 h-4 w-4" />
New application
</Link>
</Button>
</div>

<div className="space-y-3">
{isLoading ? (
<>
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
<Skeleton className="h-24 w-full" />
</>
) : (applications ?? []).length === 0 ? (
<Card>
<CardContent className="py-12 text-center">
<p className="text-sm text-muted-foreground">
No applications yet.
</p>
<Button asChild className="mt-4">
<Link href="/apply">Create your first application</Link>
</Button>
</CardContent>
</Card>
) : (
(applications ?? []).map((app) => (
<Link
key={app.id}
href={`/applications/${app.id}`}
className="block"
>
<Card className="transition-colors hover:bg-accent/40">
<CardContent className="flex items-start justify-between gap-4 py-5">
<div className="space-y-1">
<p className="text-lg font-semibold">{app.position}</p>
<p className="text-sm text-muted-foreground">{app.company}</p>
<p className="text-xs text-muted-foreground">
Applied {new Date(app.createdAt).toLocaleDateString()}
</p>
</div>
<div className="flex flex-col items-end gap-2">
<MatchBadge score={app.matchScore} />
<StatusBadge status={app.status} />
</div>
</CardContent>
</Card>
</Link>
))
)}
</div>
</div>
);
}
Loading
Loading