Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions apps/web/src/components/Settings/StudentPublicInfo.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how it looks on Galaxy S10:
Screenshot_20250916_211355-1

  1. Try to make the tabs view more responsive.
  2. The font referred to in the screenshot is quite big.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work so far.
It requires more effort from us in order to make it more user friendly. As shown in the attached screenshot. There is no enough spaces between the buttons.

We may make it more responsive by breaking the label into two lines when the width gets this small. WDYT?
Screenshot_20250922_110953

Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { languageLevels } from "@/constants/student";
import { useOnError } from "@/hooks/error";
import { useForm } from "@litespace/headless/form";
import {
useFindStudentById,
useUpdateStudent,
} from "@litespace/headless/student";
import { useInfiniteTopics, useUserTopics } from "@litespace/headless/topic";
import { useUpdateUserTopics } from "@litespace/headless/user";
import { IStudent, ITopic } from "@litespace/types";
import { Button } from "@litespace/ui/Button";
import { Form } from "@litespace/ui/Form";
import { useFormatMessage } from "@litespace/ui/hooks/intl";
import { useMakeValidators } from "@litespace/ui/hooks/validation";
import { Input } from "@litespace/ui/Input";
import { validateEnglishLevel } from "@litespace/ui/lib/validate";
import { MultiSelect } from "@litespace/ui/MultiSelect";
import { Select } from "@litespace/ui/Select";
import { Textarea } from "@litespace/ui/Textarea";
import { useToast } from "@litespace/ui/Toast";
import { Typography } from "@litespace/ui/Typography";
import React, { useCallback, useMemo } from "react";

type IForm = {
topics: ITopic.Self[];
career: string;
level: IStudent.EnglishLevel;
aim: string;
};

export const StudentPublicInfo: React.FC<{ id: number }> = ({ id }) => {
const intl = useFormatMessage();
const toast = useToast();

const { data: studentData } = useFindStudentById(id);

const { list: allTopics } = useInfiniteTopics();
const { query: studentTopics } = useUserTopics();

const onSuccess = useCallback(() => {
toast.success({
title: intl("student-settings.updated-successfully"),
});
}, [intl, toast]);

const studentOnError = useOnError({
type: "mutation",
handler: ({ messageId }) => {
toast.error({
title: intl("complete-profile.update.error"),
description: intl(messageId),
});
},
});

const topicOnError = useOnError({
type: "mutation",
handler: ({ messageId }) => {
toast.error({
title: intl("complete-profile.update.error"),
description: intl(messageId),
});
},
});

const updateStudentTopics = useUpdateUserTopics({
onError: topicOnError,
});

const updateStudent = useUpdateStudent({
onSuccess,
onError: studentOnError,
});

const validators = useMakeValidators<IForm>({
career: { required: false },
level: { required: false, validate: validateEnglishLevel },
aim: { required: false },
});

const studentTopicsIds = useMemo(
() => studentTopics.data?.map((topic) => topic.id),
[studentTopics.data]
);

const form = useForm<IForm>({
defaults: {
topics: studentTopics.data || [],
career: studentData?.jobTitle || "",
level: studentData?.englishLevel || IStudent.EnglishLevel.Beginner,
aim: studentData?.learningObjective || "",
},
validators,
onSubmit(data) {
updateStudentTopics
.mutateAsync({
addTopics: data.topics
.filter((topic) => !studentTopicsIds?.includes(topic.id))
.map((topic) => topic.id),
removeTopics:
studentTopicsIds?.filter(
(topic) => !data.topics.map((tp) => tp.id).includes(topic)
) || [],
})
.then(() =>
updateStudent.mutate({
payload: {
id,
englishLevel: data.level,
jobTitle: data.career,
learningObjective: data.aim,
},
})
);
},
});

const levels = useMemo(
() =>
Object.entries(languageLevels).map(([key, value]) => ({
label: intl(value),
value: Number(key),
})),
[intl]
);

return (
<div className="w-full">
<Form onSubmit={form.onSubmit} className="flex flex-col gap-6">
<div className="flex flex-col gap-4">
<Typography tag="h5" className="text-subtitle-2 font-bold">
{intl("student-settings.public-info.title")}
</Typography>
<MultiSelect
label={intl("complete-profile.topics.label")}
options={
allTopics?.map((topic) => ({
label: topic.name.ar,
value: topic.id,
})) || []
}
placeholder={intl("complete-profile.topics.placeholder")}
values={form.state.topics.map((topic) => topic.id)}
setValues={(values) =>
form.set(
"topics",
allTopics?.filter((topic) => values.includes(topic.id)) || []
)
}
/>

<Input
id="career"
name="career"
idleDir="rtl"
value={form.state.career}
inputSize={"large"}
autoComplete="off"
onChange={(e) => form.set("career", e.target.value)}
label={intl("labels.job")}
placeholder={intl("complete-profile.job.placeholder")}
state={form.errors.career ? "error" : undefined}
helper={form.errors.career}
/>

<Select
id="level"
value={form.state.level}
onChange={(value) => form.set("level", value)}
options={levels}
label={intl("complete-profile.level.label")}
placeholder={intl("complete-profile.level.label")}
helper={form.errors.level}
/>

<Textarea
className="min-h-[138px]"
id="aim"
name="aim"
idleDir="rtl"
value={form.state.aim}
label={intl("complete-profile.aim.label")}
placeholder={intl("complete-profile.aim.placeholder")}
state={form.errors.aim ? "error" : undefined}
helper={form.errors.aim}
onChange={({ target }) => form.set("aim", target.value)}
disabled={updateStudent.isPending}
autoComplete="off"
/>
</div>

<Button
size="large"
htmlType="submit"
loading={updateStudent.isPending}
disabled={updateStudent.isPending}
>
{intl("shared-settings.save")}
</Button>
</Form>
</div>
);
};

export default StudentPublicInfo;
24 changes: 12 additions & 12 deletions apps/web/src/components/StudentSettings/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { Tabs } from "@litespace/ui/Tabs";
import NotificationSettings from "@/components/Settings/NotificationSettings";
import UpdatePassword from "@/components/Settings/UpdatePassword";
import PersonalDetails from "@/components/Settings/PersonalDetails";
import TopicSelection from "@/components/Settings/TopicSelection";
import UploadPhoto from "@/components/StudentSettings/UploadPhoto";
import { IUser } from "@litespace/types";
import { Tab } from "@/components/StudentSettings/types";
import { isPersonalInfoIncomplete } from "@/components/Settings/utils";
import { StudentSettingsTabId } from "@litespace/utils/routes";
import StudentPublicInfo from "@/components/Settings/StudentPublicInfo";

const Content: React.FC<{
tab: StudentSettingsTabId;
Expand All @@ -26,6 +26,11 @@ const Content: React.FC<{
label: intl("shared-settings.personal.title"),
important: isPersonalInfoIncomplete(user),
},
{
id: "public-info",
label: intl("student-settings.public-info.title"),
important: false,
},
{
id: "password",
label: intl("shared-settings.password.title"),
Expand All @@ -36,11 +41,6 @@ const Content: React.FC<{
label: intl("shared-settings.notification.title"),
important: !user.notificationMethod,
},
{
id: "topics",
label: intl("student-settings.topics.title"),
important: false,
},
];
}, [intl, user]);

Expand Down Expand Up @@ -70,6 +70,12 @@ const Content: React.FC<{
/>
) : null}

{tab === "public-info" ? (
<div className="max-w-[530px] grow flex">
<StudentPublicInfo id={user.id} />
</div>
) : null}

{tab === "password" ? <UpdatePassword id={user.id} /> : null}

{tab === "notifications" ? (
Expand All @@ -81,12 +87,6 @@ const Content: React.FC<{
notificationMethod={user.notificationMethod}
/>
) : null}

{tab === "topics" ? (
<div className="max-w-[530px] grow flex">
<TopicSelection />
</div>
) : null}
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/StudentSettings/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { StudentSettingsTabId } from "@litespace/utils/routes";

export function isValidTab(tab: string): tab is StudentSettingsTabId {
return ["personal", "password", "notifications", "topics"].includes(tab);
return ["personal", "public-info", "password", "notifications"].includes(tab);
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,5 @@
"sharp"
]
},
"packageManager": "pnpm@10.16.1+sha512.0e155aa2629db8672b49e8475da6226aa4bdea85fdcdfdc15350874946d4f3c91faaf64cbdc4a5d1ab8002f473d5c3fcedcd197989cf0390f9badd3c04678706"
"packageManager": "pnpm@10.17.1+sha512.17c560fca4867ae9473a3899ad84a88334914f379be46d455cbf92e5cf4b39d34985d452d2583baf19967fa76cb5c17bc9e245529d0b98745721aa7200ecaf7a"
}
6 changes: 6 additions & 0 deletions packages/atlas/src/api/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ export class Student extends Base {
): Promise<IStudent.UpdateApiResponse> {
return this.patch({ route: `/api/v1/student/`, payload });
}

async findById(
query: IStudent.FindByIdApiQuery
): Promise<IStudent.FindByIdApiResponse> {
return this.get({ route: `api/v1/student/${query.id}` });
}
}
1 change: 1 addition & 0 deletions packages/headless/src/constants/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export enum MutationKey {
DeleteSlot = "delete-slot",
UpdateUser = "update-user",
UpdateUserPersonalInfo = "update-user-personal-info",
UdpateUsertopics = "update-user-topics",
UpdateStudent = "update-student",
UpdateTutorTopics = "update-tutor-topics",
CreateRoom = "create-room",
Expand Down
1 change: 1 addition & 0 deletions packages/headless/src/constants/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export enum QueryKey {
FindUserById = "find-user-by-id",
FindStudentStats = "find-student-stats",
FindStudents = "find-students",
FindStudentById = "find-student-by-id",
FindPersonalizedStudentStats = "find-personalized-student-stats",
FindPersonalizedTutorStats = "find-personalized-tutor-stats",
FindTopic = "find-topic",
Expand Down
18 changes: 15 additions & 3 deletions packages/headless/src/student.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useApi } from "@/api";
import { useMutation, useQuery, UseQueryResult } from "@tanstack/react-query";
import { useCallback, useMemo } from "react";
import { MutationKey, QueryKey } from "@/constants";
import { IStudent, IUser } from "@litespace/types";
import { OnError, OnSuccess } from "@/types/query";
import { IStudent, IUser } from "@litespace/types";
import { useMutation, useQuery, UseQueryResult } from "@tanstack/react-query";
import { useCallback, useMemo } from "react";

export function useFindStudentStats(
id?: number
Expand Down Expand Up @@ -84,3 +84,15 @@ export function useUpdateStudent({
onError,
});
}

export function useFindStudentById(id: number): UseQueryResult<IStudent.Self> {
const api = useApi();
const find = useCallback(async () => {
return api.student.findById({ id });
}, [api.student, id]);

return useQuery({
queryFn: find,
queryKey: [QueryKey.FindStudentById],
});
}
24 changes: 24 additions & 0 deletions packages/headless/src/topic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,27 @@ export function useDeleteTopic({
onError,
});
}

export function useUpdateUserTopics({
onSuccess,
onError,
}: {
onSuccess: OnSuccess;
onError: OnError;
}) {
const api = useApi();

const update = useCallback(
async (payload: ITopic.ReplaceUserTopicsApiPayload) => {
return await api.topic.replaceUserTopics(payload);
},
[api.topic]
);

return useMutation({
mutationFn: update,
mutationKey: [MutationKey.UdpateUsertopics],
onSuccess,
onError,
});
}
Loading