Quran_Tech_Server / F_Pro /src /components /sessions /SessionEndWaiting.tsx
aboalaa147's picture
Initial deployment
eb6a2f9
Raw
History Blame Contribute Delete
8.39 kB
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>
);
}