OhMyDitzzy commited on
Commit
a2befa6
·
1 Parent(s): e34d75e
src/modules/comic/ComicLanding.css CHANGED
@@ -423,6 +423,45 @@
423
  opacity: 0.5;
424
  }
425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  @media (max-width: 768px) {
427
  .hero-content {
428
  grid-template-columns: 1fr;
 
423
  opacity: 0.5;
424
  }
425
 
426
+ .loading-overlay {
427
+ position: fixed;
428
+ top: 0;
429
+ left: 0;
430
+ right: 0;
431
+ bottom: 0;
432
+ background: rgba(0, 0, 0, 0.8);
433
+ z-index: 9999;
434
+ display: flex;
435
+ align-items: center;
436
+ justify-content: center;
437
+ }
438
+
439
+ .error-toast {
440
+ position: fixed;
441
+ top: 2rem;
442
+ left: 50%;
443
+ transform: translateX(-50%);
444
+ padding: 1rem 2rem;
445
+ background: rgba(239, 68, 68, 0.9);
446
+ border: 1px solid rgba(239, 68, 68, 1);
447
+ border-radius: 12px;
448
+ color: white;
449
+ font-weight: 500;
450
+ z-index: 9999;
451
+ animation: slideDown 0.3s ease-out;
452
+ }
453
+
454
+ @keyframes slideDown {
455
+ from {
456
+ transform: translateX(-50%) translateY(-100%);
457
+ opacity: 0;
458
+ }
459
+ to {
460
+ transform: translateX(-50%) translateY(0);
461
+ opacity: 1;
462
+ }
463
+ }
464
+
465
  @media (max-width: 768px) {
466
  .hero-content {
467
  grid-template-columns: 1fr;
src/modules/comic/ComicLanding.tsx CHANGED
@@ -33,6 +33,7 @@ export function ComicLanding() {
33
  const [error, setError] = useState('');
34
  const [searchQuery, setSearchQuery] = useState('');
35
  const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
 
36
 
37
  const [ws, setWs] = useState<WebSocket | null>(null);
38
 
@@ -41,7 +42,7 @@ export function ComicLanding() {
41
  if (storedSession) {
42
  const session = JSON.parse(storedSession);
43
  const now = Date.now();
44
-
45
  if (now < session.expiresAt) {
46
  setIsAuth(true);
47
  setPassword(session.password);
@@ -50,7 +51,7 @@ export function ComicLanding() {
50
  sessionStorage.removeItem(`comic_${sessionId}`);
51
  }
52
  }
53
-
54
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
55
  const wsUrl = `${protocol}//${window.location.host}/ws`;
56
  const websocket = new WebSocket(wsUrl);
@@ -79,12 +80,32 @@ export function ComicLanding() {
79
  if (message.success) {
80
  setComicData(message.data);
81
  setIsAuth(true);
 
82
  sessionStorage.setItem(`comic_${sessionId}`, JSON.stringify({
83
  password,
84
  expiresAt: message.expiresAt
85
  }));
 
 
86
  } else {
87
  setError(message.error || 'Gagal memuat data');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  }
89
  }
90
  };
@@ -129,14 +150,19 @@ export function ComicLanding() {
129
  };
130
 
131
  const handleReadChapter = (chapterSlug: string) => {
132
- const chapterSessionId = `chapter_${sessionId}_${chapterSlug}`;
133
- navigate(`/read/${chapterSessionId}`, {
134
- state: {
135
- chapterSlug,
136
- password,
137
- comicTitle: comicData?.title
138
- }
139
- });
 
 
 
 
 
140
  };
141
 
142
  const filteredChapters = comicData?.chapters.filter(ch =>
@@ -167,7 +193,7 @@ export function ComicLanding() {
167
  <div className="error-icon">⚠️</div>
168
  <h2>Oops!</h2>
169
  <p>{error}</p>
170
- <button onClick={() => navigate('/')}>Kembali ke awal</button>
171
  </div>
172
  </div>
173
  );
@@ -225,6 +251,21 @@ export function ComicLanding() {
225
 
226
  return (
227
  <div className="comic-landing">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  <div className="hero-section">
229
  <div className="hero-backdrop" style={{ backgroundImage: `url(${comicData.thumbnailUrl})` }}></div>
230
  <div className="hero-content">
 
33
  const [error, setError] = useState('');
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
 
 
42
  if (storedSession) {
43
  const session = JSON.parse(storedSession);
44
  const now = Date.now();
45
+
46
  if (now < session.expiresAt) {
47
  setIsAuth(true);
48
  setPassword(session.password);
 
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);
 
80
  if (message.success) {
81
  setComicData(message.data);
82
  setIsAuth(true);
83
+
84
  sessionStorage.setItem(`comic_${sessionId}`, JSON.stringify({
85
  password,
86
  expiresAt: message.expiresAt
87
  }));
88
+
89
+ setLoadingChapter(false);
90
  } else {
91
  setError(message.error || 'Gagal memuat data');
92
+ setLoadingChapter(false);
93
+ }
94
+ }
95
+
96
+ if (message.type === 'chapter_data_response') {
97
+ setLoadingChapter(false);
98
+ if (message.success) {
99
+ navigate(`/read/${sessionId}_chapter`, {
100
+ state: {
101
+ chapterData: message.data,
102
+ password,
103
+ sessionId
104
+ }
105
+ });
106
+ } else {
107
+ setError(message.error || 'Gagal memuat chapter');
108
+ setTimeout(() => setError(''), 3000);
109
  }
110
  }
111
  };
 
150
  };
151
 
152
  const handleReadChapter = (chapterSlug: string) => {
153
+ if (!ws) {
154
+ setError('WebSocket tidak terhubung');
155
+ return;
156
+ }
157
+
158
+ setLoadingChapter(true);
159
+
160
+ ws.send(JSON.stringify({
161
+ type: 'request_chapter_data',
162
+ sessionId,
163
+ password,
164
+ chapterSlug
165
+ }));
166
  };
167
 
168
  const filteredChapters = comicData?.chapters.filter(ch =>
 
193
  <div className="error-icon">⚠️</div>
194
  <h2>Oops!</h2>
195
  <p>{error}</p>
196
+ <button onClick={() => navigate('/')}>Kembali ke Home</button>
197
  </div>
198
  </div>
199
  );
 
251
 
252
  return (
253
  <div className="comic-landing">
254
+ {loadingChapter && (
255
+ <div className="loading-overlay">
256
+ <div className="loading-container">
257
+ <div className="spinner"></div>
258
+ <p>Loading chapter...</p>
259
+ </div>
260
+ </div>
261
+ )}
262
+
263
+ {error && isAuth && (
264
+ <div className="error-toast">
265
+ {error}
266
+ </div>
267
+ )}
268
+
269
  <div className="hero-section">
270
  <div className="hero-backdrop" style={{ backgroundImage: `url(${comicData.thumbnailUrl})` }}></div>
271
  <div className="hero-content">
src/modules/comic/ComicReader.tsx CHANGED
@@ -25,7 +25,8 @@ interface ReaderData {
25
  export function ComicReader() {
26
  const { sessionId } = useParams();
27
  const navigate = useNavigate();
28
- const location = useLocation();
 
29
  const [data, setData] = useState<ReaderData | null>(null);
30
  const [loading, setLoading] = useState(true);
31
  const [error, setError] = useState('');
@@ -34,16 +35,28 @@ export function ComicReader() {
34
  const [showControls, setShowControls] = useState(true);
35
  const [showPageJump, setShowPageJump] = useState(false);
36
  const [jumpPage, setJumpPage] = useState('');
37
- const [_, setWs] = useState<WebSocket | null>(null);
 
38
  const imageRefs = useRef<{ [key: number]: HTMLImageElement | null }>({});
39
  const controlsTimeout = useRef<number | null>(null);
40
 
 
 
41
  const password = location.state?.password ||
42
  JSON.parse(sessionStorage.getItem(`comic_${sessionId}`) || '{}').password;
 
43
 
44
  useEffect(() => {
45
- if (!password) {
46
- setError('Unauthorized access');
 
 
 
 
 
 
 
 
47
  setLoading(false);
48
  return;
49
  }
@@ -53,25 +66,11 @@ export function ComicReader() {
53
  const websocket = new WebSocket(wsUrl);
54
 
55
  websocket.onopen = () => {
56
- websocket.send(JSON.stringify({
57
- type: 'request_comic_data',
58
- sessionId,
59
- password
60
- }));
61
- };
62
-
63
- websocket.onmessage = (event) => {
64
- const message = JSON.parse(event.data);
65
-
66
- if (message.type === 'comic_data_response') {
67
- if (message.success) {
68
- setData(message.data);
69
- setLoading(false);
70
- } else {
71
- setError(message.error || 'Failed to load chapter');
72
- setLoading(false);
73
- }
74
- }
75
  };
76
 
77
  websocket.onerror = () => {
@@ -84,7 +83,7 @@ export function ComicReader() {
84
  return () => {
85
  websocket.close();
86
  };
87
- }, [sessionId, password]);
88
 
89
  useEffect(() => {
90
  const resetTimeout = () => {
@@ -111,6 +110,7 @@ export function ComicReader() {
111
  };
112
  }, []);
113
 
 
114
  useEffect(() => {
115
  const handleKeyPress = (e: KeyboardEvent) => {
116
  if (e.key === 'ArrowRight') nextPage();
@@ -124,6 +124,7 @@ export function ComicReader() {
124
  return () => window.removeEventListener('keydown', handleKeyPress);
125
  }, [currentPage, data]);
126
 
 
127
  useEffect(() => {
128
  if (viewMode === 'scroll' && imageRefs.current[currentPage]) {
129
  imageRefs.current[currentPage]?.scrollIntoView({
@@ -151,14 +152,12 @@ export function ComicReader() {
151
  };
152
 
153
  const handleChapterNavigation = (chapterSlug: string) => {
154
- const newSessionId = `chapter_${Date.now()}_${chapterSlug}`;
155
- navigate(`/read/${newSessionId}`, {
156
- state: {
157
- chapterSlug,
158
- password,
159
- comicTitle: data?.title
160
- }
161
- });
162
  };
163
 
164
  const handlePageJump = () => {
 
25
  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('');
 
35
  const [showControls, setShowControls] = useState(true);
36
  const [showPageJump, setShowPageJump] = useState(false);
37
  const [jumpPage, setJumpPage] = useState('');
38
+ const [ws, setWs] = useState<WebSocket | null>(null);
39
+
40
  const imageRefs = useRef<{ [key: number]: HTMLImageElement | null }>({});
41
  const controlsTimeout = useRef<number | null>(null);
42
 
43
+ // Get data from location state (passed from ComicLanding)
44
+ const chapterData = location.state?.chapterData;
45
  const password = location.state?.password ||
46
  JSON.parse(sessionStorage.getItem(`comic_${sessionId}`) || '{}').password;
47
+ const originalSessionId = location.state?.sessionId;
48
 
49
  useEffect(() => {
50
+ // If we have chapter data from state, use it directly
51
+ if (chapterData) {
52
+ setData(chapterData);
53
+ setLoading(false);
54
+ return;
55
+ }
56
+
57
+ // Otherwise, this might be a direct link or refresh
58
+ if (!password || !originalSessionId) {
59
+ setError('Unauthorized access - please open from comic page');
60
  setLoading(false);
61
  return;
62
  }
 
66
  const websocket = new WebSocket(wsUrl);
67
 
68
  websocket.onopen = () => {
69
+ console.log('[Reader WS] Connected');
70
+ // Since we don't have chapter slug, we can't request
71
+ // User should go back to comic landing page
72
+ setError('Please open chapter from comic page');
73
+ setLoading(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  };
75
 
76
  websocket.onerror = () => {
 
83
  return () => {
84
  websocket.close();
85
  };
86
+ }, [sessionId, password, chapterData, originalSessionId]);
87
 
88
  useEffect(() => {
89
  const resetTimeout = () => {
 
110
  };
111
  }, []);
112
 
113
+ // Keyboard shortcuts
114
  useEffect(() => {
115
  const handleKeyPress = (e: KeyboardEvent) => {
116
  if (e.key === 'ArrowRight') nextPage();
 
124
  return () => window.removeEventListener('keydown', handleKeyPress);
125
  }, [currentPage, data]);
126
 
127
+ // Scroll to current page in scroll mode
128
  useEffect(() => {
129
  if (viewMode === 'scroll' && imageRefs.current[currentPage]) {
130
  imageRefs.current[currentPage]?.scrollIntoView({
 
152
  };
153
 
154
  const handleChapterNavigation = (chapterSlug: string) => {
155
+ // Go back to comic landing and let it handle chapter loading
156
+ navigate(-1);
157
+
158
+ // Alternatively, we could navigate to the same comic landing
159
+ // and trigger chapter load from there
160
+ // But going back is simpler and maintains the flow
 
 
161
  };
162
 
163
  const handlePageJump = () => {
src/server/websocket.ts CHANGED
@@ -14,11 +14,13 @@ interface ComicSession {
14
  data: any;
15
  createdAt: number;
16
  expiresAt: number;
 
17
  }
18
 
19
  const sessions = new Map<string, Session>();
20
  const comicSessions = new Map<string, ComicSession>();
21
  const botConnections = new Set<ServerWebSocket>();
 
22
 
23
  export function handleWebSocketConnection(ws: ServerWebSocket) {
24
  // @ts-ignore
@@ -76,6 +78,10 @@ function handleMessage(ws: ServerWebSocket, message: any) {
76
  handleComicStream(message);
77
  break;
78
 
 
 
 
 
79
  case 'request_comic_data':
80
  handleComicDataRequest(ws, message);
81
  break;
@@ -84,6 +90,10 @@ function handleMessage(ws: ServerWebSocket, message: any) {
84
  handlePasswordValidation(ws, message);
85
  break;
86
 
 
 
 
 
87
  default:
88
  console.log('[WebSocket] Unknown message type:', type);
89
  }
@@ -138,6 +148,9 @@ function handleComicDataRequest(ws: ServerWebSocket, message: any) {
138
  return;
139
  }
140
 
 
 
 
141
  ws.send(JSON.stringify({
142
  type: 'comic_data_response',
143
  success: true,
@@ -177,6 +190,80 @@ function handlePasswordValidation(ws: ServerWebSocket, message: any) {
177
  }));
178
  }
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  function handleRegistrationSubmit(sessionId: string, data: any) {
181
  const session = sessions.get(sessionId);
182
  if (!session) {
@@ -244,11 +331,23 @@ function cleanup(ws: ServerWebSocket) {
244
  session.ws = undefined;
245
  }
246
  }
 
 
 
 
 
 
 
 
 
 
 
 
247
  }
248
 
249
  setInterval(() => {
250
  const now = Date.now();
251
-
252
  for (const [sessionId, session] of sessions.entries()) {
253
  if (now - session.createdAt > 600000) {
254
  sessions.delete(sessionId);
@@ -259,6 +358,7 @@ setInterval(() => {
259
  for (const [sessionId, comicSession] of comicSessions.entries()) {
260
  if (now > comicSession.expiresAt) {
261
  comicSessions.delete(sessionId);
 
262
  console.log('[WebSocket] Cleaned up expired comic session:', sessionId);
263
  }
264
  }
 
14
  data: any;
15
  createdAt: number;
16
  expiresAt: number;
17
+ clientWs?: ServerWebSocket;
18
  }
19
 
20
  const sessions = new Map<string, Session>();
21
  const comicSessions = new Map<string, ComicSession>();
22
  const botConnections = new Set<ServerWebSocket>();
23
+ const clientConnections = new Map<string, ServerWebSocket>();
24
 
25
  export function handleWebSocketConnection(ws: ServerWebSocket) {
26
  // @ts-ignore
 
78
  handleComicStream(message);
79
  break;
80
 
81
+ case 'chapter_fetched':
82
+ handleChapterFetched(message);
83
+ break;
84
+
85
  case 'request_comic_data':
86
  handleComicDataRequest(ws, message);
87
  break;
 
90
  handlePasswordValidation(ws, message);
91
  break;
92
 
93
+ case 'request_chapter_data':
94
+ handleChapterDataRequest(ws, message);
95
+ break;
96
+
97
  default:
98
  console.log('[WebSocket] Unknown message type:', type);
99
  }
 
148
  return;
149
  }
150
 
151
+ comicSession.clientWs = ws;
152
+ clientConnections.set(sessionId, ws);
153
+
154
  ws.send(JSON.stringify({
155
  type: 'comic_data_response',
156
  success: true,
 
190
  }));
191
  }
192
 
193
+ function handleChapterDataRequest(ws: ServerWebSocket, message: any) {
194
+ const { sessionId, password, chapterSlug } = message;
195
+
196
+ const comicSession = comicSessions.get(sessionId);
197
+
198
+ if (!comicSession) {
199
+ ws.send(JSON.stringify({
200
+ type: 'chapter_data_response',
201
+ success: false,
202
+ error: 'Session not found or expired'
203
+ }));
204
+ return;
205
+ }
206
+
207
+ if (comicSession.password !== password) {
208
+ ws.send(JSON.stringify({
209
+ type: 'chapter_data_response',
210
+ success: false,
211
+ error: 'Invalid password'
212
+ }));
213
+ return;
214
+ }
215
+
216
+ if (Date.now() > comicSession.expiresAt) {
217
+ comicSessions.delete(sessionId);
218
+ ws.send(JSON.stringify({
219
+ type: 'chapter_data_response',
220
+ success: false,
221
+ error: 'Session expired'
222
+ }));
223
+ return;
224
+ }
225
+
226
+ comicSession.clientWs = ws;
227
+ clientConnections.set(sessionId, ws);
228
+
229
+ botConnections.forEach(botWs => {
230
+ try {
231
+ botWs.send(JSON.stringify({
232
+ type: 'fetch_chapter_request',
233
+ sessionId,
234
+ chapterSlug,
235
+ sender: comicSession.sender
236
+ }));
237
+ } catch (error) {
238
+ console.error('[WebSocket] Error sending to bot:', error);
239
+ }
240
+ });
241
+
242
+ console.log(`[WebSocket] Chapter fetch requested: ${chapterSlug} for session: ${sessionId}`);
243
+ }
244
+
245
+ function handleChapterFetched(message: any) {
246
+ const { sessionId, chapterData, success, error } = message;
247
+ const clientWs = clientConnections.get(sessionId);
248
+
249
+ if (!clientWs) {
250
+ console.log(`[WebSocket] No client connection found for session: ${sessionId}`);
251
+ return;
252
+ }
253
+
254
+ try {
255
+ clientWs.send(JSON.stringify({
256
+ type: 'chapter_data_response',
257
+ success,
258
+ data: chapterData,
259
+ error: error || null
260
+ }));
261
+ console.log(`[WebSocket] Chapter data sent to client for session: ${sessionId}`);
262
+ } catch (err) {
263
+ console.error('[WebSocket] Error sending chapter data to client:', err);
264
+ }
265
+ }
266
+
267
  function handleRegistrationSubmit(sessionId: string, data: any) {
268
  const session = sessions.get(sessionId);
269
  if (!session) {
 
331
  session.ws = undefined;
332
  }
333
  }
334
+
335
+ for (const [sessionId, clientWs] of clientConnections.entries()) {
336
+ if (clientWs === ws) {
337
+ clientConnections.delete(sessionId);
338
+ }
339
+ }
340
+
341
+ for (const [_, comicSession] of comicSessions.entries()) {
342
+ if (comicSession.clientWs === ws) {
343
+ comicSession.clientWs = undefined;
344
+ }
345
+ }
346
  }
347
 
348
  setInterval(() => {
349
  const now = Date.now();
350
+
351
  for (const [sessionId, session] of sessions.entries()) {
352
  if (now - session.createdAt > 600000) {
353
  sessions.delete(sessionId);
 
358
  for (const [sessionId, comicSession] of comicSessions.entries()) {
359
  if (now > comicSession.expiresAt) {
360
  comicSessions.delete(sessionId);
361
+ clientConnections.delete(sessionId);
362
  console.log('[WebSocket] Cleaned up expired comic session:', sessionId);
363
  }
364
  }