Skip to content

Commit c275ad0

Browse files
authored
Merge pull request #62 from CS3219-AY2425S1/ms3-jmsandiegoo/frontend-onboarding
Onboarding feature
2 parents bb1e83e + 5960c22 commit c275ad0

28 files changed

+870
-20
lines changed

frontend/src/app/dashboard/_components/QuestionTable/data-table-toolbar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { CreateQuestionModal } from "../Forms/CreateQuestionModal";
99
import { QuestionTableContext } from "@/contexts/QuestionTableContext";
1010
import { useContext } from "react";
1111
import { useUser } from "@/contexts/UserContext";
12+
import { RoleEnum } from "@/types/Role";
1213

1314
interface DataTableToolbarProps<TData> {
1415
table: Table<TData>;
@@ -64,7 +65,7 @@ export default function DataTableToolbar<TData>({
6465
/>
6566
</div>
6667
<CreateQuestionModal>
67-
{user?.roles.includes("admin") && (
68+
{user?.roles.includes(RoleEnum.enum.admin) && (
6869
<Button variant="soft">
6970
<LucidePlus className="mr-2" />
7071
Create question

frontend/src/app/dashboard/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
"use client";
1+
// "use client";
22

33
import Navbar from "@/components/Navbar";
44
import { Toaster } from "@/components/ui/toaster";
55
import { UserProvider } from "@/contexts/UserContext";
6+
import React from "react";
67
import { PropsWithChildren } from "react";
78

89
export default function DashboardLayout({ children }: PropsWithChildren) {

frontend/src/app/globals.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
--destructive-foreground: #f7f9fb;
7373
--border: #2b3545;
7474
--input: #262626;
75+
--input-background-100: #404040;
7576
--input-foreground: #f3f4f6;
7677
--input-foreground-100: #9ca3af;
7778
--input: #262626;
@@ -117,6 +118,7 @@
117118
--destructive-foreground: #f7f9fb;
118119
--border: #2b3545;
119120
--input: #262626;
121+
--input-background-100: #404040;
120122
--input-foreground: #f3f4f6;
121123
--input-foreground-100: #9ca3af;
122124
--ring: #6f8ca4;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"use client";
2+
3+
import MultiBadgeSelectInput from "@/components/form/MultiBadgeSelect";
4+
import { LoadingSpinner } from "@/components/LoadingSpinner";
5+
import { Button } from "@/components/ui/button";
6+
import {
7+
Card,
8+
CardContent,
9+
CardDescription,
10+
CardHeader,
11+
CardTitle,
12+
} from "@/components/ui/card";
13+
import { Form } from "@/components/ui/form";
14+
import { useOnboardMultiStepFormContext } from "@/contexts/OnboardMultiStepFormContext";
15+
import { refreshAccessToken } from "@/services/authService";
16+
import { editUserProfile } from "@/services/userService";
17+
import { LanguageEnum } from "@/types/Languages";
18+
import { zodResolver } from "@hookform/resolvers/zod";
19+
import { MoveLeft } from "lucide-react";
20+
import { useRouter } from "next/navigation";
21+
import { useCallback } from "react";
22+
import { useForm } from "react-hook-form";
23+
import { z } from "zod";
24+
25+
const FormSchema = z.object({
26+
languages: z.array(LanguageEnum),
27+
isOnboarded: z.boolean(),
28+
});
29+
30+
export default function LanguagesForm() {
31+
const router = useRouter();
32+
33+
const { userProfile, updateUserProfile, prevStep } =
34+
useOnboardMultiStepFormContext();
35+
36+
const form = useForm<z.infer<typeof FormSchema>>({
37+
resolver: zodResolver(FormSchema),
38+
defaultValues: {
39+
languages: userProfile.languages,
40+
isOnboarded: true,
41+
},
42+
});
43+
44+
const onSubmit = useCallback(
45+
async (data: z.infer<typeof FormSchema>) => {
46+
const updatedUserProfile = {
47+
...userProfile,
48+
...data,
49+
};
50+
51+
const userProfileResponse = await editUserProfile(updatedUserProfile);
52+
53+
if (userProfileResponse.statusCode !== 200) {
54+
console.error(userProfileResponse.message);
55+
return;
56+
}
57+
58+
const accessTokenResponse = await refreshAccessToken();
59+
60+
if (accessTokenResponse.statusCode === 200 && accessTokenResponse.data) {
61+
localStorage.setItem(
62+
"access_token",
63+
accessTokenResponse.data.access_token
64+
);
65+
66+
router.replace("/dashboard");
67+
} else {
68+
// TODO: Display error message
69+
}
70+
},
71+
[userProfile]
72+
);
73+
74+
return (
75+
<Card className="mt-3">
76+
<CardHeader>
77+
<CardTitle className="text-xl">
78+
Select your preferred programming language(s)
79+
</CardTitle>
80+
<CardDescription>
81+
We will match someone who uses the same language as you.
82+
</CardDescription>
83+
</CardHeader>
84+
<CardContent>
85+
<Form {...form}>
86+
<form
87+
onSubmit={form.handleSubmit(onSubmit)}
88+
className="flex flex-col gap-5"
89+
>
90+
<MultiBadgeSelectInput
91+
label={""}
92+
name={"languages"}
93+
options={[
94+
{
95+
value: LanguageEnum.enum.Python,
96+
label: LanguageEnum.enum.Python,
97+
},
98+
{
99+
value: LanguageEnum.enum.Java,
100+
label: LanguageEnum.enum.Java,
101+
},
102+
{
103+
value: LanguageEnum.enum["C++"],
104+
label: LanguageEnum.enum["C++"],
105+
},
106+
]}
107+
/>
108+
<div className="flex justify-end gap-2">
109+
<Button
110+
variant="ghost"
111+
className="self-start"
112+
onClick={(e) => {
113+
e.preventDefault();
114+
prevStep();
115+
}}
116+
>
117+
<MoveLeft className="stroke-foreground-100 mr-2" />
118+
Back
119+
</Button>
120+
<Button className="self-end w-full max-w-40" type="submit">
121+
{form.formState.isSubmitting ? <LoadingSpinner /> : "Done"}
122+
</Button>
123+
</div>
124+
</form>
125+
</Form>
126+
</CardContent>
127+
</Card>
128+
);
129+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"use client";
2+
3+
import { RadioGroupInput } from "@/components/form/RadioGroupInput";
4+
import { LoadingSpinner } from "@/components/LoadingSpinner";
5+
import { Button } from "@/components/ui/button";
6+
import {
7+
Card,
8+
CardContent,
9+
CardDescription,
10+
CardHeader,
11+
CardTitle,
12+
} from "@/components/ui/card";
13+
import { Form } from "@/components/ui/form";
14+
import { useOnboardMultiStepFormContext } from "@/contexts/OnboardMultiStepFormContext";
15+
import { editUserProfile } from "@/services/userService";
16+
import { ProficiencyEnum } from "@/types/Proficiency";
17+
import { zodResolver } from "@hookform/resolvers/zod";
18+
import { MoveLeft } from "lucide-react";
19+
import { useCallback, useContext } from "react";
20+
import { useForm } from "react-hook-form";
21+
import { z } from "zod";
22+
23+
const FormSchema = z.object({
24+
proficiency: ProficiencyEnum,
25+
});
26+
27+
export default function ProficiencyForm() {
28+
const { userProfile, updateUserProfile, nextStep, prevStep } =
29+
useOnboardMultiStepFormContext();
30+
31+
const form = useForm<z.infer<typeof FormSchema>>({
32+
resolver: zodResolver(FormSchema),
33+
defaultValues: {
34+
proficiency: userProfile.proficiency,
35+
},
36+
});
37+
38+
const onSubmit = useCallback(
39+
async (data: z.infer<typeof FormSchema>) => {
40+
const updatedUserProfile = {
41+
...userProfile,
42+
...data,
43+
};
44+
45+
const userProfileResponse = await editUserProfile(updatedUserProfile);
46+
47+
if (userProfileResponse.statusCode !== 200) {
48+
console.error(userProfileResponse.message);
49+
return;
50+
}
51+
52+
updateUserProfile(userProfileResponse.data);
53+
nextStep();
54+
},
55+
[updateUserProfile, userProfile, nextStep]
56+
);
57+
58+
return (
59+
<Card className="mt-3">
60+
<CardHeader>
61+
<CardTitle className="text-xl">Select your proficiency level</CardTitle>
62+
<CardDescription>
63+
We will match someone of your proficiency level
64+
</CardDescription>
65+
</CardHeader>
66+
<CardContent>
67+
<Form {...form}>
68+
<form
69+
onSubmit={form.handleSubmit(onSubmit)}
70+
className="flex flex-col gap-5"
71+
>
72+
{/* <RadioGroupCardInput defaultValue="test1"/> */}
73+
<RadioGroupInput
74+
label={""}
75+
76+
name="proficiency"
77+
options={[
78+
{
79+
value: ProficiencyEnum.Enum.Beginner,
80+
optionLabel: ProficiencyEnum.Enum.Beginner,
81+
// className: "text-difficulty-easy",
82+
},
83+
{
84+
value: ProficiencyEnum.Enum.Intermediate,
85+
optionLabel: ProficiencyEnum.Enum.Intermediate,
86+
// className: "text-difficulty-medium",
87+
},
88+
{
89+
value: ProficiencyEnum.Enum.Advanced,
90+
optionLabel: ProficiencyEnum.Enum.Advanced,
91+
// className: "text-difficulty-hard",
92+
},
93+
]}
94+
/>
95+
<div className="flex justify-end gap-2">
96+
<Button
97+
variant="ghost"
98+
className="self-start"
99+
onClick={(e) => {
100+
e.preventDefault();
101+
prevStep();
102+
}}
103+
>
104+
<MoveLeft className="stroke-foreground-100 mr-2" />
105+
Back
106+
</Button>
107+
<Button className="self-end w-full max-w-40" type="submit">
108+
{form.formState.isSubmitting ? <LoadingSpinner /> : "Next"}
109+
</Button>
110+
</div>
111+
</form>
112+
</Form>
113+
</CardContent>
114+
</Card>
115+
);
116+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"use client";
2+
3+
import { TextInput } from "@/components/form/TextInput";
4+
import { UserAvatarInput } from "@/components/form/UserAvatarInput";
5+
import { zodResolver } from "@hookform/resolvers/zod";
6+
import { useForm } from "react-hook-form";
7+
import { Form } from "@/components/ui/form";
8+
import { z } from "zod";
9+
import {
10+
Card,
11+
CardContent,
12+
CardDescription,
13+
CardHeader,
14+
CardTitle,
15+
} from "@/components/ui/card";
16+
import { Button } from "@/components/ui/button";
17+
import { useCallback, useEffect } from "react";
18+
import { useOnboardMultiStepFormContext } from "@/contexts/OnboardMultiStepFormContext";
19+
import { editUserProfile } from "@/services/userService";
20+
import { LoadingSpinner } from "@/components/LoadingSpinner";
21+
22+
const FormSchema = z.object({
23+
profilePicture: z.string(), // TODO: change to actual image file type
24+
displayName: z.string(),
25+
username: z.string().refine((val) => val === val.toLowerCase(), {
26+
message: "Username must be in lowercase",
27+
}),
28+
});
29+
30+
export default function UserDetailsForm() {
31+
const { userProfile, updateUserProfile, nextStep } =
32+
useOnboardMultiStepFormContext();
33+
34+
const form = useForm<z.infer<typeof FormSchema>>({
35+
resolver: zodResolver(FormSchema),
36+
defaultValues: {
37+
profilePicture: "",
38+
displayName: userProfile.displayName,
39+
username: userProfile.username,
40+
},
41+
});
42+
43+
const onSubmit = useCallback(
44+
async (data: z.infer<typeof FormSchema>) => {
45+
const updatedUserProfile = {
46+
...userProfile,
47+
...data,
48+
};
49+
50+
const userProfileResponse = await editUserProfile(updatedUserProfile);
51+
52+
if (userProfileResponse.statusCode !== 200) {
53+
console.error(userProfileResponse.message);
54+
return;
55+
}
56+
57+
updateUserProfile(userProfileResponse.data);
58+
nextStep();
59+
},
60+
[updateUserProfile, userProfile, nextStep]
61+
);
62+
63+
return (
64+
<Card className="mt-3">
65+
<CardHeader>
66+
<CardTitle className="text-xl">{`Let's setup your profile`}</CardTitle>
67+
<CardDescription>
68+
Tell us more about yourself so that we can provide you a personalised
69+
experience.
70+
</CardDescription>
71+
</CardHeader>
72+
<CardContent>
73+
<Form {...form}>
74+
<form
75+
onSubmit={form.handleSubmit(onSubmit)}
76+
className="flex flex-col gap-5"
77+
>
78+
<UserAvatarInput label="Profile Image" name="profilePicture" />
79+
<TextInput
80+
label="Username"
81+
name="username"
82+
placeholder={"Username"}
83+
className="bg-input-background-100"
84+
/>
85+
<TextInput
86+
label="Display Name"
87+
name="displayName"
88+
placeholder={"Name"}
89+
className="bg-input-background-100"
90+
/>
91+
<Button className="self-end w-full max-w-40" type="submit">
92+
{form.formState.isSubmitting ? <LoadingSpinner /> : "Next"}
93+
</Button>
94+
</form>
95+
</Form>
96+
</CardContent>
97+
</Card>
98+
);
99+
}

0 commit comments

Comments
 (0)