AshameTheDestroyer commited on
Commit
fdaefd4
·
1 Parent(s): 91b29b1

Logout and Submit.

Browse files
frontend/app/components/sidebar-provider.tsx CHANGED
@@ -1,15 +1,31 @@
1
  import { Avatar } from "radix-ui";
 
 
2
  import { Link, useLocation } from "react-router";
 
 
3
  import { Building2Icon, LayoutDashboardIcon } from "lucide-react";
 
4
  import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarInset, SidebarProvider as SidebarProvider_ } from "./ui/sidebar";
5
 
6
  const LINKS = [
7
  { title: "Jobs", to: "/jobs", icon: Building2Icon },
8
- { title: "Dashboard", to: "/dashboard", icon: LayoutDashboardIcon },
9
  ]
10
 
11
  export function SidebarProvider({ children }: { children: React.ReactNode }) {
12
  const { pathname } = useLocation();
 
 
 
 
 
 
 
 
 
 
 
13
  return (
14
  <SidebarProvider_>
15
  <Sidebar collapsible="icon" className="p-4 bg-indigo-100 dark:bg-slate-950">
@@ -17,7 +33,7 @@ export function SidebarProvider({ children }: { children: React.ReactNode }) {
17
  <h1 className="font-bold text-ellipsis overflow-hidden whitespace-nowrap">Talent Technical Evaluation</h1>
18
  </SidebarHeader>
19
  <SidebarContent>
20
- {LINKS.map(({title,to,icon: Icon}, i) => (
21
  <Link
22
  key={i}
23
  to={to}
@@ -30,26 +46,37 @@ export function SidebarProvider({ children }: { children: React.ReactNode }) {
30
  ))}
31
  </SidebarContent>
32
  <SidebarFooter>
33
- <div className="group-data-[collapsible=icon]:-mx-4 flex gap-2">
34
- <Avatar.Avatar className="shrink-0">
35
- <Avatar.AvatarImage
36
- src="https://github.com/ashamethedestroyer.png"
37
- alt="@ashamethedestroyer"
38
- className="size-10 rounded-full group-data-[collapsible=icon]:size-8"
39
- />
40
- <Avatar.AvatarFallback className="rounded-full bg-gray-200 dark:bg-gray-800 size-10 group-data-[collapsible=icon]:size-8 flex items-center justify-center">
41
- A
42
- </Avatar.AvatarFallback>
43
- </Avatar.Avatar>
44
- <div className="overflow-hidden group-data-[collapsible=icon]:hidden">
45
- <p className="font-bold whitespace-nowrap text-ellipsis overflow-hidden">
46
- Hashem Wannous
47
- </p>
48
- <p className="whitespace-nowrap text-ellipsis overflow-hidden">
49
- @ashamethedestroyer
50
- </p>
 
 
 
 
 
 
 
 
 
 
 
51
  </div>
52
- </div>
53
  </SidebarFooter>
54
  </Sidebar>
55
  <SidebarInset className="flex flex-col h-screen">
 
1
  import { Avatar } from "radix-ui";
2
+ import { Button } from "./ui/button";
3
+ 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 }) {
17
  const { pathname } = useLocation();
18
+ const { data: myUser } = useGetMyUser();
19
+
20
+ function handleLogout() {
21
+ HTTPManager.post("/users/registration/logout", {}).then(() => {
22
+ localStorage.removeItem("token");
23
+ if (typeof window !== "undefined") {
24
+ window.location.replace("/registration");
25
+ }
26
+ }).catch(error => toast.error("Logout failed: " + error?.message || "Unknown error"));
27
+ }
28
+
29
  return (
30
  <SidebarProvider_>
31
  <Sidebar collapsible="icon" className="p-4 bg-indigo-100 dark:bg-slate-950">
 
33
  <h1 className="font-bold text-ellipsis overflow-hidden whitespace-nowrap">Talent Technical Evaluation</h1>
34
  </SidebarHeader>
35
  <SidebarContent>
36
+ {LINKS.filter(link => !link.role || link.role === myUser?.role).map(({title,to,icon: Icon}, i) => (
37
  <Link
38
  key={i}
39
  to={to}
 
46
  ))}
47
  </SidebarContent>
48
  <SidebarFooter>
49
+ {myUser && (
50
+ <div className="group-data-[collapsible=icon]:-mx-4 flex gap-2">
51
+ <Popover>
52
+ <PopoverTrigger>
53
+ <Avatar.Avatar className="shrink-0 cursor-pointer" tabIndex={0}>
54
+ {/* <Avatar.AvatarImage
55
+ src="https://github.com/ashamethedestroyer.png"
56
+ alt="@ashamethedestroyer"
57
+ className="size-10 rounded-full group-data-[collapsible=icon]:size-8"
58
+ /> */}
59
+ <Avatar.AvatarFallback className="rounded-full bg-gray-200 dark:bg-gray-800 size-10 group-data-[collapsible=icon]:size-8 flex items-center justify-center">
60
+ {myUser ? `${myUser.first_name[0]}${myUser.last_name[0]}` : "U"}
61
+ </Avatar.AvatarFallback>
62
+ </Avatar.Avatar>
63
+ </PopoverTrigger>
64
+ <PopoverContent className="w-min p-0">
65
+ <Button variant="ghost" className="w-full text-left" onClick={handleLogout}>
66
+ Logout
67
+ </Button>
68
+ </PopoverContent>
69
+ </Popover>
70
+ <div className="overflow-hidden group-data-[collapsible=icon]:hidden">
71
+ <p className="font-bold whitespace-nowrap text-ellipsis overflow-hidden text-start">
72
+ {myUser.first_name} {myUser.last_name}
73
+ </p>
74
+ <p className="whitespace-nowrap text-ellipsis overflow-hidden">
75
+ {myUser.email}
76
+ </p>
77
+ </div>
78
  </div>
79
+ )}
80
  </SidebarFooter>
81
  </Sidebar>
82
  <SidebarInset className="flex flex-col h-screen">
frontend/app/components/ui/popover.tsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Popover as PopoverPrimitive } from "radix-ui"
3
+
4
+ import { cn } from "~/lib/utils"
5
+
6
+ function Popover({
7
+ ...props
8
+ }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
9
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />
10
+ }
11
+
12
+ function PopoverTrigger({
13
+ ...props
14
+ }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
15
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
16
+ }
17
+
18
+ function PopoverContent({
19
+ className,
20
+ align = "center",
21
+ sideOffset = 4,
22
+ ...props
23
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
24
+ return (
25
+ <PopoverPrimitive.Portal>
26
+ <PopoverPrimitive.Content
27
+ data-slot="popover-content"
28
+ align={align}
29
+ sideOffset={sideOffset}
30
+ className={cn(
31
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
32
+ className
33
+ )}
34
+ {...props}
35
+ />
36
+ </PopoverPrimitive.Portal>
37
+ )
38
+ }
39
+
40
+ function PopoverAnchor({
41
+ ...props
42
+ }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
43
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
44
+ }
45
+
46
+ function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
47
+ return (
48
+ <div
49
+ data-slot="popover-header"
50
+ className={cn("flex flex-col gap-1 text-sm", className)}
51
+ {...props}
52
+ />
53
+ )
54
+ }
55
+
56
+ function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
57
+ return (
58
+ <div
59
+ data-slot="popover-title"
60
+ className={cn("font-medium", className)}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ function PopoverDescription({
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"p">) {
70
+ return (
71
+ <p
72
+ data-slot="popover-description"
73
+ className={cn("text-muted-foreground", className)}
74
+ {...props}
75
+ />
76
+ )
77
+ }
78
+
79
+ export {
80
+ Popover,
81
+ PopoverTrigger,
82
+ PopoverContent,
83
+ PopoverAnchor,
84
+ PopoverHeader,
85
+ PopoverTitle,
86
+ PopoverDescription,
87
+ }
frontend/app/root.tsx CHANGED
@@ -21,6 +21,10 @@ export const queryClient = new QueryClient();
21
  export function Layout({ children }: { children: React.ReactNode }) {
22
  const { pathname } = useLocation();
23
 
 
 
 
 
24
  return (
25
  <html lang="en">
26
  <head>
 
21
  export function Layout({ children }: { children: React.ReactNode }) {
22
  const { pathname } = useLocation();
23
 
24
+ if (!pathname.startsWith("/registration") && typeof window !== "undefined" && localStorage.getItem("token") === null) {
25
+ window.location.replace("/registration");
26
+ }
27
+
28
  return (
29
  <html lang="en">
30
  <head>
frontend/app/routes/dashboard.tsx CHANGED
@@ -1,6 +1,7 @@
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) {
@@ -17,6 +18,8 @@ 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);
@@ -24,6 +27,15 @@ export default function Dashboard() {
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">
 
1
+ import { useEffect, useMemo } from "react";
2
  import type { Route } from "./+types/dashboard";
3
  import { useGetJobs } from "~/services/useGetJobs";
4
+ import { useGetMyUser } from "~/services/useGetMyUser";
5
  import OverviewCharts from "~/components/overview-charts";
6
 
7
  export function meta({}: Route.MetaArgs) {
 
18
  const { data, isLoading, isError } = useGetJobs();
19
  const jobs = data?.data ?? [];
20
 
21
+ const { data: myUser, isLoading: isMyUserLoading } = useGetMyUser();
22
+
23
  const totals = useMemo(() => {
24
  const totalJobs = jobs.length;
25
  const totalApplicants = jobs.reduce((s, j) => s + (j.applicants_count ?? 0), 0);
 
27
  return { totalJobs, totalApplicants, activeJobs };
28
  }, [jobs]);
29
 
30
+
31
+ useEffect(() => {
32
+ if (myUser && myUser.role != "hr") {
33
+ if (typeof window !== "undefined") {
34
+ window.location.replace("/");
35
+ }
36
+ }
37
+ }, [myUser]);
38
+
39
  return (
40
  <div className="p-6 space-y-6">
41
  <header className="flex items-center justify-between">
frontend/app/routes/jobs.$jid.assessments.$id.tsx CHANGED
@@ -11,6 +11,7 @@ import { AssessmentCard } from "~/components/assessment-card";
11
  import type { Route } from "./+types/jobs.$jid.assessments.$id";
12
  import { useGetJobAssessmentByID } from "~/services/useGetJobAssessmentByID";
13
  import { usePostAssessmentApplication } from "~/services/usePostAssessmentApplication";
 
14
 
15
  export function meta({}: Route.MetaArgs) {
16
  return [
@@ -28,8 +29,18 @@ export default function AssessmentDetailRoute() {
28
  const [answers, setAnswers] = useState({} as Record<string, any>);
29
 
30
  const { mutateAsync: submitAnswers, isPending: isSubmittingLoading } = usePostAssessmentApplication();
 
31
 
32
- if (isLoading) {
 
 
 
 
 
 
 
 
 
33
  return (
34
  <main className="container mx-auto p-4 flex flex-col gap-2 place-items-center">
35
  <div className="flex flex-col gap-2 place-items-center">
@@ -40,7 +51,7 @@ export default function AssessmentDetailRoute() {
40
  );
41
  }
42
 
43
- if (isError) {
44
  return (
45
  <main className="container mx-auto p-4 flex flex-col gap-2">
46
  <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">
@@ -56,22 +67,13 @@ export default function AssessmentDetailRoute() {
56
  );
57
  }
58
 
59
- useEffect(() => {
60
- if (assessment == null) { return }
61
-
62
- setAnswers(assessment.questions.reduce((accumulator, question) => {
63
- accumulator[question.id] = question.type === "choose_many" ? [] : "";
64
- return accumulator;
65
- }, {} as Record<string, any>));
66
- }, [assessment]);
67
-
68
  const totalWeights = assessment.questions.reduce((weights, question) => weights + question.weight, 0);
69
 
70
  function handleSubmit() {
71
  submitAnswers({
72
  job_id: jid || "",
73
  assessment_id: id || "",
74
- user_id: "",
75
  answers: Object.entries(answers).map(([question_id, answer]) => ({
76
  question_id,
77
  [typeof answer == "string" ? "text" : "options"]: typeof answer == "string" ? answer : Array.isArray(answer) ? answer : [answer],
@@ -141,13 +143,15 @@ export default function AssessmentDetailRoute() {
141
  </div>
142
  </section>
143
  <footer className="mx-auto py-4">
144
- <Button
145
- disabled={isSubmittingLoading}
146
- className="bg-indigo-600 text-white hover:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-600 disabled:opacity-50"
147
- onClick={handleSubmit}
148
- >
149
- Submit Answers
150
- </Button>
 
 
151
  </footer>
152
  </main>
153
  );
 
11
  import type { Route } from "./+types/jobs.$jid.assessments.$id";
12
  import { useGetJobAssessmentByID } from "~/services/useGetJobAssessmentByID";
13
  import { usePostAssessmentApplication } from "~/services/usePostAssessmentApplication";
14
+ import { useGetMyUser } from "~/services/useGetMyUser";
15
 
16
  export function meta({}: Route.MetaArgs) {
17
  return [
 
29
  const [answers, setAnswers] = useState({} as Record<string, any>);
30
 
31
  const { mutateAsync: submitAnswers, isPending: isSubmittingLoading } = usePostAssessmentApplication();
32
+ const { data: myUser, isLoading: isMyUserLoading, isError: isMyUserError } = useGetMyUser();
33
 
34
+ useEffect(() => {
35
+ if (assessment == null) { return }
36
+
37
+ setAnswers(assessment.questions.reduce((accumulator, question) => {
38
+ accumulator[question.id] = question.type === "choose_many" ? [] : "";
39
+ return accumulator;
40
+ }, {} as Record<string, any>));
41
+ }, [assessment]);
42
+
43
+ if (isLoading || isMyUserLoading) {
44
  return (
45
  <main className="container mx-auto p-4 flex flex-col gap-2 place-items-center">
46
  <div className="flex flex-col gap-2 place-items-center">
 
51
  );
52
  }
53
 
54
+ if (isError || isMyUserError) {
55
  return (
56
  <main className="container mx-auto p-4 flex flex-col gap-2">
57
  <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">
 
67
  );
68
  }
69
 
 
 
 
 
 
 
 
 
 
70
  const totalWeights = assessment.questions.reduce((weights, question) => weights + question.weight, 0);
71
 
72
  function handleSubmit() {
73
  submitAnswers({
74
  job_id: jid || "",
75
  assessment_id: id || "",
76
+ user_id: myUser!.id,
77
  answers: Object.entries(answers).map(([question_id, answer]) => ({
78
  question_id,
79
  [typeof answer == "string" ? "text" : "options"]: typeof answer == "string" ? answer : Array.isArray(answer) ? answer : [answer],
 
143
  </div>
144
  </section>
145
  <footer className="mx-auto py-4">
146
+ {myUser.role === "applicant" && (
147
+ <Button
148
+ disabled={isSubmittingLoading}
149
+ className="bg-indigo-600 text-white hover:bg-indigo-700 dark:bg-indigo-500 dark:hover:bg-indigo-600 disabled:opacity-50"
150
+ onClick={handleSubmit}
151
+ >
152
+ Submit Answers
153
+ </Button>
154
+ )}
155
  </footer>
156
  </main>
157
  );
frontend/app/routes/registration.tsx CHANGED
@@ -1,14 +1,154 @@
1
  import type { Route } from "./+types/registration";
 
 
 
 
 
 
 
2
 
3
  export function meta({}: Route.MetaArgs) {
4
- return [
5
- { title: "Registration" },
6
- {
7
- name: "description",
8
- content: "Login to access your account or create a new one.",
9
- },
10
- ];
11
  }
12
 
13
  export default function Registration() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  }
 
1
  import type { Route } from "./+types/registration";
2
+ import React, { useState } from "react";
3
+ import { Input } from "~/components/ui/input";
4
+ import { Button } from "~/components/ui/button";
5
+ import { HTTPManager } from "~/managers/HTTPManager";
6
+ import { useNavigate } from "react-router";
7
+ import { toast } from "react-toastify";
8
+ import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from "~/components/ui/combobox";
9
 
10
  export function meta({}: Route.MetaArgs) {
11
+ return [
12
+ { title: "Registration" },
13
+ {
14
+ name: "description",
15
+ content: "Login to access your account or create a new one.",
16
+ },
17
+ ];
18
  }
19
 
20
  export default function Registration() {
21
+ const [mode, setMode] = useState<"login" | "signup">("login");
22
+ const [loading, setLoading] = useState(false);
23
+ const [form, setForm] = useState({
24
+ first_name: "",
25
+ last_name: "",
26
+ email: "",
27
+ role: "hr",
28
+ password: "",
29
+ });
30
+
31
+ const navigate = useNavigate();
32
+
33
+ function update<K extends keyof typeof form>(k: K, v: typeof form[K]) {
34
+ setForm((s) => ({ ...s, [k]: v }));
35
+ }
36
+
37
+ async function handleSubmit(e: React.FormEvent) {
38
+ e.preventDefault();
39
+ setLoading(true);
40
+ try {
41
+ if (mode === "login") {
42
+ const resp = await HTTPManager.post("/users/registration/login", {
43
+ email: form.email,
44
+ password: form.password,
45
+ });
46
+ const token = resp?.data?.token;
47
+ if (token) {
48
+ localStorage.setItem("token", token);
49
+ toast.success("Logged in");
50
+ navigate("/dashboard");
51
+ } else {
52
+ toast.error("Login succeeded but no token returned");
53
+ }
54
+ } else {
55
+ const payload = {
56
+ first_name: form.first_name,
57
+ last_name: form.last_name,
58
+ email: form.email,
59
+ role: form.role,
60
+ password: form.password,
61
+ };
62
+ const resp = await HTTPManager.post("/users/registration/signup", payload);
63
+ const token = resp?.data?.token;
64
+ if (token) {
65
+ localStorage.setItem("token", token);
66
+ toast.success("Account created");
67
+ navigate("/dashboard");
68
+ } else {
69
+ toast.error("Signup succeeded but no token returned");
70
+ }
71
+ }
72
+ } catch (err: any) {
73
+ const message =
74
+ err?.response?.data?.detail || err?.message || "Request failed";
75
+ toast.error(message);
76
+ } finally {
77
+ setLoading(false);
78
+ }
79
+ }
80
+
81
+ return (
82
+ <div className="max-w-xl mx-auto p-6">
83
+ <h1 className="text-2xl font-semibold mb-4">Welcome</h1>
84
+
85
+ <div className="flex gap-2 mb-6">
86
+ <Button variant={mode === "login" ? "default" : "outline"} onClick={() => setMode("login")}>Login</Button>
87
+ <Button variant={mode === "signup" ? "default" : "outline"} onClick={() => setMode("signup")}>Sign up</Button>
88
+ </div>
89
+
90
+ <form onSubmit={handleSubmit} className="space-y-4 bg-white dark:bg-gray-800 p-6 rounded shadow">
91
+ {mode === "signup" && (
92
+ <div className="grid grid-cols-2 gap-3">
93
+ <div>
94
+ <label className="text-sm text-gray-700 dark:text-gray-300">First name</label>
95
+ <Input value={form.first_name} onChange={(e) => update("first_name", e.target.value)} />
96
+ </div>
97
+ <div>
98
+ <label className="text-sm text-gray-700 dark:text-gray-300">Last name</label>
99
+ <Input value={form.last_name} onChange={(e) => update("last_name", e.target.value)} />
100
+ </div>
101
+ </div>
102
+ )}
103
+
104
+ <div>
105
+ <label className="text-sm text-gray-700 dark:text-gray-300">Email</label>
106
+ <Input type="email" value={form.email} onChange={(e) => update("email", e.target.value)} />
107
+ </div>
108
+
109
+ <div>
110
+ <label className="text-sm text-gray-700 dark:text-gray-300">Password</label>
111
+ <Input type="password" value={form.password} onChange={(e) => update("password", e.target.value)} />
112
+ </div>
113
+
114
+ {mode === "signup" && (
115
+ <div>
116
+ <label className="text-sm text-gray-700 dark:text-gray-300">Role</label>
117
+ <Combobox items={["hr", "applicant"]} value={form.role} onValueChange={(value) => setForm((s) => ({ ...s, role: value as "hr" | "applicant" }))}>
118
+ <ComboboxInput placeholder="Choose value" />
119
+ <ComboboxContent>
120
+ <ComboboxEmpty>No items found.</ComboboxEmpty>
121
+ <ComboboxList>
122
+ {(item) => (
123
+ <ComboboxItem key={item} value={item}>
124
+ {item}
125
+ </ComboboxItem>
126
+ )}
127
+ </ComboboxList>
128
+ </ComboboxContent>
129
+ </Combobox>
130
+ <select
131
+ value={form.role}
132
+ onChange={(e) => update("role", e.target.value as "hr" | "applicant")}
133
+ className="mt-1 block w-full rounded-md border px-3 py-2"
134
+ >
135
+ <option value="hr">HR</option>
136
+ <option value="applicant">Candidate</option>
137
+ </select>
138
+ </div>
139
+ )}
140
+
141
+ <div className="flex items-center justify-between gap-4">
142
+ <Button variant="ghost" type="button" onClick={() => {
143
+ setForm({ first_name: "", last_name: "", email: "", role: "hr", password: "" });
144
+ }}>
145
+ Clear
146
+ </Button>
147
+ <Button type="submit" disabled={loading}>
148
+ {loading ? "Working…" : mode === "login" ? "Login" : "Create account"}
149
+ </Button>
150
+ </div>
151
+ </form>
152
+ </div>
153
+ );
154
  }
frontend/app/services/useGetMyUser.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { HTTPManager } from "~/managers/HTTPManager";
3
+
4
+ export const GET_MY_USER_KEY = "my-user";
5
+
6
+ export type User = {
7
+ "id": string
8
+ "email": string,
9
+ "last_name": string,
10
+ "first_name": string,
11
+ "role": "hr" | "applicant",
12
+ }
13
+
14
+ export const useGetMyUser = () => useQuery({
15
+ queryKey: [GET_MY_USER_KEY],
16
+ queryFn: async () => HTTPManager.get<User>("/users/me").then(response => response.data),
17
+ })