AshameTheDestroyer commited on
Commit
5441d4f
·
1 Parent(s): 88cf2ba

Questions Display.

Browse files
frontend/app/components/assessment-card.tsx CHANGED
@@ -3,36 +3,22 @@ import { useNavigate } from "react-router";
3
  import type { Assessment } from "~/services/useGetJobAssessments";
4
  import { BadgeQuestionMarkIcon, CircleCheckIcon, CircleDotIcon, HourglassIcon, PercentIcon, TextInitialIcon } from "lucide-react";
5
 
6
- export function AssessmentCard({ assessment, isStatic = false }: { assessment: Assessment, isStatic?: boolean }) {
7
  const Navigate = useNavigate();
8
 
9
  return (
10
  <div
11
  tabIndex={isStatic ? -1 : 0}
12
- className={cn("border p-4 rounded bg-indigo-100 dark:bg-gray-800 flex flex-col gap-4", isStatic ? "" : "[:is(:hover,:focus)]:shadow-lg [:is(:hover,:focus)]:scale-101 transition-all cursor-pointer")}
13
- onClick={() => isStatic || Navigate(`/assessments/${assessment.id}`)}
14
  >
15
- <h4 className="font-semibold">{assessment.title}</h4>
16
  <footer className="flex flex-col gap-2">
17
- <div className="flex flex-wrap gap-2">
18
- <span className="inline-flex gap-2 place-items-center px-3 py-1.5 rounded-xl bg-indigo-50 dark:bg-gray-700">
19
- <HourglassIcon />
20
- <p>{assessment.duration / 60} minutes</p>
21
- </span>
22
- <span className="inline-flex gap-2 place-items-center px-3 py-1.5 rounded-xl bg-indigo-50 dark:bg-gray-700">
23
- <PercentIcon />
24
- <p>{assessment.passing_score} passing score</p>
25
- </span>
26
- <span className="inline-flex gap-2 place-items-center px-3 py-1.5 rounded-xl bg-indigo-50 dark:bg-gray-700">
27
- <BadgeQuestionMarkIcon />
28
- <p>{assessment.questions_count} questions</p>
29
- </span>
30
- </div>
31
  <div className="grow flex flex-col gap-2 mt-2">
32
  <h5 className="font-semibold">Question Types</h5>
33
  <div className="grow flex flex-wrap gap-2">
34
  {[...new Set(assessment.questions.map(question => question.type))].map((type, i) => (
35
- <span className="inline-flex gap-2 place-items-center px-3 py-1.5 rounded-xl bg-indigo-50 dark:bg-gray-700">
36
  {{
37
  "text_based": <TextInitialIcon />,
38
  "choose_one": <CircleDotIcon />,
@@ -43,6 +29,23 @@ export function AssessmentCard({ assessment, isStatic = false }: { assessment: A
43
  ))}
44
  </div>
45
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  </footer>
47
  </div>
48
  );
 
3
  import type { Assessment } from "~/services/useGetJobAssessments";
4
  import { BadgeQuestionMarkIcon, CircleCheckIcon, CircleDotIcon, HourglassIcon, PercentIcon, TextInitialIcon } from "lucide-react";
5
 
6
+ export function AssessmentCard({ jid, assessment, isStatic = false }: { jid: string, assessment: Assessment, isStatic?: boolean }) {
7
  const Navigate = useNavigate();
8
 
9
  return (
10
  <div
11
  tabIndex={isStatic ? -1 : 0}
12
+ className={cn("flex flex-col gap-4", isStatic ? "" : "border p-4 rounded bg-indigo-100 dark:bg-gray-800 [:is(:hover,:focus)]:shadow-lg [:is(:hover,:focus)]:scale-101 transition-all cursor-pointer")}
13
+ onClick={() => isStatic || Navigate(`/jobs/${jid}/assessments/${assessment.id}`)}
14
  >
15
+ <h4 className={cn("font-semibold", isStatic ? "text-4xl" : "text-2xl")}>{assessment.title}</h4>
16
  <footer className="flex flex-col gap-2">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  <div className="grow flex flex-col gap-2 mt-2">
18
  <h5 className="font-semibold">Question Types</h5>
19
  <div className="grow flex flex-wrap gap-2">
20
  {[...new Set(assessment.questions.map(question => question.type))].map((type, i) => (
21
+ <span key={i} className="inline-flex gap-2 place-items-center px-3 py-1.5 rounded-xl bg-indigo-50 dark:bg-gray-700">
22
  {{
23
  "text_based": <TextInitialIcon />,
24
  "choose_one": <CircleDotIcon />,
 
29
  ))}
30
  </div>
31
  </div>
32
+ <div className="flex flex-col gap-2">
33
+ <h5 className="font-semibold">Assessment's Details</h5>
34
+ <div className="grow flex flex-wrap gap-2">
35
+ <span className="inline-flex gap-2 place-items-center px-3 py-1.5 rounded-xl bg-indigo-50 dark:bg-gray-700">
36
+ <HourglassIcon />
37
+ <p>{assessment.duration / 60} minutes</p>
38
+ </span>
39
+ <span className="inline-flex gap-2 place-items-center px-3 py-1.5 rounded-xl bg-indigo-50 dark:bg-gray-700">
40
+ <PercentIcon />
41
+ <p>{assessment.passing_score} passing score</p>
42
+ </span>
43
+ <span className="inline-flex gap-2 place-items-center px-3 py-1.5 rounded-xl bg-indigo-50 dark:bg-gray-700">
44
+ <BadgeQuestionMarkIcon />
45
+ <p>{assessment.questions_count} questions</p>
46
+ </span>
47
+ </div>
48
+ </div>
49
  </footer>
50
  </div>
51
  );
frontend/app/components/paginator.tsx CHANGED
@@ -23,11 +23,11 @@ export function Paginator({ total }: { total: number }) {
23
  <ComboboxContent>
24
  <ComboboxEmpty>No items found.</ComboboxEmpty>
25
  <ComboboxList>
26
- {(item) => (
27
- <ComboboxItem key={item} value={item}>
28
- {item}
29
- </ComboboxItem>
30
- )}
31
  </ComboboxList>
32
  </ComboboxContent>
33
  </Combobox>
 
23
  <ComboboxContent>
24
  <ComboboxEmpty>No items found.</ComboboxEmpty>
25
  <ComboboxList>
26
+ {(item) => (
27
+ <ComboboxItem key={item} value={item}>
28
+ {item}
29
+ </ComboboxItem>
30
+ )}
31
  </ComboboxList>
32
  </ComboboxContent>
33
  </Combobox>
frontend/app/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { CheckIcon } from "lucide-react"
3
+ import { Checkbox as CheckboxPrimitive } from "radix-ui"
4
+
5
+ import { cn } from "~/lib/utils"
6
+
7
+ function Checkbox({
8
+ className,
9
+ ...props
10
+ }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
11
+ return (
12
+ <CheckboxPrimitive.Root
13
+ data-slot="checkbox"
14
+ className={cn(
15
+ "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
16
+ className
17
+ )}
18
+ {...props}
19
+ >
20
+ <CheckboxPrimitive.Indicator
21
+ data-slot="checkbox-indicator"
22
+ className="grid place-content-center text-current transition-none"
23
+ >
24
+ <CheckIcon className="size-3.5" />
25
+ </CheckboxPrimitive.Indicator>
26
+ </CheckboxPrimitive.Root>
27
+ )
28
+ }
29
+
30
+ export { Checkbox }
frontend/app/components/ui/radio-group.tsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { CircleIcon } from "lucide-react"
3
+ import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
4
+
5
+ import { cn } from "~/lib/utils"
6
+
7
+ function RadioGroup({
8
+ className,
9
+ ...props
10
+ }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
11
+ return (
12
+ <RadioGroupPrimitive.Root
13
+ data-slot="radio-group"
14
+ className={cn("grid gap-3", className)}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ function RadioGroupItem({
21
+ className,
22
+ ...props
23
+ }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
24
+ return (
25
+ <RadioGroupPrimitive.Item
26
+ data-slot="radio-group-item"
27
+ className={cn(
28
+ "border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
29
+ className
30
+ )}
31
+ {...props}
32
+ >
33
+ <RadioGroupPrimitive.Indicator
34
+ data-slot="radio-group-indicator"
35
+ className="relative flex items-center justify-center"
36
+ >
37
+ <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
38
+ </RadioGroupPrimitive.Indicator>
39
+ </RadioGroupPrimitive.Item>
40
+ )
41
+ }
42
+
43
+ export { RadioGroup, RadioGroupItem }
frontend/app/routes.ts CHANGED
@@ -4,4 +4,5 @@ export default [
4
  index("routes/home.tsx"),
5
  route("jobs", "routes/jobs.tsx"),
6
  route("jobs/:id", "routes/jobs.$id.tsx"),
 
7
  ] satisfies RouteConfig;
 
4
  index("routes/home.tsx"),
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;
frontend/app/routes/jobs.$id.tsx CHANGED
@@ -1,11 +1,22 @@
1
  import { useParams } from "react-router";
2
  import {Loader2Icon, } from "lucide-react";
 
3
  import { JobCard } from "~/components/job-card";
4
  import { Paginator } from "~/components/paginator";
5
  import { useGetJobByID } from "~/services/useGetJobsByID";
6
  import { AssessmentCard } from "~/components/assessment-card";
7
  import { useGetJobAssessments } from "~/services/useGetJobAssessments";
8
 
 
 
 
 
 
 
 
 
 
 
9
  export default function JobDetailRoute() {
10
  const { id } = useParams();
11
  const { data: job, isLoading: isJobLoading, isError: isJobError, refetch: refetchJob } = useGetJobByID({ id: id || "" });
@@ -47,7 +58,7 @@ export default function JobDetailRoute() {
47
  <JobCard job={job} isStatic />
48
  <section className="flex flex-col gap-4">
49
  <h3 className="text-xl font-semibold">Job's Assessments</h3>
50
- {assessments?.map(assessment => <AssessmentCard assessment={assessment} />)}
51
  {total && <Paginator total={total} />}
52
  </section>
53
  </main>
 
1
  import { useParams } from "react-router";
2
  import {Loader2Icon, } from "lucide-react";
3
+ import type { Route } from "./+types/jobs.$id";
4
  import { JobCard } from "~/components/job-card";
5
  import { Paginator } from "~/components/paginator";
6
  import { useGetJobByID } from "~/services/useGetJobsByID";
7
  import { AssessmentCard } from "~/components/assessment-card";
8
  import { useGetJobAssessments } from "~/services/useGetJobAssessments";
9
 
10
+ export function meta({}: Route.MetaArgs) {
11
+ return [
12
+ { title: "Job Details" },
13
+ {
14
+ name: "description",
15
+ content: "Detailed view of the selected job and its assessments.",
16
+ },
17
+ ];
18
+ }
19
+
20
  export default function JobDetailRoute() {
21
  const { id } = useParams();
22
  const { data: job, isLoading: isJobLoading, isError: isJobError, refetch: refetchJob } = useGetJobByID({ id: id || "" });
 
58
  <JobCard job={job} isStatic />
59
  <section className="flex flex-col gap-4">
60
  <h3 className="text-xl font-semibold">Job's Assessments</h3>
61
+ {assessments?.map(assessment => <AssessmentCard assessment={assessment} jid={job.id} />)}
62
  {total && <Paginator total={total} />}
63
  </section>
64
  </main>
frontend/app/routes/jobs.$jid.assessments.$id.tsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useParams } from "react-router";
2
+ import { Loader2Icon } from "lucide-react";
3
+ import { Label, RadioGroup } from "radix-ui";
4
+ import { Textarea } from "~/components/ui/textarea";
5
+ import { Checkbox } from "~/components/ui/checkbox";
6
+ import { RadioGroupItem } from "~/components/ui/radio-group";
7
+ import { AssessmentCard } from "~/components/assessment-card";
8
+ import type { Route } from "./+types/jobs.$jid.assessments.$id";
9
+ import { useGetJobAssessmentByID } from "~/services/useGetJobAssessmentByID";
10
+
11
+ export function meta({}: Route.MetaArgs) {
12
+ return [
13
+ { title: "Assessment Details" },
14
+ {
15
+ name: "description",
16
+ content: "Detailed view of the selected assessment.",
17
+ },
18
+ ];
19
+ }
20
+
21
+ export default function AssessmentDetailRoute() {
22
+ const { jid, id } = useParams();
23
+ const { data: assessment, isLoading, isError, refetch } = useGetJobAssessmentByID({ jid: jid || "", id: id || "" });
24
+
25
+ if (isLoading) {
26
+ return (
27
+ <main className="container mx-auto p-4 flex flex-col gap-2 place-items-center">
28
+ <div className="flex flex-col gap-2 place-items-center">
29
+ <Loader2Icon className="animate-spin" />
30
+ <p>Loading job...</p>
31
+ </div>
32
+ </main>
33
+ );
34
+ }
35
+
36
+ if (isError) {
37
+ return (
38
+ <main className="container mx-auto p-4 flex flex-col gap-2">
39
+ <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">
40
+ <p className="text-center">Failed to load job<br />Please try again</p>
41
+ <button
42
+ onClick={() => refetch()}
43
+ className="ml-4 px-3 py-1 cursor-pointer bg-red-500 text-white dark:bg-red-200 dark:text-red-700 rounded"
44
+ >
45
+ Retry
46
+ </button>
47
+ </div>
48
+ </main>
49
+ );
50
+ }
51
+
52
+ const totalWeights = assessment.questions.reduce((weights, question) => weights + question.weight, 0);
53
+
54
+ return (
55
+ <main className="container mx-auto p-4 flex flex-col gap-8">
56
+ <AssessmentCard jid={jid || ""} assessment={assessment} isStatic />
57
+ <section className="flex flex-col gap-4">
58
+ <h3 className="text-xl font-semibold">Assessment's Questions</h3>
59
+ <div className="flex flex-col gap-4">
60
+ {assessment.questions.map((question) => (
61
+ <div key={question.id} className="border p-4 flex flex-col gap-2 rounded bg-indigo-100 dark:bg-gray-800">
62
+ <header className="flex place-content-between gap-4 place-items-start">
63
+ <h4 className="font-semibold mb-2">{question.text}</h4>
64
+ <span className="inline-flex gap-2 place-items-center px-3 py-1.5 rounded-xl bg-indigo-50 dark:bg-gray-700">
65
+ {totalWeights > 0 ? `~${Math.floor((question.weight / totalWeights) * 100) / 100}` : "0"}
66
+ </span>
67
+ </header>
68
+ {{
69
+ "text_based": <Textarea className="w-full resize-none" placeholder="Answer goes here..." />,
70
+ "choose_one": (
71
+ <RadioGroup.RadioGroup>
72
+ {question.options.map((option, i) => (
73
+ <div key={i} className="flex items-center gap-3">
74
+ <RadioGroupItem value={option.value} id={`${question.id}-option-${i}`} className="cursor-pointer" />
75
+ <Label.Label htmlFor={`${question.id}-option-${i}`} className="cursor-pointer">{option.text}</Label.Label>
76
+ </div>
77
+ ))}
78
+ </RadioGroup.RadioGroup>
79
+ ),
80
+ "choose_many": (
81
+ <div>
82
+ {question.options.map((option, i) => (
83
+ <div key={i} className="flex items-center gap-3">
84
+ <Checkbox id={`${question.id}-option-${i}`} className="cursor-pointer" />
85
+ <Label.Label htmlFor={`${question.id}-option-${i}`} className="cursor-pointer">{option.text}</Label.Label>
86
+ </div>
87
+ ))}
88
+ </div>
89
+ ),
90
+ }[question.type]}
91
+ </div>
92
+ ))}
93
+ </div>
94
+ </section>
95
+ </main>
96
+ );
97
+ }
frontend/app/services/useGetJobAssessmentByID.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { HTTPManager } from "~/managers/HTTPManager";
3
+ import type { Assessment } from "./useGetJobAssessments";
4
+
5
+ export const GET_JOB_ASSESSMENT_BY_ID_KEY = "job-assessments-by-id";
6
+
7
+ export const useGetJobAssessmentByID = ({ jid, id }: { jid: string, id: string }) => {
8
+ return useQuery({
9
+ queryKey: [GET_JOB_ASSESSMENT_BY_ID_KEY, jid, id],
10
+ queryFn: async () => HTTPManager.get<Assessment>(`/assessments/jobs/${jid}/${id}`).then(response => response.data),
11
+ });
12
+ }
frontend/app/services/useGetJobsByID.ts CHANGED
@@ -2,11 +2,11 @@ import type { Job } from "./useGetJobs";
2
  import { useQuery } from "@tanstack/react-query";
3
  import { HTTPManager } from "~/managers/HTTPManager";
4
 
5
- export const GET_JOBS_BY_ID_KEY = "jobs-by-id";
6
 
7
  export const useGetJobByID = ({ id }: { id: string }) => {
8
  return useQuery({
9
- queryKey: [GET_JOBS_BY_ID_KEY, id],
10
  queryFn: async () => HTTPManager.get<Job>(`/jobs/${id}`).then(response => response.data),
11
  });
12
  }
 
2
  import { useQuery } from "@tanstack/react-query";
3
  import { HTTPManager } from "~/managers/HTTPManager";
4
 
5
+ export const GET_JOB_BY_ID_KEY = "job-by-id";
6
 
7
  export const useGetJobByID = ({ id }: { id: string }) => {
8
  return useQuery({
9
+ queryKey: [GET_JOB_BY_ID_KEY, id],
10
  queryFn: async () => HTTPManager.get<Job>(`/jobs/${id}`).then(response => response.data),
11
  });
12
  }