11import cn from "classnames" ;
22import 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" ;
410import { Link , useNavigate } from "react-router-dom" ;
511
612import { ProfileInfo , SubscriptionQuota } from "@/components/Navbar" ;
713import { useSaveLogs } from "@/hooks/logger" ;
8- import Crown from "@litespace/assets/Crown " ;
14+ import { router } from "@/lib/routes " ;
915import { useSubscription } from "@litespace/headless/context/subscription" ;
1016import { useUser } from "@litespace/headless/context/user" ;
17+ import { useFindLessons } from "@litespace/headless/lessons" ;
18+ import { useMediaQuery } from "@litespace/headless/mediaQuery" ;
1119import { IUser } from "@litespace/types" ;
1220import { Button } from "@litespace/ui/Button" ;
1321import { useFormatMessage } from "@litespace/ui/hooks/intl" ;
1422import { Tooltip } from "@litespace/ui/Tooltip" ;
1523import { 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" ;
1730import { 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
2036const 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+
40193const 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+
152305export default Navbar ;
0 commit comments