Commit ·
5441d4f
1
Parent(s): 88cf2ba
Questions Display.
Browse files- frontend/app/components/assessment-card.tsx +22 -19
- frontend/app/components/paginator.tsx +5 -5
- frontend/app/components/ui/checkbox.tsx +30 -0
- frontend/app/components/ui/radio-group.tsx +43 -0
- frontend/app/routes.ts +1 -0
- frontend/app/routes/jobs.$id.tsx +12 -1
- frontend/app/routes/jobs.$jid.assessments.$id.tsx +97 -0
- frontend/app/services/useGetJobAssessmentByID.ts +12 -0
- frontend/app/services/useGetJobsByID.ts +2 -2
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("
|
| 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 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 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
|
| 6 |
|
| 7 |
export const useGetJobByID = ({ id }: { id: string }) => {
|
| 8 |
return useQuery({
|
| 9 |
-
queryKey: [
|
| 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 |
}
|