Skip to content

Commit 24a96fe

Browse files
committed
feat(web): add navbar timer.
1 parent 1ee8c41 commit 24a96fe

File tree

4 files changed

+197
-38
lines changed

4 files changed

+197
-38
lines changed

apps/web/src/components/Chat/Messages.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ const Messages: React.FC<{
323323
) : null}
324324

325325
{room ? (
326-
<div className={"flex flex-col h-[85%] gap-2"}>
326+
<div className={"flex flex-col min-h-[85%] gap-2"}>
327327
<div
328328
id="messages-content"
329329
className={cn(

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

Lines changed: 188 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,195 @@
11
import cn from "classnames";
22
import dayjs from "dayjs";
3-
import React, { useCallback, useMemo } from "react";
3+
import React, {
4+
useCallback,
5+
useEffect,
6+
useMemo,
7+
useRef,
8+
useState,
9+
} from "react";
410
import { Link, useNavigate } from "react-router-dom";
511

612
import { ProfileInfo, SubscriptionQuota } from "@/components/Navbar";
713
import { useSaveLogs } from "@/hooks/logger";
8-
import Crown from "@litespace/assets/Crown";
14+
import { router } from "@/lib/routes";
915
import { useSubscription } from "@litespace/headless/context/subscription";
1016
import { useUser } from "@litespace/headless/context/user";
17+
import { useFindLessons } from "@litespace/headless/lessons";
18+
import { useMediaQuery } from "@litespace/headless/mediaQuery";
1119
import { IUser } from "@litespace/types";
1220
import { Button } from "@litespace/ui/Button";
1321
import { useFormatMessage } from "@litespace/ui/hooks/intl";
1422
import { Tooltip } from "@litespace/ui/Tooltip";
1523
import { Typography } from "@litespace/ui/Typography";
16-
import { isTutorRole } from "@litespace/utils";
24+
import {
25+
isTutorRole,
26+
MAX_LESSON_DURATION,
27+
MINUTES_IN_HOUR,
28+
SECONDS_IN_MINUTE,
29+
} from "@litespace/utils";
1730
import { Web } from "@litespace/utils/routes";
18-
import { track } from "@/lib/analytics";
31+
import { first, isEmpty } from "lodash";
32+
33+
const LESSON_NOTICE_MINUTES = 3;
34+
const SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR;
1935

2036
const Navbar: React.FC = () => {
37+
const { md } = useMediaQuery();
38+
const { info } = useSubscription();
39+
const { user } = useUser();
40+
const now = useRef<string | undefined>(undefined);
41+
42+
useEffect(() => {
43+
now.current = dayjs()
44+
.subtract(MAX_LESSON_DURATION, "minutes")
45+
.toISOString();
46+
}, []);
47+
48+
const lessons = useFindLessons({
49+
userOnly: true,
50+
users: user ? [user.id] : [],
51+
canceled: false,
52+
after: now.current,
53+
size: 1,
54+
});
55+
56+
const { nextLessonStart, nextLessonId, tutorName } = useMemo(() => {
57+
const nextLesson = first(
58+
lessons.query.data?.list.filter(({ lesson }) => !lesson.canceledAt)
59+
)?.lesson;
60+
61+
const tutor = first(lessons.query.data?.list)?.members.find(
62+
(member) => member.role !== IUser.Role.Student
63+
);
64+
65+
return {
66+
nextLessonStart: nextLesson?.start,
67+
nextLessonId: nextLesson?.id,
68+
tutorName: tutor?.name,
69+
};
70+
}, [lessons.query.data?.list]);
71+
72+
if (
73+
(!md && !info && isEmpty(lessons.query.data?.list)) ||
74+
(!md && location.pathname.split("/").includes("lesson")) ||
75+
(!md &&
76+
location.pathname.includes("chat") &&
77+
location.search.includes("room") &&
78+
!location.search.includes("null"))
79+
)
80+
return;
81+
2182
return (
22-
<div className="hidden md:block shadow-app-navbar shadow lg:shadow-app-navbar-mobile w-full z-navbar bg-natural-50">
83+
<div
84+
className={cn(
85+
"shadow-app-navbar shadow lg:shadow-app-navbar-mobile w-full z-navbar bg-natural-50 md:block"
86+
)}
87+
>
2388
<div
24-
className={cn("flex justify-between gap-8 items-center py-6 px-4", {
25-
"max-w-screen-3xl mx-auto": location.pathname !== Web.Chat,
26-
})}
89+
className={cn(
90+
"flex justify-center md:justify-between gap-8 items-center py-6 px-4",
91+
{
92+
"max-w-screen-3xl mx-auto": location.pathname !== Web.Chat,
93+
}
94+
)}
2795
>
28-
<div className="hidden md:block">
29-
<Subscription />
30-
</div>
96+
{nextLessonId ? (
97+
<LessonTimer
98+
start={nextLessonStart}
99+
tutorName={tutorName}
100+
nextLessonId={nextLessonId}
101+
loading={lessons.query.isLoading}
102+
error={lessons.query.isError}
103+
/>
104+
) : null}
105+
106+
{!nextLessonId ? <Subscription /> : null}
31107

32-
<div className="ms-auto flex items-center justify-center">
108+
<div className="hidden ms-auto md:flex items-center justify-center">
33109
<User />
34110
</div>
35111
</div>
36112
</div>
37113
);
38114
};
39115

116+
const LessonTimer: React.FC<{
117+
start?: string;
118+
tutorName?: string | null;
119+
nextLessonId?: number;
120+
loading: boolean;
121+
error: boolean;
122+
}> = ({ start, tutorName, nextLessonId, loading, error }) => {
123+
const intl = useFormatMessage();
124+
125+
const [time, setTime] = useState<number | null>(null); // milliseconds to current time.
126+
127+
useEffect(() => {
128+
const interval = setInterval(() => {
129+
const timeToNextLesson = dayjs(start).diff(dayjs(), "seconds");
130+
setTime(timeToNextLesson);
131+
}, 1000);
132+
133+
return () => clearInterval(interval);
134+
}, [nextLessonId, start]);
135+
136+
const { hours, minutes, seconds } = useGetTime(time);
137+
138+
const diff = useMemo(() => dayjs(start).diff(dayjs(), "minutes"), [start]);
139+
140+
if (
141+
dayjs().isAfter(dayjs(start)) &&
142+
dayjs().diff(dayjs(start), "minutes") > MAX_LESSON_DURATION
143+
)
144+
return;
145+
146+
if (!nextLessonId || loading || error) return;
147+
148+
if (diff <= LESSON_NOTICE_MINUTES)
149+
return (
150+
<div className="flex items-center gap-6">
151+
<Typography
152+
tag="p"
153+
className="text-caption text-natural-700 max-w-[196px]"
154+
>
155+
{diff > 0
156+
? intl("navbar.can-join-lesson", { value: tutorName })
157+
: null}
158+
159+
{diff <= 0 && Math.abs(diff) < MAX_LESSON_DURATION
160+
? intl("navbar.lesson-has-started", { value: tutorName })
161+
: null}
162+
</Typography>
163+
<Link
164+
to={router.web({ route: Web.Lesson, id: nextLessonId })}
165+
tabIndex={-1}
166+
>
167+
<Button size="large">
168+
<Typography tag="span" className="text-body font-medium">
169+
{intl("navbar.buttons.enter-lesson-now")}
170+
</Typography>
171+
</Button>
172+
</Link>
173+
</div>
174+
);
175+
176+
if (!time) return;
177+
178+
return (
179+
<div className="flex flex-col items-center">
180+
<Typography tag="p" className="text-tiny font-semibold text-natural-600">
181+
{intl("navbar.lesson-timer")}
182+
</Typography>
183+
<Typography
184+
tag="p"
185+
className="[direction:ltr] text-body font-bold text-brand-500"
186+
>
187+
{`${hours} : ${String(minutes).padStart(2, "0")} : ${String(seconds).padStart(2, "0")}`}
188+
</Typography>
189+
</div>
190+
);
191+
};
192+
40193
const Subscription: React.FC = () => {
41194
const { user } = useUser();
42195
const { info, remainingWeeklyMinutes, loading } = useSubscription();
@@ -49,29 +202,7 @@ const Subscription: React.FC = () => {
49202

50203
if (loading || !user || isTutorRole(user.role)) return null;
51204

52-
if (!info || ended)
53-
return (
54-
<Link
55-
onClick={() => {
56-
track("click_subscribe", "navbar");
57-
}}
58-
to={Web.Plans}
59-
tabIndex={-1}
60-
>
61-
<Button
62-
size="large"
63-
htmlType="button"
64-
endIcon={<Crown className="[&>*]:stroke-natural-50" />}
65-
>
66-
<Typography
67-
tag="span"
68-
className="text-natural-50 text-body font-bold"
69-
>
70-
{intl("navbar.subscription.subscribe-now")}
71-
</Typography>
72-
</Button>
73-
</Link>
74-
);
205+
if (!info || ended) return;
75206

76207
return (
77208
<Tooltip
@@ -149,4 +280,26 @@ const User: React.FC = () => {
149280
);
150281
};
151282

283+
const useGetTime = (
284+
time: number | null
285+
): { hours: number; minutes: number; seconds: number } => {
286+
if (!time) return { hours: 0, minutes: 0, seconds: 0 };
287+
// Seconds remaining for the next lesson
288+
const totalSeconds = Math.floor(time);
289+
290+
// Hours remiaing for the next lesson
291+
const hours = Math.floor(totalSeconds / SECONDS_IN_HOUR);
292+
// Minutes remaining for the next lesson
293+
const minutes = Math.floor(
294+
(totalSeconds %
295+
(Math.floor(totalSeconds / SECONDS_IN_HOUR) * SECONDS_IN_HOUR)) /
296+
SECONDS_IN_MINUTE
297+
);
298+
// Seconds remaining for the next lesson
299+
const seconds =
300+
(totalSeconds % (hours * SECONDS_IN_HOUR)) % (minutes * SECONDS_IN_MINUTE);
301+
302+
return { hours, minutes, seconds };
303+
};
304+
152305
export default Navbar;

apps/web/src/pages/Chat.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ const Chat: React.FC = () => {
118118
<div
119119
className={cn(
120120
"w-full mx-auto flex flex-row overflow-hidden grow",
121-
"max-h-[calc(100vh-165px)] md:max-h-chat-tablet lg:max-h-chat-desktop"
121+
"md:max-h-chat-tablet lg:max-h-chat-desktop",
122+
{
123+
"max-h-[calc(100vh-165px)]": !location.search.includes("room"),
124+
}
122125
)}
123126
>
124127
{(!temporaryTutor && !otherMember) || mq.lg ? (

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1003,14 +1003,17 @@
10031003
"plan.instead-of": "بدلا من {value}",
10041004
"plan.price": "{value} جنيه مصري",
10051005
"plan.subscribe-now": "اشترك الآن",
1006-
"navbar.subscription.subscribe-now": "اشترك الآن",
10071006
"navbar.subscription.personal-quota": "الباقة الشخصية",
10081007
"navbar.subscription.quota-consumption": "تم استهلاك {value}",
10091008
"navbar.subscription.rest-of-quota": "متبقي {value} من هذا الأسبوع",
10101009
"navbar.subscription.tooltip": "رصيد دقائقك يتجدد تلقائيًا كل صباح يوم {day}.",
10111010
"navbar.main": "الرئيسية",
10121011
"navbar.register": "التسجيل",
10131012
"navbar.login": "تسجيل الدخول",
1013+
"navbar.lesson-timer": "الوقت المتبقي علي الحصة القادمة",
1014+
"navbar.can-join-lesson": "يمكنك دخول حصتك مع {value}",
1015+
"navbar.lesson-has-started": "لقد بدأت حصتك مع {value}",
1016+
"navbar.buttons.enter-lesson-now": "الدخول الآن",
10141017
"sidebar.main": "الرئيسية",
10151018
"sidebar.dashboard": "لوحة التحكم",
10161019
"sidebar.tutors": "المعلمين",

0 commit comments

Comments
 (0)