Skip to content

Commit 1375df3

Browse files
committed
Implement basic onboarding
1 parent 44a8401 commit 1375df3

File tree

14 files changed

+344
-56
lines changed

14 files changed

+344
-56
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/onboard/_components/Forms/LanguagesForm.tsx

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use client";
22

3+
import MultiBadgeSelectInput from "@/components/form/MultiBadgeSelect";
4+
import { LoadingSpinner } from "@/components/LoadingSpinner";
35
import { Button } from "@/components/ui/button";
46
import {
57
Card,
@@ -8,28 +10,67 @@ import {
810
CardHeader,
911
CardTitle,
1012
} from "@/components/ui/card";
11-
import { OnboardMultiStepFormContext } from "@/contexts/OnboardMultiStepFormContext";
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";
1217
import { LanguageEnum } from "@/types/Languages";
1318
import { zodResolver } from "@hookform/resolvers/zod";
1419
import { MoveLeft } from "lucide-react";
15-
import { useContext } from "react";
16-
import { Form, useForm } from "react-hook-form";
20+
import { useRouter } from "next/navigation";
21+
import { useCallback } from "react";
22+
import { useForm } from "react-hook-form";
1723
import { z } from "zod";
1824

1925
const FormSchema = z.object({
2026
languages: z.array(LanguageEnum),
27+
isOnboarded: z.boolean(),
2128
});
2229

2330
export default function LanguagesForm() {
24-
const { prevStep } = useContext(OnboardMultiStepFormContext);
31+
const router = useRouter();
32+
33+
const { userProfile, updateUserProfile, prevStep } =
34+
useOnboardMultiStepFormContext();
2535

2636
const form = useForm<z.infer<typeof FormSchema>>({
2737
resolver: zodResolver(FormSchema),
2838
defaultValues: {
29-
languages: [],
39+
languages: userProfile.languages,
40+
isOnboarded: true,
3041
},
3142
});
3243

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+
3374
return (
3475
<Card className="mt-3">
3576
<CardHeader>
@@ -42,7 +83,28 @@ export default function LanguagesForm() {
4283
</CardHeader>
4384
<CardContent>
4485
<Form {...form}>
45-
<form className="flex flex-col gap-5">
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+
/>
46108
<div className="flex justify-end gap-2">
47109
<Button
48110
variant="ghost"
@@ -55,8 +117,8 @@ export default function LanguagesForm() {
55117
<MoveLeft className="stroke-foreground-100 mr-2" />
56118
Back
57119
</Button>
58-
<Button className="w-full max-w-40" type="submit">
59-
Done
120+
<Button className="self-end w-full max-w-40" type="submit">
121+
{form.formState.isSubmitting ? <LoadingSpinner /> : "Done"}
60122
</Button>
61123
</div>
62124
</form>

frontend/src/app/onboard/_components/Forms/ProficiencyForm.tsx

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use client";
22

3+
import { RadioGroupInput } from "@/components/form/RadioGroupInput";
4+
import { LoadingSpinner } from "@/components/LoadingSpinner";
35
import { Button } from "@/components/ui/button";
46
import {
57
Card,
@@ -9,7 +11,8 @@ import {
911
CardTitle,
1012
} from "@/components/ui/card";
1113
import { Form } from "@/components/ui/form";
12-
import { OnboardMultiStepFormContext } from "@/contexts/OnboardMultiStepFormContext";
14+
import { useOnboardMultiStepFormContext } from "@/contexts/OnboardMultiStepFormContext";
15+
import { editUserProfile } from "@/services/userService";
1316
import { ProficiencyEnum } from "@/types/Proficiency";
1417
import { zodResolver } from "@hookform/resolvers/zod";
1518
import { MoveLeft } from "lucide-react";
@@ -22,18 +25,35 @@ const FormSchema = z.object({
2225
});
2326

2427
export default function ProficiencyForm() {
25-
const { nextStep, prevStep } = useContext(OnboardMultiStepFormContext);
28+
const { userProfile, updateUserProfile, nextStep, prevStep } =
29+
useOnboardMultiStepFormContext();
2630

2731
const form = useForm<z.infer<typeof FormSchema>>({
2832
resolver: zodResolver(FormSchema),
2933
defaultValues: {
30-
proficiency: ProficiencyEnum.enum.Beginner,
34+
proficiency: userProfile.proficiency,
3135
},
3236
});
3337

34-
const onSubmit = useCallback(() => {
35-
nextStep();
36-
}, [nextStep]);
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+
);
3757

3858
return (
3959
<Card className="mt-3">
@@ -49,6 +69,29 @@ export default function ProficiencyForm() {
4969
onSubmit={form.handleSubmit(onSubmit)}
5070
className="flex flex-col gap-5"
5171
>
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+
/>
5295
<div className="flex justify-end gap-2">
5396
<Button
5497
variant="ghost"
@@ -61,8 +104,8 @@ export default function ProficiencyForm() {
61104
<MoveLeft className="stroke-foreground-100 mr-2" />
62105
Back
63106
</Button>
64-
<Button className="w-full max-w-40" type="submit">
65-
Next
107+
<Button className="self-end w-full max-w-40" type="submit">
108+
{form.formState.isSubmitting ? <LoadingSpinner /> : "Next"}
66109
</Button>
67110
</div>
68111
</form>

frontend/src/app/onboard/_components/Forms/UserDetailsForm.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,51 @@ import {
1414
CardTitle,
1515
} from "@/components/ui/card";
1616
import { Button } from "@/components/ui/button";
17-
import { useCallback, useContext } from "react";
18-
import { OnboardMultiStepFormContext } from "@/contexts/OnboardMultiStepFormContext";
17+
import { useCallback, useEffect } from "react";
18+
import { useOnboardMultiStepFormContext } from "@/contexts/OnboardMultiStepFormContext";
19+
import { editUserProfile } from "@/services/userService";
20+
import { LoadingSpinner } from "@/components/LoadingSpinner";
1921

2022
const FormSchema = z.object({
2123
profilePicture: z.string(), // TODO: change to actual image file type
2224
displayName: z.string(),
25+
username: z.string().refine((val) => val === val.toLowerCase(), {
26+
message: "Username must be in lowercase",
27+
}),
2328
});
2429

2530
export default function UserDetailsForm() {
26-
const { nextStep } = useContext(OnboardMultiStepFormContext);
31+
const { userProfile, updateUserProfile, nextStep } =
32+
useOnboardMultiStepFormContext();
2733

2834
const form = useForm<z.infer<typeof FormSchema>>({
2935
resolver: zodResolver(FormSchema),
3036
defaultValues: {
3137
profilePicture: "",
32-
displayName: "",
38+
displayName: userProfile.displayName,
39+
username: userProfile.username,
3340
},
3441
});
3542

36-
const onSubmit = useCallback(() => {
37-
nextStep();
38-
}, [nextStep]);
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+
);
3962

4063
return (
4164
<Card className="mt-3">
@@ -53,14 +76,20 @@ export default function UserDetailsForm() {
5376
className="flex flex-col gap-5"
5477
>
5578
<UserAvatarInput label="Profile Image" name="profilePicture" />
79+
<TextInput
80+
label="Username"
81+
name="username"
82+
placeholder={"Username"}
83+
className="bg-input-background-100"
84+
/>
5685
<TextInput
5786
label="Display Name"
5887
name="displayName"
5988
placeholder={"Name"}
6089
className="bg-input-background-100"
6190
/>
6291
<Button className="self-end w-full max-w-40" type="submit">
63-
Next
92+
{form.formState.isSubmitting ? <LoadingSpinner /> : "Next"}
6493
</Button>
6594
</form>
6695
</Form>

frontend/src/app/onboard/_components/StepperComponent.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useOnboardMultiStepFormContext } from "@/contexts/OnboardMultiStepFormContext";
12
import { cn } from "@/lib/utils";
23
import { Separator } from "@radix-ui/react-dropdown-menu";
34
import React from "react";
@@ -6,15 +7,9 @@ interface Activatable {
67
isActive: boolean;
78
}
89

9-
interface StepperComponentProps {
10-
totalSteps: number;
11-
currStep: number;
12-
}
10+
export default function StepperComponent() {
11+
const { currStep, totalSteps } = useOnboardMultiStepFormContext();
1312

14-
export default function StepperComponent({
15-
totalSteps,
16-
currStep,
17-
}: StepperComponentProps) {
1813
return (
1914
<div className="flex items-center gap-1 px-2">
2015
{Array.from({ length: totalSteps }, (e, i) => (
@@ -34,9 +29,10 @@ interface StepComponentProps extends Activatable {
3429
}
3530

3631
function StepComponent({ isActive, value }: StepComponentProps) {
32+
3733
return (
3834
<div
39-
className={cn("w-6 h-6 text-center align-middle rounded-full", {
35+
className={cn("w-6 h-6 text-center align-middle rounded-full hover:cursor-pointer", {
4036
"bg-primary": isActive,
4137
"outline outline-1 outline-background-100": !isActive,
4238
})}

frontend/src/app/onboard/layout.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
import React from "react";
22
import Navbar from "@/components/Navbar";
33
import { OnboardMultiStepFormProvider } from "@/contexts/OnboardMultiStepFormContext";
4+
import { UserProfileResponse } from "@/types/User";
5+
import { getCurrentUser } from "@/services/userService";
6+
import { redirect } from "next/navigation";
47

5-
export default function OnboardLayout({
8+
export default async function OnboardLayout({
69
children,
710
}: {
811
children: React.ReactNode;
912
}) {
13+
const userProfileResponse: UserProfileResponse = await getCurrentUser();
14+
15+
if (
16+
userProfileResponse.statusCode === 401
17+
) {
18+
redirect("/signin");
19+
}
20+
1021
return (
1122
<div className="flex flex-col min-h-screen">
1223
<Navbar isMinimal={true} className="relative mt-8 border-b-0" />
1324
<div className="flex-1 max-h-20" />
1425
<main className="flex-1">
15-
<OnboardMultiStepFormProvider>{children}</OnboardMultiStepFormProvider>
26+
<OnboardMultiStepFormProvider defaultUserProfile={userProfileResponse.data}>{children}</OnboardMultiStepFormProvider>
1627
</main>
1728
</div>
1829
);

0 commit comments

Comments
 (0)