AshameTheDestroyer commited on
Commit
a774a10
·
1 Parent(s): 7a6bc15

Dashboard Initiated.

Browse files
frontend/app/components/overview-charts.tsx ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ Legend,
3
+ Tooltip,
4
+ BarElement,
5
+ ArcElement,
6
+ LinearScale,
7
+ LineElement,
8
+ PointElement,
9
+ CategoryScale,
10
+ Chart as ChartJS,
11
+ } from "chart.js";
12
+ import { Bar, Doughnut } from "react-chartjs-2";
13
+ import type { Job } from "~/services/useGetJobs";
14
+
15
+ ChartJS.register(
16
+ CategoryScale,
17
+ LinearScale,
18
+ BarElement,
19
+ PointElement,
20
+ LineElement,
21
+ Tooltip,
22
+ Legend,
23
+ ArcElement
24
+ );
25
+
26
+ export default function OverviewCharts({ jobs, loading }: { jobs: Job[]; loading?: boolean }) {
27
+ const labels = jobs.map(j => (j.title.length > 20 ? j.title.slice(0, 20) + "…" : j.title));
28
+ const applicantsData = jobs.map(j => j.applicants_count ?? 0);
29
+
30
+ const seniorities = ["intern", "junior", "mid", "senior"];
31
+ const seniorityCounts = seniorities.map(s => jobs.filter(j => j.seniority === s).length);
32
+
33
+ const barData = {
34
+ labels,
35
+ datasets: [
36
+ {
37
+ label: "Applicants",
38
+ data: applicantsData,
39
+ backgroundColor: "rgba(34,197,94,0.8)",
40
+ },
41
+ ],
42
+ };
43
+
44
+ const doughnutData = {
45
+ labels: seniorities,
46
+ datasets: [
47
+ {
48
+ data: seniorityCounts,
49
+ backgroundColor: ["#60A5FA", "#F59E0B", "#A78BFA", "#FB7185"],
50
+ },
51
+ ],
52
+ };
53
+
54
+ if (loading) {
55
+ return <div className="h-64 flex items-center justify-center text-gray-500">Loading charts…</div>;
56
+ }
57
+
58
+ if (jobs.length === 0) {
59
+ return <div className="h-64 flex items-center justify-center text-gray-500">No data to display.</div>;
60
+ }
61
+
62
+ return (
63
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
64
+ <div className="h-64">
65
+ <Bar data={barData} options={{ responsive: true, maintainAspectRatio: false }} />
66
+ </div>
67
+ <div className="h-64">
68
+ <Doughnut data={doughnutData} options={{ responsive: true, maintainAspectRatio: false }} />
69
+ </div>
70
+ </div>
71
+ );
72
+ }
frontend/app/routes.ts CHANGED
@@ -5,4 +5,5 @@ export default [
5
  route("jobs", "routes/jobs.tsx"),
6
  route("jobs/:id", "routes/jobs.$id.tsx"),
7
  route("jobs/:jid/assessments/:id", "routes/jobs.$jid.assessments.$id.tsx"),
 
8
  ] satisfies RouteConfig;
 
5
  route("jobs", "routes/jobs.tsx"),
6
  route("jobs/:id", "routes/jobs.$id.tsx"),
7
  route("jobs/:jid/assessments/:id", "routes/jobs.$jid.assessments.$id.tsx"),
8
+ route("dashboard", "routes/dashboard.tsx"),
9
  ] satisfies RouteConfig;
frontend/app/routes/dashboard.tsx ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo } from "react";
2
+ import type { Route } from "./+types/dashboard";
3
+ import { useGetJobs } from "~/services/useGetJobs";
4
+ import OverviewCharts from "~/components/overview-charts";
5
+
6
+ export function meta({}: Route.MetaArgs) {
7
+ return [
8
+ { title: "Dashboard" },
9
+ {
10
+ name: "description",
11
+ content: "Overview of platform metrics and insights to track your hiring process.",
12
+ },
13
+ ];
14
+ }
15
+
16
+ export default function Dashboard() {
17
+ const { data, isLoading, isError } = useGetJobs();
18
+ const jobs = data?.data ?? [];
19
+
20
+ const totals = useMemo(() => {
21
+ const totalJobs = jobs.length;
22
+ const totalApplicants = jobs.reduce((s, j) => s + (j.applicants_count ?? 0), 0);
23
+ const activeJobs = jobs.filter(j => j.active).length;
24
+ return { totalJobs, totalApplicants, activeJobs };
25
+ }, [jobs]);
26
+
27
+ return (
28
+ <div className="p-6 space-y-6">
29
+ <header className="flex items-center justify-between">
30
+ <h1 className="text-2xl font-semibold">Dashboard</h1>
31
+ <p className="text-sm text-muted-foreground">Overview and quick insights</p>
32
+ </header>
33
+
34
+ <section className="grid grid-cols-1 sm:grid-cols-3 gap-4">
35
+ <div className="p-4 bg-white dark:bg-gray-800 shadow rounded">
36
+ <div className="text-sm text-gray-500 dark:text-gray-200">Total Jobs</div>
37
+ <div className="mt-2 text-3xl font-bold">{isLoading ? "…" : totals.totalJobs}</div>
38
+ </div>
39
+ <div className="p-4 bg-white dark:bg-gray-800 shadow rounded">
40
+ <div className="text-sm text-gray-500 dark:text-gray-200">Total Applicants</div>
41
+ <div className="mt-2 text-3xl font-bold">{isLoading ? "…" : totals.totalApplicants}</div>
42
+ </div>
43
+ <div className="p-4 bg-white dark:bg-gray-800 shadow rounded">
44
+ <div className="text-sm text-gray-500 dark:text-gray-200">Active Jobs</div>
45
+ <div className="mt-2 text-3xl font-bold">{isLoading ? "…" : totals.activeJobs}</div>
46
+ </div>
47
+ </section>
48
+
49
+ <section className="grid grid-cols-1 lg:grid-cols-2 gap-6">
50
+ <div className="p-4 bg-white dark:bg-gray-800 shadow rounded">
51
+ <h2 className="text-lg font-medium mb-4">Applicants Per Job</h2>
52
+ <OverviewCharts jobs={jobs} loading={isLoading} />
53
+ </div>
54
+ <div className="p-4 bg-white dark:bg-gray-800 shadow rounded">
55
+ <h2 className="text-lg font-medium mb-4">Quick Insights</h2>
56
+ {isError && <div className="text-red-500">Error loading data</div>}
57
+ {!isLoading && jobs.length === 0 && <div className="text-sm text-gray-600 dark:text-gray-200">No jobs yet.</div>}
58
+ {!isLoading && jobs.length > 0 && (
59
+ <ul className="space-y-3">
60
+ <li className="flex justify-between">
61
+ <span className="text-sm text-gray-700 dark:text-gray-200">Top job (by applicants)</span>
62
+ <span className="font-medium">
63
+ {jobs.slice().sort((a,b) => (b.applicants_count||0)-(a.applicants_count||0))[0]?.title ?? "—"}
64
+ </span>
65
+ </li>
66
+ <li className="flex justify-between">
67
+ <span className="text-sm text-gray-700 dark:text-gray-200">Most common seniority</span>
68
+ <span className="font-medium">
69
+ {(() => {
70
+ const counts = jobs.reduce<Record<string,number>>((acc, j) => {
71
+ acc[j.seniority] = (acc[j.seniority] || 0) + 1;
72
+ return acc;
73
+ }, {});
74
+ const entries = Object.entries(counts);
75
+ if (!entries.length) return "—";
76
+ return entries.sort((a,b) => b[1]-a[1])[0][0];
77
+ })()}
78
+ </span>
79
+ </li>
80
+ <li className="flex justify-between">
81
+ <span className="text-sm text-gray-700 dark:text-gray-200">Jobs with no applicants</span>
82
+ <span className="font-medium">{jobs.filter(j => !j.applicants_count).length}</span>
83
+ </li>
84
+ </ul>
85
+ )}
86
+ </div>
87
+ </section>
88
+ </div>
89
+ );
90
+ }
frontend/package-lock.json CHANGED
@@ -11,12 +11,14 @@
11
  "@react-router/serve": "7.12.0",
12
  "@tanstack/react-query": "^4.43.0",
13
  "axios": "^1.13.4",
 
14
  "class-variance-authority": "^0.7.1",
15
  "clsx": "^2.1.1",
16
  "isbot": "^5.1.31",
17
  "lucide-react": "^0.563.0",
18
  "radix-ui": "^1.4.3",
19
  "react": "^19.2.4",
 
20
  "react-dom": "^19.2.4",
21
  "react-router": "7.12.0",
22
  "react-toastify": "^11.0.5",
@@ -1091,6 +1093,12 @@
1091
  "@jridgewell/sourcemap-codec": "^1.4.14"
1092
  }
1093
  },
 
 
 
 
 
 
1094
  "node_modules/@mjackson/node-fetch-server": {
1095
  "version": "0.2.0",
1096
  "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz",
@@ -3688,6 +3696,18 @@
3688
  ],
3689
  "license": "CC-BY-4.0"
3690
  },
 
 
 
 
 
 
 
 
 
 
 
 
3691
  "node_modules/chokidar": {
3692
  "version": "4.0.3",
3693
  "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -5282,6 +5302,16 @@
5282
  "node": ">=0.10.0"
5283
  }
5284
  },
 
 
 
 
 
 
 
 
 
 
5285
  "node_modules/react-dom": {
5286
  "version": "19.2.4",
5287
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
 
11
  "@react-router/serve": "7.12.0",
12
  "@tanstack/react-query": "^4.43.0",
13
  "axios": "^1.13.4",
14
+ "chart.js": "^4.5.1",
15
  "class-variance-authority": "^0.7.1",
16
  "clsx": "^2.1.1",
17
  "isbot": "^5.1.31",
18
  "lucide-react": "^0.563.0",
19
  "radix-ui": "^1.4.3",
20
  "react": "^19.2.4",
21
+ "react-chartjs-2": "^5.3.1",
22
  "react-dom": "^19.2.4",
23
  "react-router": "7.12.0",
24
  "react-toastify": "^11.0.5",
 
1093
  "@jridgewell/sourcemap-codec": "^1.4.14"
1094
  }
1095
  },
1096
+ "node_modules/@kurkle/color": {
1097
+ "version": "0.3.4",
1098
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
1099
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
1100
+ "license": "MIT"
1101
+ },
1102
  "node_modules/@mjackson/node-fetch-server": {
1103
  "version": "0.2.0",
1104
  "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz",
 
3696
  ],
3697
  "license": "CC-BY-4.0"
3698
  },
3699
+ "node_modules/chart.js": {
3700
+ "version": "4.5.1",
3701
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
3702
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
3703
+ "license": "MIT",
3704
+ "dependencies": {
3705
+ "@kurkle/color": "^0.3.0"
3706
+ },
3707
+ "engines": {
3708
+ "pnpm": ">=8"
3709
+ }
3710
+ },
3711
  "node_modules/chokidar": {
3712
  "version": "4.0.3",
3713
  "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
 
5302
  "node": ">=0.10.0"
5303
  }
5304
  },
5305
+ "node_modules/react-chartjs-2": {
5306
+ "version": "5.3.1",
5307
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz",
5308
+ "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==",
5309
+ "license": "MIT",
5310
+ "peerDependencies": {
5311
+ "chart.js": "^4.1.1",
5312
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
5313
+ }
5314
+ },
5315
  "node_modules/react-dom": {
5316
  "version": "19.2.4",
5317
  "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
frontend/package.json CHANGED
@@ -14,12 +14,14 @@
14
  "@react-router/serve": "7.12.0",
15
  "@tanstack/react-query": "^4.43.0",
16
  "axios": "^1.13.4",
 
17
  "class-variance-authority": "^0.7.1",
18
  "clsx": "^2.1.1",
19
  "isbot": "^5.1.31",
20
  "lucide-react": "^0.563.0",
21
  "radix-ui": "^1.4.3",
22
  "react": "^19.2.4",
 
23
  "react-dom": "^19.2.4",
24
  "react-router": "7.12.0",
25
  "react-toastify": "^11.0.5",
 
14
  "@react-router/serve": "7.12.0",
15
  "@tanstack/react-query": "^4.43.0",
16
  "axios": "^1.13.4",
17
+ "chart.js": "^4.5.1",
18
  "class-variance-authority": "^0.7.1",
19
  "clsx": "^2.1.1",
20
  "isbot": "^5.1.31",
21
  "lucide-react": "^0.563.0",
22
  "radix-ui": "^1.4.3",
23
  "react": "^19.2.4",
24
+ "react-chartjs-2": "^5.3.1",
25
  "react-dom": "^19.2.4",
26
  "react-router": "7.12.0",
27
  "react-toastify": "^11.0.5",