Skip to content

Commit 30692e4

Browse files
committed
update(web): update student settings page design
1 parent 5a9eb41 commit 30692e4

File tree

9 files changed

+235
-24
lines changed

9 files changed

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

7581
{tab === "notifications" ? (
@@ -80,12 +86,6 @@ const Content: React.FC<{
8086
notificationMethod={user.notificationMethod}
8187
/>
8288
) : null}
83-
84-
{tab === "topics" ? (
85-
<div className="max-w-[530px] grow flex">
86-
<TopicSelection />
87-
</div>
88-
) : null}
8989
</div>
9090
);
9191
};
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
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,5 @@
118118
"sharp"
119119
]
120120
},
121-
"packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67"
121+
"packageManager": "pnpm@10.16.1+sha512.0e155aa2629db8672b49e8475da6226aa4bdea85fdcdfdc15350874946d4f3c91faaf64cbdc4a5d1ab8002f473d5c3fcedcd197989cf0390f9badd3c04678706"
122122
}

packages/headless/src/student.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
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 { sample } from "lodash";
7+
import { useCallback, useMemo } from "react";
78

89
export function useFindStudentStats(
910
id?: number
@@ -89,3 +90,36 @@ export function useUpdateStudent({
8990
onError,
9091
});
9192
}
93+
94+
const levelValues = Object.values(IStudent.EnglishLevel).filter(
95+
(value) => typeof value === "number"
96+
);
97+
98+
function getRandomLevel() {
99+
const randomLevel = sample(levelValues);
100+
return randomLevel as IStudent.EnglishLevel;
101+
}
102+
103+
export function useStudentMeta(): UseQueryResult<{
104+
career: string;
105+
level: IStudent.EnglishLevel;
106+
aim: string;
107+
}> {
108+
const find = useCallback(async () => {
109+
return {
110+
career: sample([
111+
"labels.jobs.student",
112+
"labels.jobs.tutor",
113+
"labels.jobs.doctor",
114+
"labels.jobs.police-officer",
115+
]),
116+
level: getRandomLevel(),
117+
aim: "labels.random-aim",
118+
};
119+
}, []);
120+
121+
return useQuery({
122+
queryFn: find,
123+
queryKey: ["find-student-meta"],
124+
});
125+
}

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 = <T,>({
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: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,7 @@
675675
"discourage-dialog-title": "التواصل المرئي يعزز جودة الحصة",
676676
"discourage-dialog-description": "لا ننصحك باغلاق الكاميرا اثناء الحصة لأن هذا يؤثر علي التجربة العامة للحصة",
677677
"student-settings.password.title": "كلمة المرور",
678-
"student-settings.topics.title": "ادارة المفضلات",
678+
"student-settings.public-info.title": "المعلومات العامة",
679679
"student-settings.profile.title": "الإعدادات",
680680
"student-settings.upload.image.change": "تغيير الصورة",
681681
"student-settings.upload.image.clear": "حذف الصورة",
@@ -698,6 +698,7 @@
698698
"tutor-settings.topics.selection-dialog.title": "شارك خبراتك لتجذب الطلاب الذين يشاركونك الشغف!",
699699
"tutor-settings.topics.selection-dialog.description": "حدد المجالات التي تبرع فيها أو تهتم بها، مثل الرياضة أو التكنولوجيا أو علم النفس، لنساعدك في تخصيص تجربة تدريسية تناسب اهتمامات طلابك وتزيد من تفاعلهم وحماسهم للتعلم.",
700700
"student-settings.add-topics-button.label": "قم بأضافة الموضوعات المفضلة الخاصة بك",
701+
"student-settings.updated-successfully": "تم تحديث بياناتك بنجاح.",
701702
"tutor-settings.add-topics-button.label": "قم بأضافة مجالات الخبرة الخاصة بك",
702703
"shared-settings.topics.selection-dialog.loading": "برجاء الانتظار... جاري تحميل المفضلات!",
703704
"shared-settings.topics.selection-dialog.loading-error": " عذرًا، حدث خطأ أثناء تحميل المفضلات. برجاء المحاولة مرة أخرى",
@@ -919,6 +920,11 @@
919920
"payment.methods.wallet": "المحافظ الإلكترونة",
920921
"payment.methods.cards": "البطاقات",
921922
"payment.methods.fawry": "الدفع عن طريق فوري",
923+
"labels.jobs.student": "طالب",
924+
"labels.jobs.tutor": "معلم",
925+
"labels.jobs.doctor": "طبيب",
926+
"labels.random-aim": "لقد التحقت بالمنصة لأن هدفي كان دائما أن اتقن التحدث بالإنجليزية",
927+
"labels.jobs.police-officer": "ضابط شرطة",
922928
"labels.contact-us": "تواصل معنا",
923929
"labels.leave": "المغادرة",
924930
"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)