Spaces:
Running
Running
| import { useEffect, useState, useRef } from 'react'; | |
| import { useParams, useNavigate } from 'react-router-dom'; | |
| import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; | |
| import { Button } from '../ui/button'; | |
| import { Clock, ArrowLeft } from 'lucide-react'; | |
| import { useLanguage } from '../../lib/languageContext'; | |
| import { useAuth } from '../../lib/authContext'; | |
| import { getApiBaseUrl } from '../../lib/api/config'; | |
| const POLL_INTERVAL_MS = 5000; | |
| const translations = { | |
| ar: { | |
| title: 'في انتظار انتهاء الجلسة', | |
| subtitle: 'الجلسة لا تزال جارية', | |
| waitingMessage: 'الشيخ سيُنهي الجلسة عند الانتهاء من الدرس. ستُنقل تلقائيًا إلى تقرير الجلسة.', | |
| with: 'مع', | |
| scheduledAt: 'وقت الجلسة', | |
| backToSessions: 'العودة إلى الجلسات', | |
| sessionEnded: 'انتهت الجلسة، جارٍ التحويل...', | |
| sessionMissed: 'لم تُعقد الجلسة، جارٍ التحويل...', | |
| loading: 'جاري التحميل...', | |
| error: 'تعذّر تحميل بيانات الجلسة.', | |
| }, | |
| en: { | |
| title: 'Waiting for Session to End', | |
| subtitle: 'Session is still ongoing', | |
| waitingMessage: 'The sheikh will end the session when the lesson is complete. You will be redirected to the session report automatically.', | |
| with: 'with', | |
| scheduledAt: 'Scheduled at', | |
| backToSessions: 'Back to Sessions', | |
| sessionEnded: 'Session ended, redirecting...', | |
| sessionMissed: 'Session missed, redirecting...', | |
| loading: 'Loading...', | |
| error: 'Could not load session details.', | |
| }, | |
| }; | |
| export function SessionEndWaiting() { | |
| const { sessionId } = useParams<{ sessionId: string }>(); | |
| const navigate = useNavigate(); | |
| const { language } = useLanguage(); | |
| const { user } = useAuth(); | |
| const t = translations[language as 'ar' | 'en'] ?? translations.en; | |
| const isRTL = language === 'ar'; | |
| const [sessionData, setSessionData] = useState<any>(null); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| const [statusMessage, setStatusMessage] = useState<string | null>(null); | |
| const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null); | |
| const handleTerminalStatus = (status: string, id: string) => { | |
| if (status === 'COMPLETED') { | |
| setStatusMessage(t.sessionEnded); | |
| navigate(`/session-report/${id}?mode=review`); | |
| } else if (status === 'MISSED' || status === 'CANCELLED') { | |
| setStatusMessage(t.sessionMissed); | |
| navigate(`/session-report/${id}?mode=review`); | |
| } | |
| }; | |
| const fetchSession = async (): Promise<string | null> => { | |
| if (!sessionId || !user) return null; | |
| try { | |
| const token = localStorage.getItem('authToken'); | |
| const res = await fetch(`${getApiBaseUrl()}/api/student/sessions/${sessionId}`, { | |
| headers: { Authorization: `Bearer ${token}` }, | |
| }); | |
| if (!res.ok) return null; | |
| const data = await res.json(); | |
| return data.sessionStatus ?? data.status ?? null; | |
| } catch { | |
| return null; | |
| } | |
| }; | |
| // Initial load | |
| useEffect(() => { | |
| const init = async () => { | |
| if (!sessionId || !user) return; | |
| try { | |
| const token = localStorage.getItem('authToken'); | |
| const res = await fetch(`${getApiBaseUrl()}/api/student/sessions/${sessionId}`, { | |
| headers: { Authorization: `Bearer ${token}` }, | |
| }); | |
| if (!res.ok) throw new Error('not found'); | |
| const data = await res.json(); | |
| setSessionData(data); | |
| const status: string = data.sessionStatus ?? data.status ?? ''; | |
| // Immediate redirect if already terminal | |
| if (status === 'COMPLETED' || status === 'MISSED' || status === 'CANCELLED') { | |
| handleTerminalStatus(status, sessionId); | |
| return; | |
| } | |
| } catch { | |
| setError(t.error); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| init(); | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [sessionId, user]); | |
| // Polling | |
| useEffect(() => { | |
| if (loading || error || !sessionId) return; | |
| intervalRef.current = setInterval(async () => { | |
| const status = await fetchSession(); | |
| if (!status) return; // silently ignore failed polls | |
| if (status === 'COMPLETED' || status === 'MISSED' || status === 'CANCELLED') { | |
| if (intervalRef.current) clearInterval(intervalRef.current); | |
| handleTerminalStatus(status, sessionId); | |
| } | |
| }, POLL_INTERVAL_MS); | |
| return () => { | |
| if (intervalRef.current) clearInterval(intervalRef.current); | |
| }; | |
| // eslint-disable-next-line react-hooks/exhaustive-deps | |
| }, [loading, error, sessionId]); | |
| if (loading) { | |
| return ( | |
| <div className="min-h-screen flex items-center justify-center text-muted-foreground"> | |
| {t.loading} | |
| </div> | |
| ); | |
| } | |
| if (error || !sessionData) { | |
| return ( | |
| <div className="min-h-screen flex items-center justify-center p-6"> | |
| <Card className="max-w-md w-full"> | |
| <CardContent className="pt-6 text-center space-y-4"> | |
| <p className="text-destructive">{error ?? t.error}</p> | |
| <Button variant="outline" onClick={() => navigate('/student/sessions')}> | |
| <ArrowLeft className="h-4 w-4 mr-2" /> | |
| {t.backToSessions} | |
| </Button> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ); | |
| } | |
| const scheduledDate = sessionData.scheduledStart | |
| ? new Date(sessionData.scheduledStart).toLocaleString(language === 'ar' ? 'ar-EG' : 'en-US', { | |
| weekday: 'short', month: 'short', day: 'numeric', | |
| hour: '2-digit', minute: '2-digit', | |
| }) | |
| : ''; | |
| return ( | |
| <div | |
| className="min-h-screen bg-gradient-to-br from-emerald-50 to-slate-100 flex items-center justify-center p-6" | |
| dir={isRTL ? 'rtl' : 'ltr'} | |
| > | |
| <Card className="max-w-lg w-full shadow-2xl border-none overflow-hidden"> | |
| <div className="bg-emerald-600 h-2 w-full" /> | |
| <CardHeader className="text-center pb-2"> | |
| <div className="w-20 h-20 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-4"> | |
| {/* Animated pulsing ring */} | |
| <span className="relative flex h-12 w-12"> | |
| <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-50" /> | |
| <span className="relative inline-flex rounded-full h-12 w-12 bg-emerald-500 items-center justify-center"> | |
| <Clock className="h-6 w-6 text-white" /> | |
| </span> | |
| </span> | |
| </div> | |
| <CardTitle className="text-2xl font-extrabold text-slate-800">{t.title}</CardTitle> | |
| <p className="text-slate-500 text-sm">{t.subtitle}</p> | |
| </CardHeader> | |
| <CardContent className="space-y-6 p-8"> | |
| {/* Session info */} | |
| <div className="bg-white rounded-xl p-5 border border-slate-100 shadow-sm space-y-3"> | |
| <div className={`flex justify-between items-center text-sm ${isRTL ? 'flex-row-reverse' : ''}`}> | |
| <span className="text-slate-400 font-semibold uppercase tracking-wide">{t.with}</span> | |
| <span className="font-bold text-slate-700">{sessionData.sheikhName}</span> | |
| </div> | |
| {scheduledDate && ( | |
| <div className={`flex justify-between items-center text-sm ${isRTL ? 'flex-row-reverse' : ''}`}> | |
| <span className="text-slate-400 font-semibold uppercase tracking-wide">{t.scheduledAt}</span> | |
| <span className="font-medium text-slate-600">{scheduledDate}</span> | |
| </div> | |
| )} | |
| </div> | |
| {/* Waiting message */} | |
| <div | |
| data-testid="waiting-message" | |
| className="bg-emerald-50 border border-emerald-200 rounded-xl p-4 text-emerald-800 text-sm leading-relaxed text-center" | |
| > | |
| {statusMessage ?? t.waitingMessage} | |
| </div> | |
| <Button | |
| variant="ghost" | |
| className="w-full text-slate-500 hover:text-slate-800" | |
| onClick={() => navigate('/student/sessions')} | |
| > | |
| <ArrowLeft className={`h-4 w-4 ${isRTL ? 'ml-2' : 'mr-2'}`} /> | |
| {t.backToSessions} | |
| </Button> | |
| </CardContent> | |
| </Card> | |
| </div> | |
| ); | |
| } | |