import { useEffect, useState, useRef } from 'react'; import { useParams, useNavigate, useLocation } from 'react-router-dom'; import './ComicReader.css'; interface ComicImage { index: number; imageUrl: string; } interface ChapterInfo { slug: string; chapter: string; } interface ReaderData { slug: string; title: string; images: ComicImage[]; totalImages: number; prevChapter?: ChapterInfo | null; nextChapter?: ChapterInfo | null; allChapters?: ChapterInfo[]; } export function ComicReader() { const { sessionId } = useParams(); const navigate = useNavigate(); const location = useLocation(); const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [showMenu, setShowMenu] = useState(false); const [showControls, setShowControls] = useState(true); const [showPageJump, setShowPageJump] = useState(false); const [jumpPage, setJumpPage] = useState(''); const [loadingChapter, setLoadingChapter] = useState(false); const [password, setPassword] = useState(''); const [originalSessionId, setOriginalSessionId] = useState(''); const imageRefs = useRef<{ [key: number]: HTMLImageElement | null }>({}); const controlsTimeout = useRef(null); const scrollContainerRef = useRef(null); const wsRef = useRef(null); const chapterData = location.state?.chapterData; useEffect(() => { const statePassword = location.state?.password; const stateSessionId = location.state?.sessionId; if (statePassword) { setPassword(statePassword); } if (stateSessionId) { setOriginalSessionId(stateSessionId); } }, [location.state]); useEffect(() => { if (!password || !originalSessionId) return; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`; const ws = new WebSocket(wsUrl); let isConnected = false; wsRef.current = ws; ws.onopen = () => { isConnected = true; }; ws.onmessage = (event) => { try { const message = JSON.parse(event.data); if (message.type === 'chapter_data_response') { setLoadingChapter(false); if (message.success && message.data) { const newData = { ...message.data, allChapters: message.data.allChapters || data?.allChapters || [] }; setData(newData); setCurrentPage(1); scrollContainerRef.current?.scrollTo({ top: 0 }); } else { setError(message.error || 'Failed to load chapter'); setTimeout(() => setError(''), 3000); } } if (message.type === 'session_force_closed') { setError('Session dihapus oleh admin. Silakan buat session baru.'); sessionStorage.removeItem(`comic_${sessionId}`); setTimeout(() => { navigate('/'); }, 3000); } } catch (err) { console.error('[Reader WS] Parse error:', err); } }; ws.onerror = (err) => { console.error('[Reader WS] Error:', err); setLoadingChapter(false); if (!isConnected) { setError('Failed to connect to server'); } }; ws.onclose = () => { wsRef.current = null; }; return () => { if (ws.readyState === WebSocket.OPEN) { ws.close(); } }; }, [password, originalSessionId]); useEffect(() => { if (chapterData) { setData(chapterData); setLoading(false); return; } if (!password || !originalSessionId) { setError('Unauthorized access - please open from comic page'); setLoading(false); return; } setError('Please open chapter from comic page'); setLoading(false); }, [sessionId, password, chapterData, originalSessionId]); useEffect(() => { const resetTimeout = () => { if (controlsTimeout.current) { clearTimeout(controlsTimeout.current); } setShowControls(true); controlsTimeout.current = window.setTimeout(() => { setShowControls(false); setShowMenu(false); }, 3000); }; const handleMove = () => resetTimeout(); window.addEventListener('mousemove', handleMove); window.addEventListener('touchstart', handleMove); return () => { window.removeEventListener('mousemove', handleMove); window.removeEventListener('touchstart', handleMove); if (controlsTimeout.current) { clearTimeout(controlsTimeout.current); } }; }, []); useEffect(() => { const handleScroll = () => { if (!scrollContainerRef.current) return; const container = scrollContainerRef.current; const windowHeight = container.clientHeight; let currentIdx = 1; for (let i = 1; i <= (data?.totalImages || 0); i++) { const img = imageRefs.current[i]; if (img) { const rect = img.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); const imgCenter = rect.top + rect.height / 2 - containerRect.top; if (imgCenter > 0 && imgCenter < windowHeight) { currentIdx = i; break; } } } setCurrentPage(currentIdx); }; const container = scrollContainerRef.current; if (container) { container.addEventListener('scroll', handleScroll); return () => container.removeEventListener('scroll', handleScroll); } }, [data]); const scrollToPage = (page: number) => { const img = imageRefs.current[page]; if (img && scrollContainerRef.current) { const container = scrollContainerRef.current; const containerRect = container.getBoundingClientRect(); const imgRect = img.getBoundingClientRect(); const scrollTo = container.scrollTop + (imgRect.top - containerRect.top); container.scrollTo({ top: scrollTo, behavior: 'smooth' }); } }; const nextChapter = () => { if (data?.nextChapter) { handleChapterNavigation(data.nextChapter.slug); } }; const prevChapter = () => { if (data?.prevChapter) { handleChapterNavigation(data.prevChapter.slug); } }; const handlePageJump = () => { const page = parseInt(jumpPage); if (page >= 1 && page <= (data?.totalImages || 1)) { scrollToPage(page); setShowPageJump(false); setJumpPage(''); } }; const handleChapterNavigation = (chapterSlug: string) => { if (!password || !originalSessionId) { setError('Missing authentication data'); return; } if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) { if (wsRef.current?.readyState === WebSocket.CONNECTING) { setTimeout(() => { if (wsRef.current?.readyState === WebSocket.OPEN) { handleChapterNavigation(chapterSlug); } else { setError('Connection not ready. Please try again.'); } }, 1000); return; } setError('Connection lost. Please refresh the page.'); console.error('[Reader] WebSocket not connected. State:', wsRef.current?.readyState); return; } setLoadingChapter(true); setShowMenu(false); const requestData = { type: 'request_chapter_data', sessionId: originalSessionId, password: password, chapterSlug: chapterSlug, allChapters: data?.allChapters || [] }; wsRef.current.send(JSON.stringify(requestData)); }; if (loading) { return (

Loading chapter...

); } if (loadingChapter) { return (

Loading chapter...

); } if (error || !data) { return (
⚠️

Error

{error || 'Failed to load chapter'}

); } return (

{data.title}

{currentPage} / {data.totalImages}
{showMenu && (
setShowMenu(false)}>
e.stopPropagation()}>

Navigation

{(data.prevChapter || data.nextChapter) && (

Chapters

{data.prevChapter && ( )} {data.nextChapter && ( )}
)}

Info

Total Pages: {data.totalImages}

Current: Page {currentPage}

)} {showPageJump && (
setShowPageJump(false)}>
e.stopPropagation()}>

Jump to Page

setJumpPage(e.target.value)} placeholder={`1-${data.totalImages}`} autoFocus onKeyPress={(e) => e.key === 'Enter' && handlePageJump()} />
)}
{data.images.map((img) => ( (imageRefs.current[img.index] = el)} src={img.imageUrl} alt={`Page ${img.index}`} className="comic-page" loading="lazy" onError={(e) => { (e.target as HTMLImageElement).src = 'data:image/svg+xml,Failed to load page ' + img.index + ''; }} /> ))}
{currentPage} / {data.totalImages} scrollToPage(parseInt(e.target.value))} className="page-slider" />
); }