AshameTheDestroyer commited on
Commit
807aefa
·
1 Parent(s): ab5962a

View My Applications.

Browse files
frontend/app/components/application-card.tsx CHANGED
@@ -1,18 +1,22 @@
1
  import { cn } from "~/lib/utils";
2
  import { Avatar } from "radix-ui";
3
  import { useNavigate } from "react-router";
 
4
  import type { Application } from "~/services/useGetJobAssessmentApplications";
5
 
6
- export function ApplicationCard({ application, aid, jid, isStatic = false } : { application: Application, jid: string, aid: string, isStatic?: boolean }) {
7
  const Navigate = useNavigate();
8
 
9
  return (
10
  <div
11
  tabIndex={isStatic ? -1 : 0}
12
  className={cn("p-4 flex flex-wrap justify-between gap-4 place-items-center", isStatic ? "" : "border rounded bg-indigo-100 dark:bg-gray-700 [:is(:hover,:focus)]:shadow-lg [:is(:hover,:focus)]:scale-101 transition-all cursor-pointer")}
13
- onClick={() => isStatic || Navigate(`/jobs/${jid}/assessments/${aid}/applications/${application.id}`)}
14
  >
15
- <h1 className={cn("grow font-bold w-full", isStatic ? "text-3xl" : "text-xl")}>{application.assessment_details.title}</h1>
 
 
 
16
  <div className="group-data-[collapsible=icon]:-mx-4 flex gap-2">
17
  <Avatar.Avatar className="shrink-0 cursor-pointer" tabIndex={0}>
18
  <Avatar.AvatarFallback className="rounded-full bg-indigo-200 dark:bg-gray-800 size-10 group-data-[collapsible=icon]:size-8 flex items-center justify-center">
 
1
  import { cn } from "~/lib/utils";
2
  import { Avatar } from "radix-ui";
3
  import { useNavigate } from "react-router";
4
+ import type { MyApplication } from "~/services/useGetMyApplications";
5
  import type { Application } from "~/services/useGetJobAssessmentApplications";
6
 
7
+ export function ApplicationCard({ application, aid, jid, isStatic = false, safeRoute = false } : { application: Application & { job?: MyApplication["job"] }, jid: string, aid: string, isStatic?: boolean, safeRoute?: boolean }) {
8
  const Navigate = useNavigate();
9
 
10
  return (
11
  <div
12
  tabIndex={isStatic ? -1 : 0}
13
  className={cn("p-4 flex flex-wrap justify-between gap-4 place-items-center", isStatic ? "" : "border rounded bg-indigo-100 dark:bg-gray-700 [:is(:hover,:focus)]:shadow-lg [:is(:hover,:focus)]:scale-101 transition-all cursor-pointer")}
14
+ onClick={() => isStatic || Navigate(safeRoute ? `/my-applications/${application.id}` : `/jobs/${jid}/assessments/${aid}/applications/${application.id}`)}
15
  >
16
+ <header className="flex flex-col gap-2 w-full grow">
17
+ <h1 className={cn("font-bold", isStatic ? "text-3xl" : "text-xl")}>{application.assessment_details.title}</h1>
18
+ {application.job && <p className="text-gray-500 dark:text-gray-200">{application.job.title}</p>}
19
+ </header>
20
  <div className="group-data-[collapsible=icon]:-mx-4 flex gap-2">
21
  <Avatar.Avatar className="shrink-0 cursor-pointer" tabIndex={0}>
22
  <Avatar.AvatarFallback className="rounded-full bg-indigo-200 dark:bg-gray-800 size-10 group-data-[collapsible=icon]:size-8 flex items-center justify-center">
frontend/app/components/sidebar-provider.tsx CHANGED
@@ -4,13 +4,14 @@ import { toast } from "react-toastify";
4
  import { Link, useLocation } from "react-router";
5
  import { HTTPManager } from "~/managers/HTTPManager";
6
  import { useGetMyUser } from "~/services/useGetMyUser";
7
- import { Building2Icon, LayoutDashboardIcon } from "lucide-react";
8
  import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
 
9
  import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarInset, SidebarProvider as SidebarProvider_ } from "./ui/sidebar";
10
 
11
  const LINKS = [
12
  { title: "Jobs", to: "/jobs", icon: Building2Icon },
13
  { title: "Dashboard", to: "/dashboard", icon: LayoutDashboardIcon, role: "hr" },
 
14
  ]
15
 
16
  export function SidebarProvider({ children }: { children: React.ReactNode }) {
 
4
  import { Link, useLocation } from "react-router";
5
  import { HTTPManager } from "~/managers/HTTPManager";
6
  import { useGetMyUser } from "~/services/useGetMyUser";
 
7
  import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
8
+ import { Building2Icon, FilesIcon, LayoutDashboardIcon } from "lucide-react";
9
  import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarInset, SidebarProvider as SidebarProvider_ } from "./ui/sidebar";
10
 
11
  const LINKS = [
12
  { title: "Jobs", to: "/jobs", icon: Building2Icon },
13
  { title: "Dashboard", to: "/dashboard", icon: LayoutDashboardIcon, role: "hr" },
14
+ { title: "My Applications", to: "/my-applications", icon: FilesIcon, role: "applicant" },
15
  ]
16
 
17
  export function SidebarProvider({ children }: { children: React.ReactNode }) {
frontend/app/routes.ts CHANGED
@@ -9,6 +9,8 @@ export default [
9
  route("jobs/:jid/assessments/:id", "routes/jobs.$jid.assessments.$id.tsx"),
10
  route("jobs/:jid/assessments/:aid/applications", "routes/jobs.$jid.assessments.$aid.applications.tsx"),
11
  route("jobs/:jid/assessments/:aid/applications/:id", "routes/jobs.$jid.assessments.$aid.applications.$id.tsx"),
 
 
12
  route("dashboard", "routes/dashboard.tsx"),
13
  route("registration", "routes/registration.tsx"),
14
  ] satisfies RouteConfig;
 
9
  route("jobs/:jid/assessments/:id", "routes/jobs.$jid.assessments.$id.tsx"),
10
  route("jobs/:jid/assessments/:aid/applications", "routes/jobs.$jid.assessments.$aid.applications.tsx"),
11
  route("jobs/:jid/assessments/:aid/applications/:id", "routes/jobs.$jid.assessments.$aid.applications.$id.tsx"),
12
+ route("my-applications", "routes/my-applications.tsx"),
13
+ route("my-applications/:id", "routes/my-applications.$id.tsx"),
14
  route("dashboard", "routes/dashboard.tsx"),
15
  route("registration", "routes/registration.tsx"),
16
  ] satisfies RouteConfig;
frontend/app/routes/jobs.$jid.assessments.$aid.applications.$id.tsx CHANGED
@@ -75,5 +75,5 @@ export default function ApplicationDetailsRoute() {
75
  />
76
  ))}
77
  </main>
78
- )
79
  }
 
75
  />
76
  ))}
77
  </main>
78
+ );
79
  }
frontend/app/routes/my-applications.$id.tsx ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useParams } from "react-router";
2
+ import { Loader2Icon } from "lucide-react";
3
+ import type { Route } from "./+types/my-applications";
4
+ import { QuestionCard } from "~/components/question-card";
5
+ import { ApplicationCard } from "~/components/application-card";
6
+ import { useGetMyApplicationByID } from "~/services/useGetMyApplicationByID";
7
+ import type { DetailedApplication } from "~/services/useGetJobAssessmentApplicationByID";
8
+
9
+ export function meta({}: Route.MetaArgs) {
10
+ return [
11
+ { title: "My Application Details" },
12
+ {
13
+ name: "description",
14
+ content: "Detailed view of the selected application, including candidate information and assessment results.",
15
+ },
16
+ ];
17
+ }
18
+
19
+ export default function MyApplicationDetailsRoute() {
20
+ const { id } = useParams();
21
+ const { data: application, isLoading, isError, refetch } = useGetMyApplicationByID({ id: id || "" });
22
+
23
+ if (isLoading) {
24
+ return (
25
+ <main className="container mx-auto p-4 flex flex-col gap-2 place-items-center">
26
+ <div className="flex flex-col gap-2 place-items-center">
27
+ <Loader2Icon className="animate-spin" />
28
+ <p>Loading application...</p>
29
+ </div>
30
+ </main>
31
+ );
32
+ }
33
+
34
+ if (isError) {
35
+ return (
36
+ <main className="container mx-auto p-4 flex flex-col gap-2">
37
+ <div className="bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-100 p-4 rounded flex flex-col gap-2 place-items-center">
38
+ <p className="text-center">Failed to load application<br />Please try again</p>
39
+ <button
40
+ onClick={() => refetch()}
41
+ className="ml-4 px-3 py-1 cursor-pointer bg-red-500 text-white dark:bg-red-200 dark:text-red-700 rounded"
42
+ >
43
+ Retry
44
+ </button>
45
+ </div>
46
+ </main>
47
+ );
48
+ }
49
+
50
+ const totalWeights = application.answers.reduce((weights, answer) => weights + answer.weight, 0);
51
+
52
+ return (
53
+ <main className="container mx-auto p-4 flex flex-col gap-4">
54
+ <ApplicationCard application={application} aid={application.assessment.id || ""} jid={application.job.id || ""} isStatic />
55
+ {application.answers.map((answer: DetailedApplication["answers"][number]) => (
56
+ <QuestionCard
57
+ key={answer.question_id}
58
+ isStatic
59
+ displayCheckboxMessage
60
+ question={{
61
+ type: answer.type,
62
+ weight: answer.weight,
63
+ id: answer.question_id,
64
+ text: answer.question_text,
65
+ options: answer.question_options,
66
+ correct_options: answer.correct_options,
67
+ skill_categories: answer.skill_categories,
68
+ }}
69
+ answers={application.answers.reduce((accumulator, current) => ({
70
+ ...accumulator,
71
+ [current.question_id]: current.type == "text_based" ? current.text : current.type == "choose_one" ? current.options : current.options,
72
+ }), {})}
73
+ setAnswers={() => {}}
74
+ totalWeights={totalWeights}
75
+ rationale={answer.rationale}
76
+ />
77
+ ))}
78
+ </main>
79
+ );
80
+ }
frontend/app/routes/my-applications.tsx ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Loader2Icon } from "lucide-react";
2
+ import { Paginator } from "~/components/paginator";
3
+ import type { Route } from "./+types/my-applications";
4
+ import { useGetMyUser } from "~/services/useGetMyUser";
5
+ import { ApplicationCard } from "~/components/application-card";
6
+ import { useGetMyApplications } from "~/services/useGetMyApplications";
7
+
8
+ export function meta({}: Route.MetaArgs) {
9
+ return [
10
+ { title: "My Applications" },
11
+ {
12
+ name: "description",
13
+ content: "View and manage your job applications, track their status, and review feedback from assessments.",
14
+ },
15
+ ];
16
+ }
17
+
18
+ export default function MyApplicationsRoute() {
19
+ const { data: myUser, isLoading: isMyUserLoading, isError: isMyUserError, refetch: refetchMyUser } = useGetMyUser();
20
+ const { data: { data: applications, total } = { data: [] }, isLoading: isApplicationsLoading, isError: isApplicationsError, refetch: refetchApplications } = useGetMyApplications();
21
+
22
+ const isError = isMyUserError || isApplicationsError;
23
+ const isLoading = isMyUserLoading || isApplicationsLoading;
24
+ const refetch = () => (refetchMyUser(), refetchApplications());
25
+
26
+ if (isLoading) {
27
+ return (
28
+ <main className="container mx-auto p-4 flex flex-col gap-2 place-items-center">
29
+ <div className="flex flex-col gap-2 place-items-center">
30
+ <Loader2Icon className="animate-spin" />
31
+ <p>Loading Applications...</p>
32
+ </div>
33
+ </main>
34
+ );
35
+ }
36
+
37
+ if (isError) {
38
+ return (
39
+ <main className="container mx-auto p-4 flex flex-col gap-2">
40
+ <div className="bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-100 p-4 rounded flex flex-col gap-2 place-items-center">
41
+ <p className="text-center">Failed to load applications<br />Please try again</p>
42
+ <button
43
+ onClick={() => refetch()}
44
+ className="ml-4 px-3 py-1 cursor-pointer bg-red-500 text-white dark:bg-red-200 dark:text-red-700 rounded"
45
+ >
46
+ Retry
47
+ </button>
48
+ </div>
49
+ </main>
50
+ );
51
+ }
52
+
53
+
54
+ return (
55
+ <main className="container mx-auto p-4 flex flex-col gap-8">
56
+ <section className="flex flex-col gap-4">
57
+ <h3 className="text-xl font-semibold">Assessment's Applications</h3>
58
+ <div className="grid grid-cols-[repeat(auto-fill,minmax(400px,1fr))] gap-4">
59
+ {applications.length === 0 ? (
60
+ <p>No applications found.<br/>Start applying now!</p>
61
+ ) : applications.map(application => (
62
+ <ApplicationCard key={application.id} application={{...application, user: myUser, passing_score: application.assessment.passing_score, assessment_details: application.assessment}} jid={application.job.id || ""} aid={application.assessment.id || ""} safeRoute />
63
+ ))}
64
+ </div>
65
+ {total != null && total > 0 && <Paginator total={total} />}
66
+ </section>
67
+ </main>
68
+ );
69
+ }
frontend/app/services/useGetJobAssessmentApplicationByID.ts CHANGED
@@ -32,7 +32,7 @@ export type DetailedApplication = {
32
  };
33
 
34
  export const useGetJobAssessmentApplicationByID = ({ jid, aid, id }: { jid: string, aid: string, id: string }) => useQuery({
35
- queryKey: [GET_JOB_ASSESSMENT_APPLICATION_BY_ID_KEY, jid, id],
36
  queryFn: async () =>
37
  HTTPManager.get<DetailedApplication>(
38
  `/applications/jobs/${jid}/assessment_id/${aid}/applications/${id}`,
 
32
  };
33
 
34
  export const useGetJobAssessmentApplicationByID = ({ jid, aid, id }: { jid: string, aid: string, id: string }) => useQuery({
35
+ queryKey: [GET_JOB_ASSESSMENT_APPLICATION_BY_ID_KEY, jid, aid, id],
36
  queryFn: async () =>
37
  HTTPManager.get<DetailedApplication>(
38
  `/applications/jobs/${jid}/assessment_id/${aid}/applications/${id}`,
frontend/app/services/useGetJobAssessmentApplications.ts CHANGED
@@ -4,7 +4,7 @@ import { HTTPManager } from "~/managers/HTTPManager";
4
  import type { Pagination } from "~/types/pagination";
5
  import { usePagination } from "~/hooks/use-pagination";
6
 
7
- export const GET_JOB_ASSESSMENT_APPLICATION_KEY = "job-assessments-application";
8
 
9
  export type Application = {
10
  id: string;
 
4
  import type { Pagination } from "~/types/pagination";
5
  import { usePagination } from "~/hooks/use-pagination";
6
 
7
+ export const GET_JOB_ASSESSMENT_APPLICATION_KEY = "job-assessments-applications";
8
 
9
  export type Application = {
10
  id: string;
frontend/app/services/useGetMyApplicationByID.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { HTTPManager } from "~/managers/HTTPManager";
3
+ import type { MyApplication } from "./useGetMyApplications";
4
+ import type { DetailedApplication } from "./useGetJobAssessmentApplicationByID";
5
+
6
+ export const GET_JOB_ASSESSMENT_APPLICATION_BY_ID_KEY = "job-assessment-application-by-id";
7
+
8
+ export type DetailedMyApplication = MyApplication & DetailedApplication;
9
+
10
+ export const useGetMyApplicationByID = ({ id }: { id: string }) => useQuery({
11
+ queryKey: [GET_JOB_ASSESSMENT_APPLICATION_BY_ID_KEY, id],
12
+ queryFn: async () =>
13
+ HTTPManager.get<DetailedMyApplication>(`/applications/my-applications/${id}`).then((response) => response.data),
14
+ });
frontend/app/services/useGetMyApplications.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { HTTPManager } from "~/managers/HTTPManager";
3
+ import type { Pagination } from "~/types/pagination";
4
+ import { usePagination } from "~/hooks/use-pagination";
5
+
6
+ export const GET_MY_APPLICATIONS_KEY = "job-my-applications";
7
+
8
+ export type MyApplication = {
9
+ id: string;
10
+ score: number;
11
+ job: {
12
+ id: string;
13
+ title: string;
14
+ description: string;
15
+ seniority: "intern" | "junior" | "mid" | "senior";
16
+ };
17
+ assessment: {
18
+ id: string;
19
+ title: string;
20
+ passing_score: number;
21
+ };
22
+ }
23
+
24
+ export const useGetMyApplications = () => {
25
+ const { page, limit } = usePagination();
26
+ const searchParams = new URLSearchParams({ page: String(page), limit: String(limit) });
27
+ return useQuery({
28
+ queryKey: [GET_MY_APPLICATIONS_KEY, page, limit],
29
+ queryFn: async () => HTTPManager.get<Pagination<MyApplication>>(`/applications/my-applications?` + searchParams.toString()).then(response => response.data),
30
+ });
31
+ }