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
2 changes: 1 addition & 1 deletion apps/web/src/components/Chat/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ const Messages: React.FC<{
) : null}

{room ? (
<div className={"flex flex-col h-[85%] gap-2"}>
<div className={"flex flex-col min-h-[85%] gap-2"}>
<div
id="messages-content"
className={cn(
Expand Down
216 changes: 187 additions & 29 deletions apps/web/src/components/Layout/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,198 @@
import cn from "classnames";
import dayjs from "dayjs";
import React, { useCallback, useMemo } from "react";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Link, useNavigate } from "react-router-dom";

import { ProfileInfo, SubscriptionQuota } from "@/components/Navbar";
import { useSaveLogs } from "@/hooks/logger";
import Crown from "@litespace/assets/Crown";
import { router } from "@/lib/routes";
import { useSubscription } from "@litespace/headless/context/subscription";
import { useUser } from "@litespace/headless/context/user";
import { useFindLessons } from "@litespace/headless/lessons";
import { useMediaQuery } from "@litespace/headless/mediaQuery";
import { IUser } from "@litespace/types";
import { Button } from "@litespace/ui/Button";
import { useFormatMessage } from "@litespace/ui/hooks/intl";
import { Tooltip } from "@litespace/ui/Tooltip";
import { Typography } from "@litespace/ui/Typography";
import { isTutorRole } from "@litespace/utils";
import {
isTutorRole,
MAX_LESSON_DURATION,
MINUTES_IN_HOUR,
SECONDS_IN_MINUTE,
} from "@litespace/utils";
import { Web } from "@litespace/utils/routes";
import { track } from "@/lib/analytics";
import { first, isEmpty } from "lodash";
import Crown from "@litespace/assets/Crown";

const LESSON_NOTICE_MINUTES = 3;
const SECONDS_IN_HOUR = SECONDS_IN_MINUTE * MINUTES_IN_HOUR;

const Navbar: React.FC = () => {
const { md } = useMediaQuery();
const { info } = useSubscription();
const { user } = useUser();
const now = useRef<string | undefined>(undefined);

useEffect(() => {
now.current = dayjs()
.subtract(MAX_LESSON_DURATION, "minutes")
.toISOString();
}, []);

const lessons = useFindLessons({
userOnly: true,
users: user ? [user.id] : [],
canceled: false,
after: now.current,
size: 1,
});

const { nextLessonStart, nextLessonId, tutorName } = useMemo(() => {
const nextLesson = first(
lessons.query.data?.list.filter(({ lesson }) => !lesson.canceledAt)
)?.lesson;

const tutor = first(lessons.query.data?.list)?.members.find(
(member) => member.role !== IUser.Role.Student
);

return {
nextLessonStart: nextLesson?.start,
nextLessonId: nextLesson?.id,
tutorName: tutor?.name,
};
}, [lessons.query.data?.list]);

if (
(!md && !info && isEmpty(lessons.query.data?.list)) ||
(!md && location.pathname.split("/").includes("lesson")) ||
(!md &&
location.pathname.includes("chat") &&
location.search.includes("room") &&
!location.search.includes("null"))
)
return;

return (
<div className="hidden md:block shadow-app-navbar shadow lg:shadow-app-navbar-mobile w-full z-navbar bg-natural-50">
<div
className={cn(
"shadow-app-navbar shadow lg:shadow-app-navbar-mobile w-full z-navbar bg-natural-50 md:block"
)}
>
<div
className={cn("flex justify-between gap-8 items-center py-6 px-4", {
"max-w-screen-3xl mx-auto": location.pathname !== Web.Chat,
})}
className={cn(
"flex justify-center md:justify-between gap-8 items-center py-6 px-4",
{
"max-w-screen-3xl mx-auto": location.pathname !== Web.Chat,
}
)}
>
<div className="hidden md:block">
<Subscription />
</div>
{nextLessonId ? (
<LessonTimer
start={nextLessonStart}
tutorName={tutorName}
nextLessonId={nextLessonId}
loading={lessons.query.isLoading}
error={lessons.query.isError}
/>
) : null}

{!nextLessonId ? <Subscription /> : null}

<div className="ms-auto flex items-center justify-center">
<div className="hidden ms-auto md:flex items-center justify-center">
<User />
</div>
</div>
</div>
);
};

const LessonTimer: React.FC<{
start?: string;
tutorName?: string | null;
nextLessonId?: number;
loading: boolean;
error: boolean;
}> = ({ start, tutorName, nextLessonId, loading, error }) => {
const intl = useFormatMessage();

const [time, setTime] = useState<number | null>(null); // milliseconds to current time.

useEffect(() => {
const interval = setInterval(() => {
const timeToNextLesson = dayjs(start).diff(dayjs(), "seconds");
setTime(timeToNextLesson);
}, 1000);

return () => clearInterval(interval);
}, [nextLessonId, start]);

const { hours, minutes, seconds } = useGetTime(time);

const diff = useMemo(() => dayjs(start).diff(dayjs(), "minutes"), [start]);

if (
dayjs().isAfter(dayjs(start)) &&
dayjs().diff(dayjs(start), "minutes") > MAX_LESSON_DURATION
)
return;

if (!nextLessonId || error) return;

if (diff <= LESSON_NOTICE_MINUTES)
return (
<div className="flex items-center gap-6">
<Typography
tag="p"
className="text-caption text-natural-700 max-w-[196px]"
>
{diff > 0
? intl("navbar.can-join-lesson", { value: tutorName })
: null}

{diff <= 0 && Math.abs(diff) < MAX_LESSON_DURATION
? intl("navbar.lesson-has-started", { value: tutorName })
: null}
</Typography>
<Link
to={router.web({ route: Web.Lesson, id: nextLessonId })}
tabIndex={-1}
>
<Button size="large">
<Typography tag="span" className="text-body font-medium">
{intl("navbar.buttons.enter-lesson-now")}
</Typography>
</Button>
</Link>
</div>
);

return (
<div className="flex flex-col items-center">
<Typography tag="p" className="text-tiny font-semibold text-natural-600">
{intl("navbar.lesson-timer")}
</Typography>
<Typography
tag="p"
className="[direction:ltr] text-body font-bold text-brand-500"
>
<span>{loading ? 0 : hours}</span>
<span className="mx-[11px]"> : </span>
<span>{loading ? 0 : String(minutes).padStart(2, "0")}</span>
<span className="mx-[11px]"> : </span>
<span>{loading ? 0 : String(seconds).padStart(2, "0")}</span>
</Typography>
</div>
);
};

const Subscription: React.FC = () => {
const { user } = useUser();
const { info, remainingWeeklyMinutes, loading } = useSubscription();
Expand All @@ -51,23 +207,10 @@ const Subscription: React.FC = () => {

if (!info || ended)
return (
<Link
onClick={() => {
track("click_subscribe", "navbar");
}}
to={Web.Plans}
tabIndex={-1}
>
<Button
size="large"
htmlType="button"
endIcon={<Crown className="[&>*]:stroke-natural-50" />}
>
<Typography
tag="span"
className="text-natural-50 text-body font-bold"
>
{intl("navbar.subscription.subscribe-now")}
<Link to={Web.Plans} tabIndex={-1}>
<Button size="large" endIcon={<Crown className="icon" />}>
<Typography tag="span" className="text-body font-medium">
{intl("navbar.buttons.subscribe-now")}
</Typography>
</Button>
</Link>
Expand Down Expand Up @@ -149,4 +292,19 @@ const User: React.FC = () => {
);
};

const useGetTime = (
time: number | null
): { hours: number; minutes: number; seconds: number } => {
if (!time) return { hours: 0, minutes: 0, seconds: 0 };
const totalSeconds = Math.floor(time);

const hours = Math.floor(totalSeconds / SECONDS_IN_HOUR);
const minutes = Math.floor(
(totalSeconds % SECONDS_IN_HOUR) / SECONDS_IN_MINUTE
);
const seconds = totalSeconds % SECONDS_IN_MINUTE;

return { hours, minutes, seconds };
};

export default Navbar;
5 changes: 4 additions & 1 deletion apps/web/src/pages/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ const Chat: React.FC = () => {
<div
className={cn(
"w-full mx-auto flex flex-row overflow-hidden grow",
"max-h-[calc(100vh-165px)] md:max-h-chat-tablet lg:max-h-chat-desktop"
"md:max-h-chat-tablet lg:max-h-chat-desktop",
{
"max-h-[calc(100vh-165px)]": !location.search.includes("room"),
}
)}
>
{(!temporaryTutor && !otherMember) || mq.lg ? (
Expand Down
6 changes: 5 additions & 1 deletion packages/ui/src/locales/ar-eg.json
Original file line number Diff line number Diff line change
Expand Up @@ -1028,14 +1028,18 @@
"plan.instead-of": "بدلا من {value}",
"plan.price": "{value} جنيه مصري",
"plan.subscribe-now": "اشترك الآن",
"navbar.subscription.subscribe-now": "اشترك الآن",
"navbar.subscription.personal-quota": "الباقة الشخصية",
"navbar.subscription.quota-consumption": "تم استهلاك {value}",
"navbar.subscription.rest-of-quota": "متبقي {value} من هذا الأسبوع",
"navbar.subscription.tooltip": "رصيد دقائقك يتجدد تلقائيًا كل صباح يوم {day}.",
"navbar.main": "الرئيسية",
"navbar.register": "التسجيل",
"navbar.login": "تسجيل الدخول",
"navbar.lesson-timer": "الوقت المتبقي علي الحصة القادمة",
"navbar.can-join-lesson": "يمكنك دخول حصتك مع {value}",
"navbar.lesson-has-started": "لقد بدأت حصتك مع {value}",
"navbar.buttons.enter-lesson-now": "الدخول الآن",
"navbar.buttons.subscribe-now": "اشترك الآن",
"sidebar.main": "الرئيسية",
"sidebar.dashboard": "لوحة التحكم",
"sidebar.tutors": "المعلمين",
Expand Down