Skip to content

Commit e99ae02

Browse files
committed
update(web): update student settings page design
1 parent 711777f commit e99ae02

File tree

10 files changed

+218
-23
lines changed

10 files changed

+218
-23
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { languageLevels } from "@/constants/student";
2+
import { useOnError } from "@/hooks/error";
3+
import { useForm } from "@litespace/headless/form";
4+
import {
5+
useFindStudentById,
6+
useUpdateStudent,
7+
} from "@litespace/headless/student";
8+
import { useInfiniteTopics } from "@litespace/headless/topic";
9+
import { IStudent, ITopic } from "@litespace/types";
10+
import { Button } from "@litespace/ui/Button";
11+
import { Form } from "@litespace/ui/Form";
12+
import { useFormatMessage } from "@litespace/ui/hooks/intl";
13+
import { useMakeValidators } from "@litespace/ui/hooks/validation";
14+
import { Input } from "@litespace/ui/Input";
15+
import { validateEnglishLevel } from "@litespace/ui/lib/validate";
16+
import { MultiSelect } from "@litespace/ui/MultiSelect";
17+
import { Select } from "@litespace/ui/Select";
18+
import { Textarea } from "@litespace/ui/Textarea";
19+
import { useToast } from "@litespace/ui/Toast";
20+
import { Typography } from "@litespace/ui/Typography";
21+
import React, { useCallback, useMemo } from "react";
22+
23+
type IForm = {
24+
topics: ITopic.Self["name"]["ar"][];
25+
career: string;
26+
level: IStudent.EnglishLevel;
27+
aim: string;
28+
};
29+
30+
export const StudentPublicInfo: React.FC<{ id: number }> = ({ id }) => {
31+
const intl = useFormatMessage();
32+
const toast = useToast();
33+
34+
const { data } = useFindStudentById(id);
35+
36+
const { list: allTopics } = useInfiniteTopics();
37+
38+
const onSuccess = useCallback(() => {
39+
toast.success({
40+
title: intl("student-settings.updated-successfully"),
41+
});
42+
}, [intl, toast]);
43+
44+
const onError = useOnError({
45+
type: "mutation",
46+
handler: ({ messageId }) => {
47+
toast.error({
48+
title: intl("complete-profile.update.error"),
49+
description: intl(messageId),
50+
});
51+
},
52+
});
53+
54+
const updateStudent = useUpdateStudent({ onSuccess, onError });
55+
56+
const validators = useMakeValidators<IForm>({
57+
career: { required: false },
58+
level: { required: false, validate: validateEnglishLevel },
59+
aim: { required: false },
60+
});
61+
62+
const form = useForm<IForm>({
63+
defaults: {
64+
topics: data?.topics.map((topic) => topic.name.ar) || [],
65+
career: data?.career || intl("labels.jobs.student"),
66+
level: data?.level || IStudent.EnglishLevel.Beginner,
67+
aim: data?.aim || "",
68+
},
69+
validators,
70+
onSubmit(data) {
71+
updateStudent.mutate({
72+
id,
73+
payload: {
74+
topics: data.topics,
75+
career: data.career,
76+
level: data.level,
77+
aim: data.aim,
78+
},
79+
});
80+
},
81+
});
82+
83+
const levels = useMemo(
84+
() =>
85+
Object.entries(languageLevels).map(([key, value]) => ({
86+
label: intl(value),
87+
value: Number(key),
88+
})),
89+
[intl]
90+
);
91+
92+
return (
93+
<div className="w-full">
94+
<Form onSubmit={form.onSubmit} className="flex flex-col gap-6">
95+
<div className="flex flex-col gap-4">
96+
<Typography tag="h5" className="text-subtitle-2 font-bold">
97+
{intl("student-settings.public-info.title")}
98+
</Typography>
99+
<MultiSelect
100+
label={intl("complete-profile.topics.label")}
101+
options={
102+
allTopics?.map((topic) => ({
103+
label: topic.name.ar,
104+
value: topic.name.ar,
105+
})) || []
106+
}
107+
placeholder={intl("complete-profile.topics.placeholder")}
108+
values={form.state.topics}
109+
setValues={(values) => form.set("topics", values)}
110+
/>
111+
112+
<Input
113+
id="career"
114+
name="career"
115+
idleDir="rtl"
116+
value={form.state.career}
117+
inputSize={"large"}
118+
autoComplete="off"
119+
onChange={(e) => form.set("career", e.target.value)}
120+
label={intl("labels.job")}
121+
placeholder={intl("complete-profile.job.placeholder")}
122+
state={form.errors.career ? "error" : undefined}
123+
helper={form.errors.career}
124+
/>
125+
126+
<Select
127+
id="level"
128+
value={form.state.level}
129+
onChange={(value) => form.set("level", value)}
130+
options={levels}
131+
label={intl("complete-profile.level.label")}
132+
placeholder={intl("complete-profile.level.label")}
133+
helper={form.errors.level}
134+
/>
135+
136+
<Textarea
137+
className="min-h-[138px]"
138+
id="aim"
139+
name="aim"
140+
idleDir="rtl"
141+
value={form.state.aim}
142+
label={intl("complete-profile.aim.label")}
143+
placeholder={intl("complete-profile.aim.placeholder")}
144+
state={form.errors.aim ? "error" : undefined}
145+
helper={form.errors.aim}
146+
onChange={({ target }) => form.set("aim", target.value)}
147+
disabled={updateStudent.isPending}
148+
autoComplete="off"
149+
/>
150+
</div>
151+
152+
<Button
153+
size="large"
154+
htmlType="submit"
155+
loading={updateStudent.isPending}
156+
disabled={updateStudent.isPending}
157+
>
158+
{intl("shared-settings.save")}
159+
</Button>
160+
</Form>
161+
</div>
162+
);
163+
};
164+
165+
export default StudentPublicInfo;

apps/web/src/components/StudentSettings/Content.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import { Tabs } from "@litespace/ui/Tabs";
55
import NotificationSettings from "@/components/Settings/NotificationSettings";
66
import UpdatePassword from "@/components/Settings/UpdatePassword";
77
import PersonalDetails from "@/components/Settings/PersonalDetails";
8-
import TopicSelection from "@/components/Settings/TopicSelection";
98
import UploadPhoto from "@/components/StudentSettings/UploadPhoto";
109
import { IUser } from "@litespace/types";
1110
import { Tab } from "@/components/StudentSettings/types";
1211
import { isPersonalInfoIncomplete } from "@/components/Settings/utils";
1312
import { StudentSettingsTabId } from "@litespace/utils/routes";
13+
import StudentPublicInfo from "@/components/Settings/StudentPublicInfo";
1414

1515
const Content: React.FC<{
1616
tab: StudentSettingsTabId;
@@ -26,6 +26,11 @@ const Content: React.FC<{
2626
label: intl("shared-settings.personal.title"),
2727
important: isPersonalInfoIncomplete(user),
2828
},
29+
{
30+
id: "public-info",
31+
label: intl("student-settings.public-info.title"),
32+
important: false,
33+
},
2934
{
3035
id: "password",
3136
label: intl("shared-settings.password.title"),
@@ -36,11 +41,6 @@ const Content: React.FC<{
3641
label: intl("shared-settings.notification.title"),
3742
important: !user.notificationMethod,
3843
},
39-
{
40-
id: "topics",
41-
label: intl("student-settings.topics.title"),
42-
important: false,
43-
},
4444
];
4545
}, [intl, user]);
4646

@@ -70,6 +70,12 @@ const Content: React.FC<{
7070
/>
7171
) : null}
7272

73+
{tab === "public-info" ? (
74+
<div className="max-w-[530px] grow flex">
75+
<StudentPublicInfo id={user.id} />
76+
</div>
77+
) : null}
78+
7379
{tab === "password" ? <UpdatePassword id={user.id} /> : null}
7480

7581
{tab === "notifications" ? (
@@ -81,12 +87,6 @@ const Content: React.FC<{
8187
notificationMethod={user.notificationMethod}
8288
/>
8389
) : null}
84-
85-
{tab === "topics" ? (
86-
<div className="max-w-[530px] grow flex">
87-
<TopicSelection />
88-
</div>
89-
) : null}
9090
</div>
9191
);
9292
};
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { StudentSettingsTabId } from "@litespace/utils/routes";
22

33
export function isValidTab(tab: string): tab is StudentSettingsTabId {
4-
return ["personal", "password", "notifications", "topics"].includes(tab);
4+
return ["personal", "public-info", "password", "notifications"].includes(tab);
55
}

packages/atlas/src/api/student.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@ export class Student extends Base {
1313
): Promise<IStudent.UpdateApiResponse> {
1414
return this.patch({ route: `/api/v1/student/`, payload });
1515
}
16+
17+
async findById(id: number): Promise<IStudent.FindStudentMetaApiResponse> {
18+
return this.get({ route: `api/v1/student/${id}` });
19+
}
1620
}

packages/headless/src/student.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { useApi } from "@/api";
2-
import { useMutation, useQuery, UseQueryResult } from "@tanstack/react-query";
3-
import { useCallback, useMemo } from "react";
42
import { MutationKey, QueryKey } from "@/constants";
5-
import { IStudent, IUser } from "@litespace/types";
63
import { OnError, OnSuccess } from "@/types/query";
4+
import { IStudent, IUser } from "@litespace/types";
5+
import { useMutation, useQuery, UseQueryResult } from "@tanstack/react-query";
6+
import { useCallback, useMemo } from "react";
77

88
export function useFindStudentStats(
99
id?: number
@@ -84,3 +84,17 @@ export function useUpdateStudent({
8484
onError,
8585
});
8686
}
87+
88+
export function useFindStudentById(
89+
id: number
90+
): UseQueryResult<IStudent.FindStudentMetaApiResponse> {
91+
const api = useApi();
92+
const find = useCallback(async () => {
93+
return api.student.findById(id);
94+
}, [api.student, id]);
95+
96+
return useQuery({
97+
queryFn: find,
98+
queryKey: ["find-student-by-id"],
99+
});
100+
}

packages/types/src/student.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
<<<<<<< HEAD
12
import { IFilter, IUser, Paginated } from "@/index";
3+
=======
4+
import { IFilter, ITopic, IUser } from "@/index";
5+
>>>>>>> 9805f504 (update(web): update student settings page design)
26

37
export enum EnglishLevel {
48
Beginner = 1,
@@ -75,3 +79,9 @@ export type UpdateApiResponse = unknown;
7579
export type FindApiQuery = FindModelQuery;
7680

7781
export type FindApiResponse = Paginated<Self>;
82+
export type FindStudentMetaApiResponse = Self & {
83+
career: string;
84+
level: EnglishLevel;
85+
aim: string;
86+
topics: ITopic.Self[];
87+
};

packages/ui/src/components/MultiSelect/MultiSelect.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export const MultiSelect = ({
8585
{isEmpty(selectedOptions) ? (
8686
<Typography
8787
tag="span"
88-
className="flex-1 text-natural-400 text-start"
88+
className="flex-1 text-natural-600 text-start text-caption font-medium"
8989
>
9090
{placeholder}
9191
</Typography>

packages/ui/src/components/Tabs/Tabs.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const Tabs = <T extends string>({
2121
setTab: (id: T) => void;
2222
}) => {
2323
return (
24-
<div className="border border-natural-200 rounded-xl flex items-center justify-between w-full">
24+
<div className="border border-natural-200 rounded-xl flex items-center justify-between w-full max-w-[530px]">
2525
{tabs.map(({ id, label, important, disabled }) => (
2626
<Tab
2727
key={id}
@@ -47,7 +47,7 @@ const Tab: React.FC<{
4747
<button
4848
type="button"
4949
className={cn(
50-
"relative px-4 py-[10px] group rounded-xl w-full",
50+
"relative px-auto md:px-4 py-[10px] group rounded-xl w-full",
5151
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-offset-0 focus-visible:ring-brand-500",
5252
"flex items-center justify-center"
5353
)}
@@ -57,7 +57,7 @@ const Tab: React.FC<{
5757
<Typography
5858
tag="h6"
5959
className={cn(
60-
"relative font-medium text-tiny md:text-body whitespace-nowrap w-fit",
60+
"relative font-normal md:font-medium text-tiny md:text-body whitespace-nowrap w-fit",
6161
active
6262
? "text-brand-700"
6363
: "text-natural-500 group-hover:text-brand-500 group-active:text-brand-700"

packages/ui/src/locales/ar-eg.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,7 @@
678678
"discourage-dialog-title": "التواصل المرئي يعزز جودة الحصة",
679679
"discourage-dialog-description": "لا ننصحك باغلاق الكاميرا اثناء الحصة لأن هذا يؤثر علي التجربة العامة للحصة",
680680
"student-settings.password.title": "كلمة المرور",
681-
"student-settings.topics.title": "ادارة المفضلات",
681+
"student-settings.public-info.title": "المعلومات العامة",
682682
"student-settings.profile.title": "الإعدادات",
683683
"student-settings.upload.image.change": "تغيير الصورة",
684684
"student-settings.upload.image.clear": "حذف الصورة",
@@ -702,6 +702,7 @@
702702
"tutor-settings.topics.selection-dialog.description": "حدد المجالات التي تبرع فيها أو تهتم بها، مثل الرياضة أو التكنولوجيا أو علم النفس، لنساعدك في تخصيص تجربة تدريسية تناسب اهتمامات طلابك وتزيد من تفاعلهم وحماسهم للتعلم.",
703703
"student-settings.add-topics-button.label": "قم بإضافة الموضوعات المفضلة الخاصة بك",
704704
"tutor-settings.add-topics-button.label": "قم بإضافة مجالات الخبرة الخاصة بك",
705+
"student-settings.updated-successfully": "تم تحديث بياناتك بنجاح.",
705706
"shared-settings.topics.selection-dialog.loading": "برجاء الانتظار... جاري تحميل المفضلات!",
706707
"shared-settings.topics.selection-dialog.loading-error": " عذرًا، حدث خطأ أثناء تحميل المفضلات. برجاء المحاولة مرة أخرى",
707708
"shared-settings.topics.selection-dialog.update-error": " عذرًا، حدث خطأ أثناء تحديث المفضلات. برجاء المحاولة مرة أخرى",
@@ -924,6 +925,7 @@
924925
"payment.methods.wallet": "المحافظ الإلكترونة",
925926
"payment.methods.cards": "البطاقات",
926927
"payment.methods.fawry": "الدفع عن طريق فوري",
928+
"labels.jobs.student": "طالب",
927929
"labels.contact-us": "تواصل معنا",
928930
"labels.leave": "المغادرة",
929931
"labels.enable-notifications": "تفعيل الإشعارات",

packages/utils/src/routes/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,6 @@ export enum Dashboard {
6868

6969
export type StudentSettingsTabId =
7070
| "personal"
71+
| "public-info"
7172
| "password"
72-
| "notifications"
73-
| "topics";
73+
| "notifications";

0 commit comments

Comments
 (0)