Skip to content

Commit 09644ab

Browse files
committed
Add basic onboard frontend components
1 parent c48e64c commit 09644ab

File tree

13 files changed

+303
-19
lines changed

13 files changed

+303
-19
lines changed

frontend/src/app/dashboard/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Toaster } from "@/components/ui/toaster";
33
import React from "react";
44

55
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
6-
return (
6+
return (
77
<>
88
<Navbar />
99
<main>{children}</main>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
Card,
6+
CardContent,
7+
CardDescription,
8+
CardHeader,
9+
CardTitle,
10+
} from "@/components/ui/card";
11+
import { OnboardMultiStepFormContext } from "@/contexts/OnboardMultiStepFormContext";
12+
import { LanguageEnum } from "@/types/Languages";
13+
import { zodResolver } from "@hookform/resolvers/zod";
14+
import { MoveLeft } from "lucide-react";
15+
import { useContext } from "react";
16+
import { Form, useForm } from "react-hook-form";
17+
import { z } from "zod";
18+
19+
const FormSchema = z.object({
20+
languages: z.array(LanguageEnum),
21+
});
22+
23+
export default function LanguagesForm() {
24+
const { prevStep } = useContext(OnboardMultiStepFormContext);
25+
26+
const form = useForm<z.infer<typeof FormSchema>>({
27+
resolver: zodResolver(FormSchema),
28+
defaultValues: {
29+
languages: [],
30+
},
31+
});
32+
33+
return (
34+
<Card className="mt-3">
35+
<CardHeader>
36+
<CardTitle className="text-xl">
37+
Select your preferred programming language(s)
38+
</CardTitle>
39+
<CardDescription>
40+
We will match someone who uses the same language as you.
41+
</CardDescription>
42+
</CardHeader>
43+
<CardContent>
44+
<Form {...form}>
45+
<form className="flex flex-col gap-5">
46+
<div className="flex justify-end gap-2">
47+
<Button
48+
variant="ghost"
49+
className="self-start"
50+
onClick={(e) => {
51+
e.preventDefault();
52+
prevStep();
53+
}}
54+
>
55+
<MoveLeft className="stroke-foreground-100 mr-2" />
56+
Back
57+
</Button>
58+
<Button className="w-full max-w-40" type="submit">
59+
Done
60+
</Button>
61+
</div>
62+
</form>
63+
</Form>
64+
</CardContent>
65+
</Card>
66+
);
67+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
Card,
6+
CardContent,
7+
CardDescription,
8+
CardHeader,
9+
CardTitle,
10+
} from "@/components/ui/card";
11+
import { Form } from "@/components/ui/form";
12+
import { OnboardMultiStepFormContext } from "@/contexts/OnboardMultiStepFormContext";
13+
import { ProficiencyEnum } from "@/types/Proficiency";
14+
import { zodResolver } from "@hookform/resolvers/zod";
15+
import { MoveLeft } from "lucide-react";
16+
import { useCallback, useContext } from "react";
17+
import { useForm } from "react-hook-form";
18+
import { z } from "zod";
19+
20+
const FormSchema = z.object({
21+
proficiency: ProficiencyEnum,
22+
});
23+
24+
export default function ProficiencyForm() {
25+
const { nextStep, prevStep } = useContext(OnboardMultiStepFormContext);
26+
27+
const form = useForm<z.infer<typeof FormSchema>>({
28+
resolver: zodResolver(FormSchema),
29+
defaultValues: {
30+
proficiency: ProficiencyEnum.enum.Beginner,
31+
},
32+
});
33+
34+
const onSubmit = useCallback(() => {
35+
nextStep();
36+
}, [nextStep]);
37+
38+
return (
39+
<Card className="mt-3">
40+
<CardHeader>
41+
<CardTitle className="text-xl">Select your proficiency level</CardTitle>
42+
<CardDescription>
43+
We will match someone of your proficiency level
44+
</CardDescription>
45+
</CardHeader>
46+
<CardContent>
47+
<Form {...form}>
48+
<form
49+
onSubmit={form.handleSubmit(onSubmit)}
50+
className="flex flex-col gap-5"
51+
>
52+
<div className="flex justify-end gap-2">
53+
<Button
54+
variant="ghost"
55+
className="self-start"
56+
onClick={(e) => {
57+
e.preventDefault();
58+
prevStep();
59+
}}
60+
>
61+
<MoveLeft className="stroke-foreground-100 mr-2" />
62+
Back
63+
</Button>
64+
<Button className="w-full max-w-40" type="submit">
65+
Next
66+
</Button>
67+
</div>
68+
</form>
69+
</Form>
70+
</CardContent>
71+
</Card>
72+
);
73+
}

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

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ 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";
1719

1820
const FormSchema = z.object({
1921
profilePicture: z.string(), // TODO: change to actual image file type
2022
displayName: z.string(),
2123
});
2224

2325
export default function UserDetailsForm() {
26+
const { nextStep } = useContext(OnboardMultiStepFormContext);
27+
2428
const form = useForm<z.infer<typeof FormSchema>>({
2529
resolver: zodResolver(FormSchema),
2630
defaultValues: {
@@ -29,26 +33,35 @@ export default function UserDetailsForm() {
2933
},
3034
});
3135

36+
const onSubmit = useCallback(() => {
37+
nextStep();
38+
}, [nextStep]);
39+
3240
return (
3341
<Card className="mt-3">
3442
<CardHeader>
35-
<CardTitle className="text-xl">Let's setup your profile</CardTitle>
43+
<CardTitle className="text-xl">{`Let's setup your profile`}</CardTitle>
3644
<CardDescription>
3745
Tell us more about yourself so that we can provide you a personalised
3846
experience.
3947
</CardDescription>
4048
</CardHeader>
4149
<CardContent>
4250
<Form {...form}>
43-
<form className="flex flex-col gap-5">
51+
<form
52+
onSubmit={form.handleSubmit(onSubmit)}
53+
className="flex flex-col gap-5"
54+
>
4455
<UserAvatarInput label="Profile Image" name="profilePicture" />
4556
<TextInput
4657
label="Display Name"
4758
name="displayName"
4859
placeholder={"Name"}
4960
className="bg-input-background-100"
5061
/>
51-
<Button className="self-center max-w-40" type="submit">Next</Button>
62+
<Button className="self-end w-full max-w-40" type="submit">
63+
Next
64+
</Button>
5265
</form>
5366
</Form>
5467
</CardContent>

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ export default function StepperComponent({
1818
return (
1919
<div className="flex items-center gap-1 px-2">
2020
{Array.from({ length: totalSteps }, (e, i) => (
21-
<>
22-
<StepComponent isActive={i + 1 <= currStep} value={i + 1} key={i} />
21+
<React.Fragment key={i}>
22+
<StepComponent isActive={i + 1 <= currStep} value={i + 1} />
2323
{i < totalSteps - 1 && (
2424
<SeparatorComponent isActive={i + 1 < currStep} />
2525
)}
26-
</>
26+
</React.Fragment>
2727
))}
2828
</div>
2929
);
@@ -46,14 +46,11 @@ function StepComponent({ isActive, value }: StepComponentProps) {
4646
);
4747
}
4848

49-
interface SeparatorComponentProps extends Activatable {
50-
key?: number;
51-
}
49+
interface SeparatorComponentProps extends Activatable {}
5250

53-
function SeparatorComponent({ key, isActive }: SeparatorComponentProps) {
51+
function SeparatorComponent({ isActive }: SeparatorComponentProps) {
5452
return (
5553
<Separator
56-
key={key}
5754
className={cn("flex-1 h-[2px]", {
5855
"bg-primary": isActive,
5956
"bg-background-100": !isActive,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from "react";
2+
import Navbar from "@/components/Navbar";
3+
import { OnboardMultiStepFormProvider } from "@/contexts/OnboardMultiStepFormContext";
4+
5+
export default function OnboardLayout({
6+
children,
7+
}: {
8+
children: React.ReactNode;
9+
}) {
10+
return (
11+
<div className="flex flex-col min-h-screen">
12+
<Navbar isMinimal={true} className="relative mt-8 border-b-0" />
13+
<div className="flex-1 max-h-20" />
14+
<main className="flex-1">
15+
<OnboardMultiStepFormProvider>{children}</OnboardMultiStepFormProvider>
16+
</main>
17+
</div>
18+
);
19+
}

frontend/src/app/onboard/page.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"use client";
2+
3+
import StepperComponent from "./_components/StepperComponent";
4+
import UserDetailsForm from "./_components/Forms/UserDetailsForm";
5+
import { useContext } from "react";
6+
import { OnboardMultiStepFormContext } from "@/contexts/OnboardMultiStepFormContext";
7+
import ProficiencyForm from "./_components/Forms/ProficiencyForm";
8+
import LanguagesForm from "./_components/Forms/LanguagesForm";
9+
10+
export default function OnboardPage() {
11+
const { currStep, totalSteps } = useContext(OnboardMultiStepFormContext);
12+
13+
return (
14+
<div className="max-w-md mx-auto">
15+
<StepperComponent totalSteps={totalSteps} currStep={currStep} />
16+
{currStep === 1 ? (
17+
<UserDetailsForm />
18+
) : currStep === 2 ? (
19+
<ProficiencyForm />
20+
) : (
21+
<LanguagesForm />
22+
)}
23+
</div>
24+
);
25+
}

frontend/src/components/form/UserAvatarInput.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,21 @@ export function UserAvatarInput<TFieldValues extends FieldValues>({
3030
<FormLabel>{label}</FormLabel>
3131
<FormControl>
3232
<div className="flex gap-5">
33-
<UserAvatar isHoverEnabled={false} src={""} name={"A"} className="w-20 h-20" />
33+
<UserAvatar
34+
isHoverEnabled={false}
35+
src={""}
36+
name={"_"}
37+
className="w-20 h-20"
38+
/>
3439
<div className="flex flex-col items-start flex-1">
35-
<Button variant="soft">Upload Image</Button>
40+
<Button
41+
variant="soft"
42+
onClick={(e) => {
43+
e.preventDefault();
44+
}}
45+
>
46+
Upload Image
47+
</Button>
3648
<small>
3749
.png, .jpeg files up to 2MB. Recommended size is 256x256px.
3850
</small>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"use client";
2+
3+
import { User } from "@/types/User";
4+
import { createContext, useCallback, useState } from "react";
5+
6+
interface OnboardMultiStepFormContextType {
7+
user: User | null;
8+
totalSteps: number;
9+
currStep: number;
10+
nextStep: () => void;
11+
prevStep: () => void;
12+
updateUser: (val: User) => void;
13+
}
14+
15+
const defaultValues: OnboardMultiStepFormContextType = {
16+
user: null,
17+
totalSteps: 3,
18+
currStep: 1,
19+
nextStep: () => {},
20+
prevStep: () => {},
21+
updateUser: () => {},
22+
};
23+
24+
export const OnboardMultiStepFormContext =
25+
createContext<OnboardMultiStepFormContextType>(defaultValues);
26+
27+
export function OnboardMultiStepFormProvider({
28+
children,
29+
}: {
30+
children: React.ReactNode;
31+
}) {
32+
const [currStep, setCurrStep] = useState<number>(defaultValues.currStep);
33+
const [user, setUser] = useState<User | null>(defaultValues.user);
34+
35+
const nextStep = useCallback(() => {
36+
setCurrStep((prevCurrStep) =>
37+
Math.min(prevCurrStep + 1, defaultValues.totalSteps)
38+
);
39+
}, []);
40+
41+
const prevStep = useCallback(() => {
42+
setCurrStep((prevCurrStep) => Math.max(prevCurrStep - 1, 1));
43+
}, []);
44+
45+
const updateUser = useCallback((updatedUser: User) => {
46+
setUser((prevUser) => ({
47+
...prevUser,
48+
...updatedUser,
49+
}));
50+
}, []);
51+
52+
return (
53+
<OnboardMultiStepFormContext.Provider
54+
value={{
55+
user,
56+
totalSteps: defaultValues.totalSteps,
57+
currStep,
58+
nextStep,
59+
prevStep,
60+
updateUser,
61+
}}
62+
>
63+
{children}
64+
</OnboardMultiStepFormContext.Provider>
65+
);
66+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { z } from "zod";
2+
3+
const AccountProviderEnum = z.enum(["local", "google", "github"]);
4+
5+
type AccountProvider = z.infer<typeof AccountProviderEnum>;
6+
7+
export { AccountProviderEnum, type AccountProvider };

0 commit comments

Comments
 (0)