Spaces:
Sleeping
Sleeping
| import { useState, useEffect, useRef } from 'react'; | |
| import { useParams, useNavigate } from 'react-router-dom'; | |
| import { Button } from '../ui/button'; | |
| import { Badge } from '../ui/badge'; | |
| import { Textarea } from '../ui/textarea'; | |
| import { | |
| Video, VideoOff, Mic, MicOff, PhoneOff, | |
| MessageSquare, Wifi, AlertTriangle, Clock, | |
| } from 'lucide-react'; | |
| import { useAuth } from '../../lib/authContext'; | |
| import { useWebRTC } from '../../hooks/useWebRTC'; | |
| import { getApiBaseUrl } from '../../lib/api/config'; | |
| const BASE_URL = getApiBaseUrl(); | |
| /** | |
| * The sheikh's side of the verification interview. | |
| * Joins the same WebRTC room as the admin (room = "interview-{sheikhId}"). | |
| * No approve/reject logic — just the video call. | |
| */ | |
| export function SheikhInterviewSession() { | |
| const { sheikhId } = useParams<{ sheikhId: string }>(); | |
| const navigate = useNavigate(); | |
| const { user } = useAuth(); | |
| const roomId = `interview-${sheikhId ?? 'unknown'}`; | |
| const [isVideoOn, setIsVideoOn] = useState(true); | |
| const [isAudioOn, setIsAudioOn] = useState(true); | |
| const [sessionTime, setSessionTime] = useState(0); | |
| const [newMessage, setNewMessage] = useState(''); | |
| const [showChat, setShowChat] = useState(false); | |
| const localVideoRef = useRef<HTMLVideoElement>(null); | |
| const remoteVideoRef = useRef<HTMLVideoElement>(null); | |
| const { | |
| localStream, remoteStream, isConnected, | |
| messages, sendChatMessage, | |
| toggleVideo, toggleAudio, connectionQuality, | |
| } = useWebRTC(roomId, user?.id ?? 'sheikh'); | |
| useEffect(() => { | |
| if (localVideoRef.current && localStream) localVideoRef.current.srcObject = localStream; | |
| }, [localStream]); | |
| useEffect(() => { | |
| if (remoteVideoRef.current && remoteStream) { | |
| remoteVideoRef.current.srcObject = remoteStream; | |
| } | |
| }, [remoteStream]); | |
| useEffect(() => { | |
| const timer = setInterval(() => setSessionTime(prev => prev + 1), 1000); | |
| return () => clearInterval(timer); | |
| }, []); | |
| // Poll every 10s for admin's decision — redirect when approved or rejected | |
| useEffect(() => { | |
| const token = localStorage.getItem('authToken') ?? ''; | |
| if (!token) return; | |
| const checkDecision = async () => { | |
| try { | |
| const res = await fetch(`${BASE_URL}/api/auth/verify-profile`, { | |
| headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, | |
| }); | |
| if (!res.ok) return; | |
| const data = await res.json(); | |
| const status: string = data.sheikhApprovalStatus ?? ''; | |
| if (status === 'APPROVED') { | |
| navigate('/sheikh/dashboard', { replace: true }); | |
| } else if (status === 'REJECTED') { | |
| const reason: string = data.rejectionReason ?? ''; | |
| navigate('/sheikh/interview', { | |
| replace: true, | |
| state: { rejected: true, reason }, | |
| }); | |
| } | |
| } catch { | |
| // silent — background poll, don't disrupt UI | |
| } | |
| }; | |
| const timer = setInterval(checkDecision, 10_000); | |
| return () => clearInterval(timer); | |
| }, [navigate]); | |
| const formatTime = (seconds: number) => { | |
| const h = Math.floor(seconds / 3600); | |
| const m = Math.floor((seconds % 3600) / 60); | |
| const s = seconds % 60; | |
| return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; | |
| }; | |
| const handleSendMessage = () => { | |
| if (newMessage.trim()) { sendChatMessage(newMessage); setNewMessage(''); } | |
| }; | |
| const handleLeave = () => { | |
| navigate('/sheikh/interview'); | |
| }; | |
| const getConnectionColor = () => { | |
| switch (connectionQuality) { | |
| case 'good': return 'text-emerald-500'; | |
| case 'fair': return 'text-yellow-500'; | |
| case 'poor': return 'text-red-500'; | |
| default: return 'text-gray-500'; | |
| } | |
| }; | |
| const getConnectionLabel = () => { | |
| if (!isConnected) return 'Connecting...'; | |
| switch (connectionQuality) { | |
| case 'good': return 'Excellent'; | |
| case 'fair': return 'Stable'; | |
| case 'poor': return 'Poor Connection'; | |
| default: return 'Waiting for Admin...'; | |
| } | |
| }; | |
| return ( | |
| <div className="h-screen bg-gray-900 flex flex-col"> | |
| {/* Header */} | |
| <div className="bg-gray-800 border-b border-gray-700 px-6 py-3"> | |
| <div className="flex items-center justify-between"> | |
| <div> | |
| <h2 className="text-white font-semibold">Verification Interview</h2> | |
| <p className="text-gray-400 text-sm">With the review team</p> | |
| </div> | |
| <div className="flex items-center gap-6"> | |
| <div className={`flex items-center gap-1.5 px-3 py-1 rounded-full bg-gray-700/50 border border-gray-600 ${getConnectionColor()}`}> | |
| {connectionQuality === 'poor' | |
| ? <AlertTriangle className="h-3.5 w-3.5" /> | |
| : <Wifi className="h-3.5 w-3.5" />} | |
| <span className="text-xs font-medium">{getConnectionLabel()}</span> | |
| </div> | |
| <div className="flex items-center gap-2 text-white border-l border-gray-700 pl-6"> | |
| <Clock className="h-4 w-4 text-emerald-500" /> | |
| <span className="font-mono text-sm">{formatTime(sessionTime)}</span> | |
| </div> | |
| <Badge variant="destructive" className="animate-pulse px-3">LIVE</Badge> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Main Content */} | |
| <div className="flex-1 flex overflow-hidden"> | |
| {/* Video Area */} | |
| <div className="flex-1 relative bg-black"> | |
| <div className="absolute inset-0 flex items-center justify-center"> | |
| {/* Remote video always in DOM so ref is always attached */} | |
| <video | |
| ref={remoteVideoRef} | |
| autoPlay | |
| playsInline | |
| className={`w-full h-full object-cover ${remoteStream ? '' : 'hidden'}`} | |
| /> | |
| {!remoteStream && ( | |
| <div className="w-full h-full bg-gradient-to-br from-gray-800 to-gray-900 flex items-center justify-center"> | |
| <div className="text-center"> | |
| <div className="w-32 h-32 rounded-full bg-emerald-600 flex items-center justify-center text-white text-4xl mx-auto mb-4"> | |
| A | |
| </div> | |
| <p className="text-white text-xl">Admin</p> | |
| <p className="text-gray-400 mt-2">Waiting for admin to join...</p> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {/* Local PiP */} | |
| <div className="absolute top-4 right-4 w-48 h-36 bg-black rounded-lg shadow-lg border-2 border-white overflow-hidden"> | |
| <video | |
| ref={localVideoRef} | |
| autoPlay playsInline muted | |
| className={`w-full h-full object-cover ${!isVideoOn ? 'hidden' : ''}`} | |
| /> | |
| {!isVideoOn && ( | |
| <div className="w-full h-full flex items-center justify-center bg-gray-800"> | |
| <div className="text-center"> | |
| <div className="w-12 h-12 rounded-full bg-blue-600 flex items-center justify-center text-white mx-auto mb-1"> | |
| {user?.name?.[0] ?? 'S'} | |
| </div> | |
| <p className="text-white text-xs">You (Video Off)</p> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| {/* Chat Sidebar */} | |
| {showChat && ( | |
| <div className="w-80 bg-gray-800 border-l border-gray-700 flex flex-col"> | |
| <div className="p-4 border-b border-gray-700"> | |
| <h3 className="text-white font-medium">Chat</h3> | |
| </div> | |
| <div className="flex-1 overflow-y-auto p-4 space-y-3"> | |
| {messages.map((msg, i) => ( | |
| <div key={i} className={`flex flex-col ${msg.sender === 'You' ? 'items-end' : 'items-start'}`}> | |
| <div className={`max-w-[80%] rounded-lg p-3 ${msg.sender === 'You' ? 'bg-emerald-600' : 'bg-gray-700'}`}> | |
| <p className="text-white text-sm">{msg.message}</p> | |
| </div> | |
| <p className="text-gray-400 text-xs mt-1">{msg.sender} · {msg.time}</p> | |
| </div> | |
| ))} | |
| </div> | |
| <div className="p-4 border-t border-gray-700 flex gap-2"> | |
| <Textarea | |
| value={newMessage} | |
| onChange={e => setNewMessage(e.target.value)} | |
| onKeyDown={e => e.key === 'Enter' && !e.shiftKey && (e.preventDefault(), handleSendMessage())} | |
| placeholder="Type a message..." | |
| rows={2} | |
| className="bg-gray-700 border-gray-600 text-white" | |
| /> | |
| <Button onClick={handleSendMessage} size="sm">Send</Button> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {/* Controls */} | |
| <div className="bg-gray-800 border-t border-gray-700 px-6 py-4"> | |
| <div className="flex items-center justify-center gap-4"> | |
| <Button | |
| variant={isVideoOn ? 'outline' : 'destructive'} | |
| size="lg" | |
| className={isVideoOn ? 'bg-gray-700 border-gray-600 hover:bg-gray-600 text-white' : ''} | |
| onClick={() => { setIsVideoOn(!isVideoOn); toggleVideo(); }} | |
| > | |
| {isVideoOn ? <Video className="h-5 w-5" /> : <VideoOff className="h-5 w-5" />} | |
| </Button> | |
| <Button | |
| variant={isAudioOn ? 'outline' : 'destructive'} | |
| size="lg" | |
| className={isAudioOn ? 'bg-gray-700 border-gray-600 hover:bg-gray-600 text-white' : ''} | |
| onClick={() => { setIsAudioOn(!isAudioOn); toggleAudio(); }} | |
| > | |
| {isAudioOn ? <Mic className="h-5 w-5" /> : <MicOff className="h-5 w-5" />} | |
| </Button> | |
| <Button | |
| variant={showChat ? 'default' : 'outline'} | |
| size="lg" | |
| className={showChat ? 'bg-emerald-600 text-white hover:bg-emerald-700' : 'bg-gray-700 border-gray-600 hover:bg-gray-600 text-white'} | |
| onClick={() => setShowChat(!showChat)} | |
| > | |
| <MessageSquare className="h-5 w-5" /> | |
| </Button> | |
| <Button variant="destructive" size="lg" onClick={handleLeave}> | |
| <PhoneOff className="h-5 w-5 mr-2" /> | |
| Leave Interview | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |