OhMyDitzzy commited on
Commit
4afc549
·
1 Parent(s): ec4e9ef
src/modules/comic/ComicLanding.tsx CHANGED
@@ -25,7 +25,7 @@ interface ComicData {
25
  export function ComicLanding() {
26
  const { sessionId } = useParams();
27
  const navigate = useNavigate();
28
-
29
  const [isAuth, setIsAuth] = useState(false);
30
  const [password, setPassword] = useState('');
31
  const [comicData, setComicData] = useState<ComicData | null>(null);
@@ -34,7 +34,7 @@ export function ComicLanding() {
34
  const [searchQuery, setSearchQuery] = useState('');
35
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
36
  const [loadingChapter, setLoadingChapter] = useState(false);
37
-
38
  const [ws, setWs] = useState<WebSocket | null>(null);
39
 
40
  useEffect(() => {
@@ -51,13 +51,12 @@ export function ComicLanding() {
51
  sessionStorage.removeItem(`comic_${sessionId}`);
52
  }
53
  }
54
-
55
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
56
  const wsUrl = `${protocol}//${window.location.host}/ws`;
57
  const websocket = new WebSocket(wsUrl);
58
 
59
  websocket.onopen = () => {
60
- console.log('[WS] Connected');
61
  websocket.send(JSON.stringify({
62
  type: 'validate_comic_password',
63
  sessionId
@@ -66,7 +65,7 @@ export function ComicLanding() {
66
 
67
  websocket.onmessage = (event) => {
68
  const message = JSON.parse(event.data);
69
-
70
  if (message.type === 'password_validation_response') {
71
  if (!message.success) {
72
  setError(message.error || 'Session tidak valid');
@@ -87,7 +86,7 @@ export function ComicLanding() {
87
  }));
88
  return currentPassword;
89
  });
90
-
91
  setLoadingChapter(false);
92
  } else {
93
  setError(message.error || 'Gagal memuat data');
@@ -99,21 +98,14 @@ export function ComicLanding() {
99
  setLoadingChapter(false);
100
  if (message.success) {
101
  setPassword((currentPassword) => {
102
- console.log('[ComicLanding] Navigating to reader with:', {
103
- hasData: !!message.data,
104
- hasPassword: !!currentPassword,
105
- password: currentPassword,
106
- sessionId: sessionId
107
- });
108
-
109
- navigate(`/read/${sessionId}_chapter`, {
110
- state: {
111
  chapterData: message.data,
112
  password: currentPassword,
113
  sessionId: sessionId
114
- }
115
  });
116
-
117
  return currentPassword;
118
  });
119
  } else {
@@ -147,7 +139,7 @@ export function ComicLanding() {
147
 
148
  const handlePasswordSubmit = (e: React.FormEvent) => {
149
  e.preventDefault();
150
-
151
  if (!password.trim()) {
152
  setError('Password tidak boleh kosong');
153
  return;
@@ -173,15 +165,8 @@ export function ComicLanding() {
173
  return;
174
  }
175
 
176
- console.log('[ComicLanding] Reading chapter with:', {
177
- chapterSlug,
178
- sessionId,
179
- hasPassword: !!password,
180
- password: password
181
- });
182
-
183
  setLoadingChapter(true);
184
-
185
  ws.send(JSON.stringify({
186
  type: 'request_chapter_data',
187
  sessionId,
@@ -234,7 +219,7 @@ export function ComicLanding() {
234
  <h2>Protected Content</h2>
235
  <p>Masukkan password untuk melanjutkan</p>
236
  </div>
237
-
238
  <form onSubmit={handlePasswordSubmit} className="auth-form">
239
  <div className="input-group">
240
  <input
@@ -246,9 +231,9 @@ export function ComicLanding() {
246
  autoFocus
247
  />
248
  </div>
249
-
250
  {error && <div className="error-message">{error}</div>}
251
-
252
  <button type="submit" className="submit-btn">
253
  Unlock
254
  </button>
@@ -298,13 +283,13 @@ export function ComicLanding() {
298
  <img src={comicData.thumbnailUrl} alt={comicData.title} />
299
  <div className="type-badge">{comicData.type}</div>
300
  </div>
301
-
302
  <div className="hero-info">
303
  <h1 className="comic-title">{comicData.title}</h1>
304
  {comicData.indonesiaTitle && (
305
  <h2 className="comic-subtitle">{comicData.indonesiaTitle}</h2>
306
  )}
307
-
308
  <div className="meta-info">
309
  <div className="meta-item">
310
  <span className="meta-label">Author:</span>
@@ -339,7 +324,7 @@ export function ComicLanding() {
339
  <div className="chapters-section">
340
  <div className="chapters-header">
341
  <h2>Chapters</h2>
342
-
343
  <div className="chapters-controls">
344
  <div className="search-box">
345
  <input
@@ -349,8 +334,8 @@ export function ComicLanding() {
349
  onChange={(e) => setSearchQuery(e.target.value)}
350
  />
351
  </div>
352
-
353
- <button
354
  className="sort-btn"
355
  onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
356
  >
@@ -366,8 +351,8 @@ export function ComicLanding() {
366
  </div>
367
  ) : (
368
  sortedChapters.map((chapter, index) => (
369
- <div
370
- key={index}
371
  className="chapter-item"
372
  onClick={() => handleReadChapter(chapter.slug)}
373
  >
@@ -388,4 +373,4 @@ export function ComicLanding() {
388
  </div>
389
  </div>
390
  );
391
- }
 
25
  export function ComicLanding() {
26
  const { sessionId } = useParams();
27
  const navigate = useNavigate();
28
+
29
  const [isAuth, setIsAuth] = useState(false);
30
  const [password, setPassword] = useState('');
31
  const [comicData, setComicData] = useState<ComicData | null>(null);
 
34
  const [searchQuery, setSearchQuery] = useState('');
35
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
36
  const [loadingChapter, setLoadingChapter] = useState(false);
37
+
38
  const [ws, setWs] = useState<WebSocket | null>(null);
39
 
40
  useEffect(() => {
 
51
  sessionStorage.removeItem(`comic_${sessionId}`);
52
  }
53
  }
54
+
55
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
56
  const wsUrl = `${protocol}//${window.location.host}/ws`;
57
  const websocket = new WebSocket(wsUrl);
58
 
59
  websocket.onopen = () => {
 
60
  websocket.send(JSON.stringify({
61
  type: 'validate_comic_password',
62
  sessionId
 
65
 
66
  websocket.onmessage = (event) => {
67
  const message = JSON.parse(event.data);
68
+
69
  if (message.type === 'password_validation_response') {
70
  if (!message.success) {
71
  setError(message.error || 'Session tidak valid');
 
86
  }));
87
  return currentPassword;
88
  });
89
+
90
  setLoadingChapter(false);
91
  } else {
92
  setError(message.error || 'Gagal memuat data');
 
98
  setLoadingChapter(false);
99
  if (message.success) {
100
  setPassword((currentPassword) => {
101
+ navigate(`/read/${sessionId}_chapter`, {
102
+ state: {
 
 
 
 
 
 
 
103
  chapterData: message.data,
104
  password: currentPassword,
105
  sessionId: sessionId
106
+ }
107
  });
108
+
109
  return currentPassword;
110
  });
111
  } else {
 
139
 
140
  const handlePasswordSubmit = (e: React.FormEvent) => {
141
  e.preventDefault();
142
+
143
  if (!password.trim()) {
144
  setError('Password tidak boleh kosong');
145
  return;
 
165
  return;
166
  }
167
 
 
 
 
 
 
 
 
168
  setLoadingChapter(true);
169
+
170
  ws.send(JSON.stringify({
171
  type: 'request_chapter_data',
172
  sessionId,
 
219
  <h2>Protected Content</h2>
220
  <p>Masukkan password untuk melanjutkan</p>
221
  </div>
222
+
223
  <form onSubmit={handlePasswordSubmit} className="auth-form">
224
  <div className="input-group">
225
  <input
 
231
  autoFocus
232
  />
233
  </div>
234
+
235
  {error && <div className="error-message">{error}</div>}
236
+
237
  <button type="submit" className="submit-btn">
238
  Unlock
239
  </button>
 
283
  <img src={comicData.thumbnailUrl} alt={comicData.title} />
284
  <div className="type-badge">{comicData.type}</div>
285
  </div>
286
+
287
  <div className="hero-info">
288
  <h1 className="comic-title">{comicData.title}</h1>
289
  {comicData.indonesiaTitle && (
290
  <h2 className="comic-subtitle">{comicData.indonesiaTitle}</h2>
291
  )}
292
+
293
  <div className="meta-info">
294
  <div className="meta-item">
295
  <span className="meta-label">Author:</span>
 
324
  <div className="chapters-section">
325
  <div className="chapters-header">
326
  <h2>Chapters</h2>
327
+
328
  <div className="chapters-controls">
329
  <div className="search-box">
330
  <input
 
334
  onChange={(e) => setSearchQuery(e.target.value)}
335
  />
336
  </div>
337
+
338
+ <button
339
  className="sort-btn"
340
  onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
341
  >
 
351
  </div>
352
  ) : (
353
  sortedChapters.map((chapter, index) => (
354
+ <div
355
+ key={index}
356
  className="chapter-item"
357
  onClick={() => handleReadChapter(chapter.slug)}
358
  >
 
373
  </div>
374
  </div>
375
  );
376
+ }
src/modules/comic/ComicReader.tsx CHANGED
@@ -26,7 +26,7 @@ export function ComicReader() {
26
  const { sessionId } = useParams();
27
  const navigate = useNavigate();
28
  const location = useLocation();
29
-
30
  const [data, setData] = useState<ReaderData | null>(null);
31
  const [loading, setLoading] = useState(true);
32
  const [error, setError] = useState('');
@@ -38,72 +38,59 @@ export function ComicReader() {
38
  const [loadingChapter, setLoadingChapter] = useState(false);
39
  const [password, setPassword] = useState<string>('');
40
  const [originalSessionId, setOriginalSessionId] = useState<string>('');
41
-
42
  const imageRefs = useRef<{ [key: number]: HTMLImageElement | null }>({});
43
  const controlsTimeout = useRef<number | null>(null);
44
  const scrollContainerRef = useRef<HTMLDivElement | null>(null);
45
  const wsRef = useRef<WebSocket | null>(null);
46
  const chapterData = location.state?.chapterData;
47
  useEffect(() => {
48
- const statePassword = location.state?.password;
49
- const stateSessionId = location.state?.sessionId;
50
-
51
- console.log('[Reader] Initializing auth data:', {
52
- hasPassword: !!statePassword,
53
- hasSessionId: !!stateSessionId,
54
- password: statePassword,
55
- sessionId: stateSessionId
56
- });
57
-
58
- if (statePassword) {
59
- setPassword(statePassword);
60
- }
61
-
62
- if (stateSessionId) {
63
- setOriginalSessionId(stateSessionId);
64
- }
65
- }, [location.state]);
66
 
67
  useEffect(() => {
68
  if (!password || !originalSessionId) return;
69
 
70
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
71
  const wsUrl = `${protocol}//${window.location.host}/ws`;
72
- console.log('[Reader WS] Connecting to:', wsUrl);
73
-
74
  const ws = new WebSocket(wsUrl);
75
  let isConnected = false;
76
  wsRef.current = ws;
77
 
78
- ws.onopen = () => {
79
- console.log('[Reader WS] Connected and ready');
80
  isConnected = true;
81
  };
82
 
83
  ws.onmessage = (event) => {
84
  try {
85
  const message = JSON.parse(event.data);
86
-
87
  if (message.type === 'chapter_data_response') {
88
  setLoadingChapter(false);
89
-
90
  if (message.success && message.data) {
91
- console.log('[Reader WS] New chapter data received:', {
92
- title: message.data.title,
93
- hasPrev: !!message.data.prevChapter,
94
- hasNext: !!message.data.nextChapter,
95
- prevChapter: message.data.prevChapter,
96
- nextChapter: message.data.nextChapter,
97
- hasAllChapters: !!message.data.allChapters,
98
- allChaptersCount: message.data.allChapters?.length || 0
99
- });
100
-
101
- // Preserve allChapters if not in response
102
  const newData = {
103
  ...message.data,
104
  allChapters: message.data.allChapters || data?.allChapters || []
105
  };
106
-
107
  setData(newData);
108
  setCurrentPage(1);
109
  scrollContainerRef.current?.scrollTo({ top: 0 });
@@ -126,7 +113,6 @@ export function ComicReader() {
126
  };
127
 
128
  ws.onclose = () => {
129
- console.log('[Reader WS] Disconnected');
130
  wsRef.current = null;
131
  };
132
 
@@ -139,14 +125,6 @@ export function ComicReader() {
139
 
140
  useEffect(() => {
141
  if (chapterData) {
142
- console.log('[Reader] Chapter data received:', {
143
- title: chapterData.title,
144
- totalImages: chapterData.totalImages,
145
- hasPrev: !!chapterData.prevChapter,
146
- hasNext: !!chapterData.nextChapter,
147
- prevChapter: chapterData.prevChapter,
148
- nextChapter: chapterData.nextChapter
149
- });
150
  setData(chapterData);
151
  setLoading(false);
152
  return;
@@ -161,7 +139,7 @@ export function ComicReader() {
161
  setError('Please open chapter from comic page');
162
  setLoading(false);
163
  }, [sessionId, password, chapterData, originalSessionId]);
164
-
165
  useEffect(() => {
166
  const resetTimeout = () => {
167
  if (controlsTimeout.current) {
@@ -175,7 +153,7 @@ export function ComicReader() {
175
  };
176
 
177
  const handleMove = () => resetTimeout();
178
-
179
  window.addEventListener('mousemove', handleMove);
180
  window.addEventListener('touchstart', handleMove);
181
 
@@ -187,15 +165,15 @@ export function ComicReader() {
187
  }
188
  };
189
  }, []);
190
-
191
  useEffect(() => {
192
  const handleScroll = () => {
193
  if (!scrollContainerRef.current) return;
194
-
195
- const container = scrollContainerRef.current;
196
  const windowHeight = container.clientHeight;
197
  let currentIdx = 1;
198
-
199
  for (let i = 1; i <= (data?.totalImages || 0); i++) {
200
  const img = imageRefs.current[i];
201
  if (img) {
@@ -208,7 +186,7 @@ export function ComicReader() {
208
  }
209
  }
210
  }
211
-
212
  setCurrentPage(currentIdx);
213
  };
214
 
@@ -225,7 +203,7 @@ export function ComicReader() {
225
  const container = scrollContainerRef.current;
226
  const containerRect = container.getBoundingClientRect();
227
  const imgRect = img.getBoundingClientRect();
228
-
229
  const scrollTo = container.scrollTop + (imgRect.top - containerRect.top);
230
  container.scrollTo({ top: scrollTo, behavior: 'smooth' });
231
  }
@@ -251,24 +229,15 @@ export function ComicReader() {
251
  setJumpPage('');
252
  }
253
  };
254
-
255
- const handleChapterNavigation = (chapterSlug: string) => {
256
- console.log('[Reader] Attempting chapter navigation to:', chapterSlug);
257
- console.log('[Reader] WebSocket state:', wsRef.current?.readyState);
258
- console.log('[Reader] Has password:', !!password);
259
- console.log('[Reader] Has sessionId:', !!originalSessionId);
260
 
 
261
  if (!password || !originalSessionId) {
262
  setError('Missing authentication data');
263
  return;
264
  }
265
 
266
- // If WebSocket is not open, try to wait or show error
267
  if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
268
- // Check if it's connecting
269
  if (wsRef.current?.readyState === WebSocket.CONNECTING) {
270
- console.log('[Reader] WebSocket is connecting, waiting...');
271
- // Wait a bit and retry
272
  setTimeout(() => {
273
  if (wsRef.current?.readyState === WebSocket.OPEN) {
274
  handleChapterNavigation(chapterSlug);
@@ -278,7 +247,7 @@ export function ComicReader() {
278
  }, 1000);
279
  return;
280
  }
281
-
282
  setError('Connection lost. Please refresh the page.');
283
  console.error('[Reader] WebSocket not connected. State:', wsRef.current?.readyState);
284
  return;
@@ -295,7 +264,6 @@ export function ComicReader() {
295
  allChapters: data?.allChapters || []
296
  };
297
 
298
- console.log('[Reader] Sending chapter request:', requestData);
299
  wsRef.current.send(JSON.stringify(requestData));
300
  };
301
 
@@ -312,7 +280,7 @@ export function ComicReader() {
312
  return (
313
  <div className="reader-loading">
314
  <div className="spinner"></div>
315
- <p>Loading next chapter...</p>
316
  </div>
317
  );
318
  }
@@ -334,7 +302,7 @@ export function ComicReader() {
334
  <button className="back-btn" onClick={() => navigate(-1)}>
335
  ← Back
336
  </button>
337
-
338
  <div className="reader-title">
339
  <h1>{data.title}</h1>
340
  <span className="page-indicator">
@@ -342,8 +310,8 @@ export function ComicReader() {
342
  </span>
343
  </div>
344
 
345
- <button
346
- className="menu-btn"
347
  onClick={() => setShowMenu(!showMenu)}
348
  >
349
 
@@ -354,7 +322,7 @@ export function ComicReader() {
354
  <div className="hamburger-menu" onClick={() => setShowMenu(false)}>
355
  <div className="menu-content" onClick={(e) => e.stopPropagation()}>
356
  <button onClick={() => setShowMenu(false)} className="close-menu">✕</button>
357
-
358
  <div className="menu-section">
359
  <h3>Navigation</h3>
360
  <button onClick={() => { scrollToPage(1); setShowMenu(false); }}>
@@ -437,14 +405,14 @@ export function ComicReader() {
437
 
438
  <div className="reader-footer">
439
  <div className="page-controls">
440
- <button
441
  className="nav-control-btn"
442
  onClick={prevChapter}
443
  disabled={!data.prevChapter}
444
  >
445
  ← Prev
446
  </button>
447
-
448
  <div className="page-info">
449
  <span>{currentPage} / {data.totalImages}</span>
450
  <input
@@ -457,7 +425,7 @@ export function ComicReader() {
457
  />
458
  </div>
459
 
460
- <button
461
  className="nav-control-btn"
462
  onClick={nextChapter}
463
  disabled={!data.nextChapter}
@@ -468,4 +436,4 @@ export function ComicReader() {
468
  </div>
469
  </div>
470
  );
471
- }
 
26
  const { sessionId } = useParams();
27
  const navigate = useNavigate();
28
  const location = useLocation();
29
+
30
  const [data, setData] = useState<ReaderData | null>(null);
31
  const [loading, setLoading] = useState(true);
32
  const [error, setError] = useState('');
 
38
  const [loadingChapter, setLoadingChapter] = useState(false);
39
  const [password, setPassword] = useState<string>('');
40
  const [originalSessionId, setOriginalSessionId] = useState<string>('');
41
+
42
  const imageRefs = useRef<{ [key: number]: HTMLImageElement | null }>({});
43
  const controlsTimeout = useRef<number | null>(null);
44
  const scrollContainerRef = useRef<HTMLDivElement | null>(null);
45
  const wsRef = useRef<WebSocket | null>(null);
46
  const chapterData = location.state?.chapterData;
47
  useEffect(() => {
48
+ const statePassword = location.state?.password;
49
+ const stateSessionId = location.state?.sessionId;
50
+
51
+ console.log('[Reader] Initializing auth data:', {
52
+ hasPassword: !!statePassword,
53
+ hasSessionId: !!stateSessionId,
54
+ password: statePassword,
55
+ sessionId: stateSessionId
56
+ });
57
+
58
+ if (statePassword) {
59
+ setPassword(statePassword);
60
+ }
61
+
62
+ if (stateSessionId) {
63
+ setOriginalSessionId(stateSessionId);
64
+ }
65
+ }, [location.state]);
66
 
67
  useEffect(() => {
68
  if (!password || !originalSessionId) return;
69
 
70
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
71
  const wsUrl = `${protocol}//${window.location.host}/ws`;
72
+
 
73
  const ws = new WebSocket(wsUrl);
74
  let isConnected = false;
75
  wsRef.current = ws;
76
 
77
+ ws.onopen = () => {
 
78
  isConnected = true;
79
  };
80
 
81
  ws.onmessage = (event) => {
82
  try {
83
  const message = JSON.parse(event.data);
84
+
85
  if (message.type === 'chapter_data_response') {
86
  setLoadingChapter(false);
87
+
88
  if (message.success && message.data) {
 
 
 
 
 
 
 
 
 
 
 
89
  const newData = {
90
  ...message.data,
91
  allChapters: message.data.allChapters || data?.allChapters || []
92
  };
93
+
94
  setData(newData);
95
  setCurrentPage(1);
96
  scrollContainerRef.current?.scrollTo({ top: 0 });
 
113
  };
114
 
115
  ws.onclose = () => {
 
116
  wsRef.current = null;
117
  };
118
 
 
125
 
126
  useEffect(() => {
127
  if (chapterData) {
 
 
 
 
 
 
 
 
128
  setData(chapterData);
129
  setLoading(false);
130
  return;
 
139
  setError('Please open chapter from comic page');
140
  setLoading(false);
141
  }, [sessionId, password, chapterData, originalSessionId]);
142
+
143
  useEffect(() => {
144
  const resetTimeout = () => {
145
  if (controlsTimeout.current) {
 
153
  };
154
 
155
  const handleMove = () => resetTimeout();
156
+
157
  window.addEventListener('mousemove', handleMove);
158
  window.addEventListener('touchstart', handleMove);
159
 
 
165
  }
166
  };
167
  }, []);
168
+
169
  useEffect(() => {
170
  const handleScroll = () => {
171
  if (!scrollContainerRef.current) return;
172
+
173
+ const container = scrollContainerRef.current;
174
  const windowHeight = container.clientHeight;
175
  let currentIdx = 1;
176
+
177
  for (let i = 1; i <= (data?.totalImages || 0); i++) {
178
  const img = imageRefs.current[i];
179
  if (img) {
 
186
  }
187
  }
188
  }
189
+
190
  setCurrentPage(currentIdx);
191
  };
192
 
 
203
  const container = scrollContainerRef.current;
204
  const containerRect = container.getBoundingClientRect();
205
  const imgRect = img.getBoundingClientRect();
206
+
207
  const scrollTo = container.scrollTop + (imgRect.top - containerRect.top);
208
  container.scrollTo({ top: scrollTo, behavior: 'smooth' });
209
  }
 
229
  setJumpPage('');
230
  }
231
  };
 
 
 
 
 
 
232
 
233
+ const handleChapterNavigation = (chapterSlug: string) => {
234
  if (!password || !originalSessionId) {
235
  setError('Missing authentication data');
236
  return;
237
  }
238
 
 
239
  if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
 
240
  if (wsRef.current?.readyState === WebSocket.CONNECTING) {
 
 
241
  setTimeout(() => {
242
  if (wsRef.current?.readyState === WebSocket.OPEN) {
243
  handleChapterNavigation(chapterSlug);
 
247
  }, 1000);
248
  return;
249
  }
250
+
251
  setError('Connection lost. Please refresh the page.');
252
  console.error('[Reader] WebSocket not connected. State:', wsRef.current?.readyState);
253
  return;
 
264
  allChapters: data?.allChapters || []
265
  };
266
 
 
267
  wsRef.current.send(JSON.stringify(requestData));
268
  };
269
 
 
280
  return (
281
  <div className="reader-loading">
282
  <div className="spinner"></div>
283
+ <p>Loading chapter...</p>
284
  </div>
285
  );
286
  }
 
302
  <button className="back-btn" onClick={() => navigate(-1)}>
303
  ← Back
304
  </button>
305
+
306
  <div className="reader-title">
307
  <h1>{data.title}</h1>
308
  <span className="page-indicator">
 
310
  </span>
311
  </div>
312
 
313
+ <button
314
+ className="menu-btn"
315
  onClick={() => setShowMenu(!showMenu)}
316
  >
317
 
 
322
  <div className="hamburger-menu" onClick={() => setShowMenu(false)}>
323
  <div className="menu-content" onClick={(e) => e.stopPropagation()}>
324
  <button onClick={() => setShowMenu(false)} className="close-menu">✕</button>
325
+
326
  <div className="menu-section">
327
  <h3>Navigation</h3>
328
  <button onClick={() => { scrollToPage(1); setShowMenu(false); }}>
 
405
 
406
  <div className="reader-footer">
407
  <div className="page-controls">
408
+ <button
409
  className="nav-control-btn"
410
  onClick={prevChapter}
411
  disabled={!data.prevChapter}
412
  >
413
  ← Prev
414
  </button>
415
+
416
  <div className="page-info">
417
  <span>{currentPage} / {data.totalImages}</span>
418
  <input
 
425
  />
426
  </div>
427
 
428
+ <button
429
  className="nav-control-btn"
430
  onClick={nextChapter}
431
  disabled={!data.nextChapter}
 
436
  </div>
437
  </div>
438
  );
439
+ }