Skip to content

Commit dac2c8a

Browse files
authored
Merge pull request #257 from PiCiU1221/librarian-lazy-loading-book-display
librarian-lazy-loading-book-display
2 parents 058494c + 291b67b commit dac2c8a

8 files changed

Lines changed: 140 additions & 36 deletions

web_app/package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web_app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"react": "^18.3.1",
1616
"react-dom": "^18.3.1",
1717
"react-router-dom": "^7.0.2",
18+
"react-toastify": "^11.0.5",
1819
"swagger-ui": "^5.18.2",
1920
"swagger-ui-react": "^5.18.2"
2021
},

web_app/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,12 @@ import LegalInfoPage from './Utils/LegalInfoPage.tsx';
3636
import SwaggerUI from 'swagger-ui-react';
3737
import 'swagger-ui-react/swagger-ui.css';
3838

39+
import {ToastContainer} from 'react-toastify';
40+
3941
const App: React.FC = () => {
4042
return (
43+
<>
44+
<ToastContainer />
4145
<Router>
4246
<Routes>
4347
<Route path="/" element={<LandingPage />} />
@@ -84,6 +88,7 @@ const App: React.FC = () => {
8488
/>
8589
</Routes>
8690
</Router>
91+
</>
8792
);
8893
};
8994

web_app/src/Librarian/LibrarianAddBook.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ const LibrarianAddBook: React.FC = () => {
9090
try {
9191
const token = localStorage.getItem('access_token');
9292
if (!token) {
93-
setError('Brak tokena autoryzacyjnego.');
9493
return;
9594
}
9695

@@ -122,8 +121,10 @@ const LibrarianAddBook: React.FC = () => {
122121
};
123122

124123
const handleAuthorInput = (e: React.ChangeEvent<HTMLInputElement>) => {
124+
const input = e.target.value;
125125
setAuthorInput(e.target.value);
126126
setShowDropdown(true);
127+
fetchAuthors(input);
127128
};
128129

129130
const handleRemoveAuthor = (authorToRemove: string) => {
@@ -220,7 +221,6 @@ const LibrarianAddBook: React.FC = () => {
220221
throw new Error(`Błąd ${response.status}: ${errorText}`);
221222
}
222223

223-
// alert('Książka dodana');
224224
navigate('/librarian-dashboard');
225225
} catch (error) {
226226
setError((error as Error).message || 'Wystąpił błąd podczas dodawania książki.');

web_app/src/Librarian/LibrarianHomePage.tsx

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ const LibrarianHomePage: React.FC = () => {
6868

6969
const navigate = useNavigate();
7070

71+
// Lazy Loading for the book list display
72+
const [page, setPage] = useState(0);
73+
const [hasMore, setHasMore] = useState(true);
74+
const [isLoading, setIsLoading] = useState(false);
75+
7176
useEffect(() => {
7277
fetchAssignedLibrary();
7378
fetchDropdownData();
@@ -113,6 +118,19 @@ const LibrarianHomePage: React.FC = () => {
113118
}
114119
}, [deleteBooksMessage]);
115120

121+
// Lazy Loading
122+
useEffect(() => {
123+
const handleScroll = () => {
124+
const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 1000;
125+
if (nearBottom && !isLoading && hasMore) {
126+
fetchBooks(isUserLibraryChecked, page);
127+
}
128+
};
129+
130+
window.addEventListener('scroll', handleScroll);
131+
return () => window.removeEventListener('scroll', handleScroll);
132+
}, [isLoading, hasMore, page, isUserLibraryChecked]);
133+
116134
const fetchDropdownData = async () => {
117135
const token = localStorage.getItem('access_token');
118136

@@ -201,14 +219,16 @@ const LibrarianHomePage: React.FC = () => {
201219
}
202220
};
203221

204-
const fetchBooks = async (filterByLibrary: boolean) => {
222+
const fetchBooks = async (filterByLibrary: boolean, pageToFetch: number = 0) => {
205223
const token = localStorage.getItem('access_token');
206-
if (!token) return;
224+
if (!token || isLoading || !hasMore) return;
225+
226+
setIsLoading(true);
207227

208228
const queryParams = new URLSearchParams();
209229

210-
if (filterByLibrary && assignedLibrary?.name) {
211-
queryParams.append("library", assignedLibrary.name);
230+
if (filterByLibrary && assignedLibrary?.id) {
231+
queryParams.append("libraryId", assignedLibrary.id.toString());
212232
}
213233

214234
if (bookSearchInput) queryParams.append("title", bookSearchInput);
@@ -220,8 +240,8 @@ const LibrarianHomePage: React.FC = () => {
220240
if (releaseYearFrom) queryParams.append("releaseYearFrom", releaseYearFrom.toString());
221241
if (releaseYearTo) queryParams.append("releaseYearTo", releaseYearTo.toString());
222242

223-
queryParams.append("page", "0");
224-
queryParams.append("size", "20");
243+
queryParams.append("page", pageToFetch.toString());
244+
queryParams.append("size", "3");
225245

226246
try {
227247
const response = await fetch(`${API_BASE_URL}/api/books/search?${queryParams.toString()}`, {
@@ -232,22 +252,29 @@ const LibrarianHomePage: React.FC = () => {
232252
},
233253
});
234254

235-
if (!response.ok) {
236-
throw new Error(`Error: ${response.statusText}`);
237-
}
255+
if (!response.ok) throw new Error(`Error: ${response.statusText}`);
238256

239257
const data = await response.json();
240-
setBookSearchResults(data.content);
258+
const newBooks = data.content;
259+
260+
setBookSearchResults(prev => [...prev, ...newBooks]);
261+
setPage(pageToFetch + 1);
262+
setHasMore(!data.last);
263+
241264
} catch (error) {
242265
console.error("Error: ", error);
266+
} finally {
267+
setIsLoading(false);
243268
}
244269
};
245270

246271
const handleSearch = (reset: boolean = false) => {
247272
if (reset) {
248273
setBookSearchResults([]);
274+
setPage(0);
275+
setHasMore(true);
249276
}
250-
fetchBooks(isUserLibraryChecked);
277+
fetchBooks(isUserLibraryChecked, 0);
251278
};
252279

253280
const handleRedirectToAddBook = () => {

web_app/src/Librarian/LibrarianOrders.tsx

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -146,18 +146,7 @@ const LibrarianOrders: React.FC = () => {
146146
if (response.ok) {
147147
setPendingMessage('Zamówienie zostało zatwierdzone.');
148148
setPendingMessageType('success');
149-
150-
setTimeout(() => {
151-
window.location.reload();
152-
}, 2000);
153-
154-
setOrderDetails(prev =>
155-
prev.map(order =>
156-
order.orderId === orderId
157-
? { ...order, status: 'IN_REALIZATION' }
158-
: order
159-
)
160-
);
149+
await fetchOrderDetails();
161150
} else {
162151
setPendingMessage('Nie udało się zatwierdzić zamówienia.');
163152
setPendingMessageType('error');
@@ -194,11 +183,7 @@ const LibrarianOrders: React.FC = () => {
194183
setSelectedOrderId(null);
195184
setRejectionReason('');
196185
setCustomReason('');
197-
fetchOrderDetails();
198-
199-
setTimeout(() => {
200-
window.location.reload();
201-
}, 2000);
186+
await fetchOrderDetails();
202187
} else {
203188
setPendingMessage('Błąd przy odrzucaniu zamówienia.');
204189
setPendingMessageType('error');
@@ -229,16 +214,13 @@ const LibrarianOrders: React.FC = () => {
229214
if (response.ok) {
230215
setRealizationMessage(`Zamówienie ${orderId} zostało przekazane pomyślnie!`);
231216
setRealizationMessageType("success");
232-
233-
setTimeout(() => {
234-
window.location.reload();
235-
}, 2000);
217+
await fetchOrderDetails();
236218
} else {
237219
setRealizationMessage('Błąd przy przekazywaniu zamówienia.');
238220
setRealizationMessageType('error');
239221
}
240222
} catch (err) {
241-
console.error('Error declining order:', err);
223+
console.error('Error handing over order:', err);
242224
}
243225
};
244226

web_app/src/Librarian/LibrarianReturns.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import React, {useEffect, useState} from 'react';
22
import {Link, useNavigate} from 'react-router-dom';
3+
import {useWebSocketNewOrderNotification} from './useWebSocketNewOrderNotification.tsx';
4+
import {toast} from 'react-toastify';
5+
import 'react-toastify/dist/ReactToastify.css';
36

47
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
58

69
const LibrarianReturns: React.FC = () => {
7-
// Success and error messages
810
const [message, setMessage] = useState<string | null>(null);
911
const [messageType, setMessageType] = useState<'success' | 'error' | null>(null);
1012

13+
useWebSocketNewOrderNotification('librarian/orders/pending', () => {
14+
toast.info("Otrzymano nowe zamówienie!", {
15+
position: "bottom-right",
16+
});
17+
console.log("New order received!");
18+
});
19+
1120
// Returns
1221
interface RentalReturnItem {
1322
id: number;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { useEffect, useRef } from "react";
2+
3+
export function useWebSocketNewOrderNotification(
4+
channel: string,
5+
onMessage: (msg: string) => void
6+
) {
7+
const wsRef = useRef<WebSocket | null>(null);
8+
const keepAliveRef = useRef<NodeJS.Timeout | null>(null);
9+
10+
useEffect(() => {
11+
const token = localStorage.getItem("access_token");
12+
if (!token) return;
13+
14+
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL?.replace(/^https?:\/\//, "") || "localhost";
15+
const wsUrl = `wss://${apiBaseUrl}/ws?token=${encodeURIComponent(token)}&channel=${encodeURIComponent(channel)}`;
16+
17+
console.log("Connecting to:", wsUrl);
18+
19+
const ws = new WebSocket(wsUrl);
20+
wsRef.current = ws;
21+
22+
ws.onopen = () => {
23+
console.log("WebSocket opened:", wsUrl);
24+
25+
// Keep-alive ping for the established WS connection
26+
keepAliveRef.current = setInterval(() => {
27+
if (ws.readyState === WebSocket.OPEN) {
28+
ws.send("ping");
29+
}
30+
}, 30000);
31+
};
32+
33+
ws.onmessage = (event) => {
34+
onMessage(event.data);
35+
};
36+
37+
ws.onerror = (err) => {
38+
console.error("WebSocket error:", err);
39+
};
40+
41+
ws.onclose = (event) => {
42+
console.warn("WebSocket closed", event);
43+
if (keepAliveRef.current) {
44+
clearInterval(keepAliveRef.current);
45+
}
46+
};
47+
48+
return () => {
49+
console.log("Cleaning up WebSocket connection");
50+
if (keepAliveRef.current) {
51+
clearInterval(keepAliveRef.current);
52+
}
53+
ws.close();
54+
wsRef.current = null;
55+
};
56+
}, [channel]);
57+
}

0 commit comments

Comments
 (0)