Skip to content

Commit 0507c13

Browse files
committed
update(web): redesign studnet navbar
1 parent fa0afaf commit 0507c13

File tree

10 files changed

+1232
-451
lines changed

10 files changed

+1232
-451
lines changed

apps/mobile/locales/ar-eg.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,7 @@
790790
"student-dashboard.past-lessons.loading": "برجاء الانتظار... جاري تحميل الدروس السابقة!",
791791
"student-dashboard.past-lessons.error": "عذرًا، حدث خطأ أثناء تحميل الدروس السابقة، برجاء المحاولة مرة أخرى",
792792
"tour.sdashboard/1.title": "👋 مرحبًا بك في رحلتك لتعلّم اللغة!",
793-
"tour.sdashboard/1.description": "من خلال صفحة المدرّسين، تستطيع ان تستعرض كل المعلمين وتقوم بـ حجز جلسة مباشرة مع أيّ معلم منهم.كل ما عليك هو ان تضغط على المدرّس المناسب لك، وتستعرض ملفه الشخصي، مواعيده المتاحة، وتبدأ الحجز في الحال!",
793+
"tour.sdashboard/1.description": "من خلال صفحة المدرّسين، تستطيع ان تستعرض كل المعلمين وتقوم بـ حجز جلسة مباشرة مع أيّ معلم منهم.كل ما عليك هو ان تضغط على المدرّس المناسب لك، وتستعرض ملفه الشخصي، مواعيده المتاحة، وتبدأ الحجز في الحال!",
794794
"tour.sdashboard/2.title": "هل وجدت المعلم المناسب لك؟",
795795
"tour.sdashboard/2.description": "اضغط على 'احجز الآن' لكي تبدأ أول جلسة مع المعلم.",
796796
"tour.sdashboard/3.title": "الفيديو التعريفي للمعلم",
@@ -900,6 +900,7 @@
900900
"navbar.subscription.personal-quota": "الباقة الشخصية",
901901
"navbar.subscription.quota-consumption": "تم استهلاك {value}",
902902
"navbar.subscription.rest-of-quota": "متبقي {value} من هذا الأسبوع",
903+
"navbar.subscription.rest-of-quota-trial": "متبقي {value} من الفترة التجريبية",
903904
"navbar.subscription.tooltip": "رصيد دقائقك يتجدد تلقائيًا كل صباح يوم {day}.",
904905
"navbar.main": "الرئيسية",
905906
"navbar.register": "التسجيل",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Typography } from "@litespace/ui/Typography";
2+
import React from "react";
3+
4+
type Props = {
5+
label: string;
6+
seconds: number | null;
7+
};
8+
9+
const LessonCountdown: React.FC<Props> = ({ label, seconds }) => {
10+
if (seconds === null) return null;
11+
12+
const hours = Math.floor(seconds / 3600);
13+
const minutes = Math.floor((seconds % 3600) / 60);
14+
const secs = seconds % 60;
15+
16+
return (
17+
<div className="flex flex-col items-center gap-1 mr-6 ml-auto w-[203px] h-[42px] text-center">
18+
<Typography
19+
tag="span"
20+
className="text-tiny font-semibold text-natural-600"
21+
>
22+
{label}
23+
</Typography>
24+
25+
<div className="flex items-center gap-3">
26+
<Typography tag="span" className="text-base font-bold text-brand-500">
27+
{secs}
28+
</Typography>
29+
<Typography tag="span" className="text-base font-bold text-brand-500">
30+
:
31+
</Typography>
32+
<Typography tag="span" className="text-base font-bold text-brand-500">
33+
{minutes}
34+
</Typography>
35+
<Typography tag="span" className="text-base font-bold text-brand-500">
36+
:
37+
</Typography>
38+
<Typography tag="span" className="text-base font-bold text-brand-500">
39+
{hours}
40+
</Typography>
41+
</div>
42+
</div>
43+
);
44+
};
45+
46+
export default LessonCountdown;

apps/web/src/components/Layout/Navbar.tsx

Lines changed: 141 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
import cn from "classnames";
22
import dayjs from "dayjs";
3-
import React, { useCallback, useMemo } from "react";
3+
import React, { useEffect, useMemo, useRef, useState } from "react";
44
import { Link, useNavigate } from "react-router-dom";
55

66
import { ProfileInfo, SubscriptionQuota } from "@/components/Navbar";
77
import { useSaveLogs } from "@/hooks/logger";
8-
import Crown from "@litespace/assets/Crown";
98
import { useSubscription } from "@litespace/headless/context/subscription";
109
import { useUser } from "@litespace/headless/context/user";
10+
import { useFindLessons } from "@litespace/headless/lessons";
1111
import { IUser } from "@litespace/types";
1212
import { Button } from "@litespace/ui/Button";
1313
import { useFormatMessage } from "@litespace/ui/hooks/intl";
1414
import { Tooltip } from "@litespace/ui/Tooltip";
1515
import { Typography } from "@litespace/ui/Typography";
1616
import { isTutorRole } from "@litespace/utils";
1717
import { Web } from "@litespace/utils/routes";
18+
import { router } from "@/lib/routes";
19+
import LessonCountdown from "@/components/Layout/LessonCountdown";
20+
21+
const LESSON_WINDOW_SECONDS = 4 * 60 * 60;
1822

1923
const Navbar: React.FC = () => {
2024
return (
21-
<div className="shadow-app-navbar shadow lg:shadow-app-navbar-mobile w-full z-navbar bg-natural-50">
25+
<div className="shadow-app-navbar lg:shadow-app-navbar-mobile w-full z-navbar bg-natural-50">
2226
<div
2327
className={cn("flex justify-between gap-8 items-center py-6 px-4", {
2428
"max-w-screen-3xl mx-auto": location.pathname !== Web.Chat,
@@ -40,45 +44,145 @@ const Subscription: React.FC = () => {
4044
const { user } = useUser();
4145
const { info, remainingWeeklyMinutes, loading } = useSubscription();
4246
const intl = useFormatMessage();
47+
const now = useRef(dayjs().toISOString());
48+
const lessons = useFindLessons({
49+
canceled: false,
50+
users: user ? [user.id] : [],
51+
after: now.current,
52+
userOnly: true,
53+
size: 3,
54+
});
55+
const [remainingSeconds, setRemainingSeconds] = useState<number | null>(null);
56+
const [, refreshLiveLesson] = useState(0);
57+
const liveLesson = useMemo(() => {
58+
const items = lessons.query.data?.list;
59+
if (!items?.length) return null;
60+
const present = dayjs();
61+
return (
62+
items.find(({ lesson }) => {
63+
const start = dayjs(lesson.start);
64+
const end = start.add(lesson.duration, "minute");
65+
return !start.isAfter(present) && end.isAfter(present);
66+
}) || null
67+
);
68+
}, [lessons.query.data?.list]);
69+
const liveTutor = useMemo(() => {
70+
if (!liveLesson) return null;
71+
return liveLesson.members.find(
72+
(member) => member.role !== IUser.Role.Student
73+
);
74+
}, [liveLesson]);
4375

44-
const ended = useMemo(
45-
() => !!info && dayjs(info.end).isBefore(dayjs()),
46-
[info]
47-
);
76+
useEffect(() => {
77+
if (!liveLesson) return;
78+
const id = window.setInterval(
79+
() => refreshLiveLesson((value) => value + 1),
80+
30_000
81+
);
82+
return () => window.clearInterval(id);
83+
}, [liveLesson]);
84+
85+
useEffect(() => {
86+
const firstLesson = lessons.query.data?.list?.[0];
87+
if (!firstLesson?.lesson?.start) {
88+
setRemainingSeconds(null);
89+
return;
90+
}
91+
92+
const target = dayjs(firstLesson.lesson.start);
93+
const sync = () => {
94+
const diff = target.diff(dayjs(), "second");
95+
if (diff > 0 && diff <= LESSON_WINDOW_SECONDS) {
96+
setRemainingSeconds(diff);
97+
return true;
98+
}
99+
setRemainingSeconds(null);
100+
return false;
101+
};
102+
103+
if (!sync()) return;
104+
105+
const id = window.setInterval(() => {
106+
if (!sync()) {
107+
window.clearInterval(id);
108+
}
109+
}, 1000);
110+
111+
return () => window.clearInterval(id);
112+
}, [lessons.query.data?.list]);
113+
114+
const ended = !!info && dayjs(info.end).isBefore(dayjs());
115+
const noMinutesLeft = remainingWeeklyMinutes <= 0;
116+
const showSubscribeCTA =
117+
remainingSeconds === null && (noMinutesLeft || ended);
118+
const ctaButtonClass =
119+
"flex h-10 items-center justify-center gap-2 rounded-lg border border-brand-950/20 bg-brand-500 text-natural-0 px-4 hover:bg-brand-600 focus-visible:bg-brand-600 active:bg-brand-600";
48120

49121
if (loading || !user || isTutorRole(user.role)) return null;
50122

51-
if ((info?.id === -1 && remainingWeeklyMinutes === 0) || ended)
123+
if (liveLesson && liveTutor) {
124+
const durationSeconds = liveLesson.lesson.duration * 60;
125+
const halfPassed =
126+
durationSeconds > 0 &&
127+
dayjs().diff(liveLesson.lesson.start, "second") >= durationSeconds / 2;
128+
const liveLessonMessageKey = halfPassed
129+
? "navbar.lesson.ongoing.after-half"
130+
: "navbar.lesson.ongoing.message";
52131
return (
53-
<Link to={Web.Plans} tabIndex={-1}>
54-
<Button
55-
size="large"
56-
htmlType="button"
57-
endIcon={<Crown className="[&>*]:stroke-natural-50" />}
132+
<div className="flex items-center gap-4">
133+
<Typography
134+
tag="p"
135+
className="text-base font-medium text-right text-natural-700"
136+
>
137+
{intl(liveLessonMessageKey, {
138+
tutorName: liveTutor.name,
139+
})}
140+
</Typography>
141+
<Link
142+
to={router.web({ route: Web.Lesson, id: liveLesson.lesson.id })}
143+
tabIndex={-1}
58144
>
59-
<Typography
60-
tag="span"
61-
className="text-natural-50 text-body font-bold"
62-
>
63-
{intl("navbar.subscription.subscribe-now")}
64-
</Typography>
65-
</Button>
66-
</Link>
145+
<Button size="large" className={ctaButtonClass}>
146+
<span className="text-base font-medium leading-6">
147+
{intl("navbar.lesson.join-now")}
148+
</span>
149+
</Button>
150+
</Link>
151+
</div>
67152
);
153+
}
68154

69155
return (
70-
<Tooltip
71-
content={intl("navbar.subscription.tooltip", {
72-
day: dayjs(info?.start).format("dddd"),
73-
})}
74-
>
75-
<div>
76-
<SubscriptionQuota
77-
remainingMinutes={remainingWeeklyMinutes}
78-
weeklyMinutes={info?.weeklyMinutes || 0}
79-
/>
80-
</div>
81-
</Tooltip>
156+
<>
157+
<LessonCountdown
158+
label={intl("navbar.lesson.starts-in")}
159+
seconds={remainingSeconds}
160+
/>
161+
{showSubscribeCTA && (
162+
<Link to={Web.Plans} tabIndex={-1}>
163+
<Button size="large" htmlType="button" className={ctaButtonClass}>
164+
<span className="text-base font-medium leading-6">
165+
{intl("navbar.subscription.minutes-depleted")}
166+
</span>
167+
</Button>
168+
</Link>
169+
)}
170+
{remainingSeconds === null && !showSubscribeCTA && (
171+
<Tooltip
172+
content={intl("navbar.subscription.tooltip", {
173+
day: dayjs(info?.start).format("dddd"),
174+
})}
175+
>
176+
<div>
177+
<SubscriptionQuota
178+
remainingMinutes={remainingWeeklyMinutes}
179+
weeklyMinutes={info?.weeklyMinutes || 0}
180+
period={info?.period}
181+
/>
182+
</div>
183+
</Tooltip>
184+
)}
185+
</>
82186
);
83187
};
84188

@@ -88,14 +192,14 @@ const User: React.FC = () => {
88192
const navigate = useNavigate();
89193
const { save: saveLogs } = useSaveLogs();
90194

91-
const navToSettings = useCallback(() => {
195+
const navToSettings = () => {
92196
if (!user) return;
93197
navigate(
94198
user.role === IUser.Role.Student
95199
? Web.StudentSettings
96200
: Web.TutorProfileSettings
97201
);
98-
}, [user, navigate]);
202+
};
99203

100204
if (!user)
101205
return (
@@ -131,6 +235,8 @@ const User: React.FC = () => {
131235
onDoubleClick={async () => {
132236
saveLogs();
133237
}}
238+
aria-label={user.name ?? ""}
239+
title={user.name ?? ""}
134240
>
135241
<ProfileInfo
136242
imageUrl={user.image}

apps/web/src/components/Navbar/SubscriptionQuota/SubscriptionQouta.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { formatMinutes, formatPercentage } from "@litespace/ui/utils";
44
import React, { useMemo } from "react";
55
import Info from "@litespace/assets/Info";
66
import cn from "classnames";
7+
import { IPlan } from "@litespace/types";
78

89
const SubscriptionQouta: React.FC<{
910
weeklyMinutes: number;
1011
remainingMinutes: number;
11-
}> = ({ weeklyMinutes, remainingMinutes }) => {
12+
period?: IPlan.Period;
13+
}> = ({ weeklyMinutes, remainingMinutes, period }) => {
1214
const intl = useFormatMessage();
1315

1416
const remaining = useMemo(() => {
@@ -59,9 +61,14 @@ const SubscriptionQouta: React.FC<{
5961
tag="span"
6062
className="text-natural-950 text-tiny font-semibold text-right"
6163
>
62-
{intl("navbar.subscription.rest-of-quota", {
63-
value: formatMinutes(remainingMinutes),
64-
})}
64+
{intl(
65+
period === IPlan.Period.FreeTrial
66+
? "navbar.subscription.rest-of-quota-trial"
67+
: "navbar.subscription.rest-of-quota",
68+
{
69+
value: formatMinutes(remainingMinutes),
70+
}
71+
)}
6572
</Typography>
6673
</div>
6774
);

apps/web/src/components/Session/InSession.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect, useState } from "react";
22
import { SessionChat } from "@/components/Session/SessionChat";
3-
import { useNavigate, useSearchParams } from "react-router-dom";
3+
import { useSearchParams } from "react-router-dom";
44
import Controllers, { Controller } from "@/components/Session/Controllers";
55
import CallMembers from "@/components/Session/CallMembers";
66
import { useMediaCall } from "@/hooks/mediaCall";
@@ -18,8 +18,6 @@ import { useUser } from "@litespace/headless/context/user";
1818
import dayjs from "@/lib/dayjs";
1919
import { useOnError } from "@/hooks/error";
2020
import { useToast } from "@litespace/ui/Toast";
21-
import { useReportLesson } from "@litespace/headless/lessons";
22-
import { Web } from "@litespace/utils/routes";
2321
import { ConfirmationDialog } from "@litespace/ui/ConfirmationDialog";
2422
import CallIncoming from "@litespace/assets/CallIncoming";
2523
import { MIN_LESSON_DURATION } from "@litespace/utils";
@@ -47,7 +45,6 @@ const InSession: React.FC<{
4745
const call = useMediaCall();
4846
const intl = useFormatMessage();
4947
const toast = useToast();
50-
const navigate = useNavigate();
5148
const { user } = useUser();
5249
const { socket, reconnect } = useSocket();
5350

@@ -149,11 +146,6 @@ const InSession: React.FC<{
149146
onError,
150147
});
151148

152-
const reportLesson = useReportLesson({
153-
onSuccess: () => navigate(Web.UpcomingLessons),
154-
onError,
155-
});
156-
157149
// render an alert component based on three factors: one: the difference between
158150
// the current time and the session time, two: the role of the current user, and
159151
// three: the joined call members.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export const Content: React.FC<{
5252
queryClient.invalidateQueries({
5353
queryKey: [QueryKey.FindInfiniteLessons],
5454
});
55+
queryClient.invalidateQueries({
56+
queryKey: [QueryKey.FindLessons],
57+
});
5558
}, [toast, intl, queryClient]);
5659

5760
const onCancelError = useCallback(

apps/web/src/pages/LessonsSchedule.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const LessonsSchedule: React.FC = () => {
8484
toast.success({ title: intl("cancel-lesson.success") });
8585
setLessonId(null);
8686
invalidate([QueryKey.FindInfiniteLessons]);
87+
invalidate([QueryKey.FindLessons]);
8788
}, [toast, intl, invalidate]);
8889

8990
const onCancelError = useCallback(

0 commit comments

Comments
 (0)