diff --git a/webpage/bug-report.md b/webpage/bug-report.md index d45bef7..1d86ef7 100644 --- a/webpage/bug-report.md +++ b/webpage/bug-report.md @@ -1,6 +1,60 @@ # Bug Report File +## 2025/12/13 02:20 + +Another slight issue occured: After I change the only alias of an image from "1" to "huh", the aliases listed at side bar and main page are not in the same order. +- Side bar: "宅斃了", "huh" +- Main page: "huh", "宅斃了" + + +## 2025/12/13 02:18 + +Seems there are some rendering errors that makes the home page not loading correctly: +``` +Access token: GZ9aUgchr8gJAT5xwmj-s6eS +api.ts:11 Hello, someone is trying to access the web page. +api.ts:12 Token: GZ9aUgchr8gJAT5xwmj-s6eS +installHook.js:1 Access token: GZ9aUgchr8gJAT5xwmj-s6eS +installHook.js:1 Hello, someone is trying to access the web page. +installHook.js:1 Token: GZ9aUgchr8gJAT5xwmj-s6eS +api.ts:26 Login successful +App.tsx:36 Login successful +api.ts:26 Login successful +App.tsx:36 Login successful +installHook.js:1 React has detected a change in the order of Hooks called by App. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: https://react.dev/link/rules-of-hooks + + Previous render Next render + ------------------------------------------------------ +1. useState useState +2. useState useState +3. useState useState +4. useState useState +5. useState useState +6. useState useState +7. useState useState +8. useState useState +9. useState useState +10. useState useState +11. useState useState +12. useEffect useEffect +13. useEffect useEffect +14. useCallback useCallback +15. useEffect useEffect +16. undefined useEffect + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +react-dom_client.js?v=80307a6a:5790 Uncaught Error: Rendered more hooks than during the previous render. + at App (App.tsx:159:3) +installHook.js:1 An error occurred in the component. + +Consider adding an error boundary to your tree to customize error handling behavior. +Visit https://react.dev/link/error-boundaries to learn more about error boundaries. +overrideMethod @ installHook.js:1 +``` + + + + ## 2025/12/13 00:33 Trying to delete an alias from a image, but got error "500 Internal Server Error". diff --git a/webpage/spec.md b/webpage/spec.md index 20a3f82..b102fa0 100644 --- a/webpage/spec.md +++ b/webpage/spec.md @@ -1,6 +1,45 @@ # Webpage for Memebot +## Update the filtering policy of the search bar + +The current filtering policy is: +```ts +// Filter aliases based on search +const filteredAliases = searchQuery +? allAliases.filter(alias => + alias.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) +: allAliases; +``` + +Please update the policy from "filter by substring" to "filter by subarray". + +Suppose "cd" is the current search query, the following examples are matched results: +- "abcde" (matched) +- "cwwwwd" (not a match originally) + +The following examples are not matched: +- "adce" (not a match originally) +- "dwwwwc" (not a match originally) + +The implementation logic may should be O(sum of length of all aliases + length of search query) and should break the loop of checking a single alias as soon as that alias is marked as "matched". + + + + + + +## Don't redirect 401 to home page + +Modify the login mechanism such that: +- When invalid token is provided upon entering the webpage by web browser, show a popup message "login error" and do not redirect to home page (even not load elements of the webpage). +- Do not use alert() to show "login error", instead, use a React component to show "login error". + + ## Modify the uploadmodal's and imagemodal's alias adding/removing mechanism diff --git a/webpage/src/App.tsx b/webpage/src/App.tsx index bb2aa75..d6d1e35 100644 --- a/webpage/src/App.tsx +++ b/webpage/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import Navbar from './components/Navbar'; import Sidebar from './components/Sidebar'; import SearchBar from './components/SearchBar'; @@ -6,6 +6,7 @@ import ImageGrid from './components/ImageGrid'; import ImageModal from './components/ImageModal'; import UploadModal from './components/UploadModal'; import Pagination from './components/Pagination'; +import ErrorModal from './components/ErrorModal'; import Login from './pages/Login'; import { api, ALIASES_PER_PAGE } from './api'; import type { Image, Alias } from './types'; @@ -13,6 +14,8 @@ import type { Image, Alias } from './types'; function App() { const [isLoginMode, setIsLoginMode] = useState(false); const [isInitializing, setIsInitializing] = useState(true); + const [loginError, setLoginError] = useState(null); + const [shouldLoadData, setShouldLoadData] = useState(false); const [images, setImages] = useState([]); const [allAliases, setAllAliases] = useState([]); const [searchQuery, setSearchQuery] = useState(''); @@ -33,43 +36,51 @@ function App() { console.log("Login successful"); // Remove token from URL for security window.history.replaceState({}, document.title, '/'); - // Exit login mode and load data + // Exit login mode and allow data loading setIsLoginMode(false); setIsInitializing(false); - loadData(); + setShouldLoadData(true); }) .catch((error) => { console.error("Login failed:", error); - setIsLoginMode(true); + // Show error modal instead of redirecting + setLoginError("Login error"); setIsInitializing(false); }); } else { setIsInitializing(false); - loadData(); + setShouldLoadData(true); } }, []); - useEffect(() => { - // Don't load data during initial mount when login is in progress - if (!isLoginMode && !isInitializing) { - loadData(); - } - }, [searchQuery, isLoginMode, isInitializing]); - useEffect(() => { // Reset to page 1 when search query changes setCurrentPage(1); }, [searchQuery]); - const loadData = async () => { + const loadData = useCallback(async () => { + if (loginError) return; // Don't load if there's a login error + setLoading(true); try { const aliasesData = await api.getAllAliases(); + aliasesData.sort((a, b) => a.name.localeCompare(b.name)); setAllAliases(aliasesData); - // Get images for all aliases or filtered aliases + // Get images for all aliases or filtered aliases using subarray matching + const matchesSubarray = (text: string, query: string): boolean => { + let queryIndex = 0; + for (let i = 0; i < text.length; i++) { + if (queryIndex === query.length) break; + if (text[i].toLowerCase() === query[queryIndex].toLowerCase()) { + queryIndex++; + } + } + return queryIndex === query.length; + }; + const aliasIds = searchQuery - ? aliasesData.filter(a => a.name.toLowerCase().includes(searchQuery.toLowerCase())).map(a => a.id) + ? aliasesData.filter(a => matchesSubarray(a.name, searchQuery)).map(a => a.id) : aliasesData.map(a => a.id); const imagesData = aliasIds.length > 0 @@ -82,8 +93,48 @@ function App() { } finally { setLoading(false); } + }, [searchQuery, loginError]); + + useEffect(() => { + // Load data when shouldLoadData is true and not in login mode or initializing + if (shouldLoadData && !isLoginMode && !isInitializing && !loginError) { + loadData(); + } + }, [shouldLoadData, isLoginMode, isInitializing, loginError, loadData]); + + // Filter aliases based on search - using subarray matching + const matchesSubarray = (text: string, query: string): boolean => { + let queryIndex = 0; + + for (let i = 0; i < text.length; i++) { + if (queryIndex === query.length) break; // Early exit when all query chars matched + if (text[i].toLowerCase() === query[queryIndex].toLowerCase()) { + queryIndex++; + } + } + + console.log(query, text, queryIndex); + if (queryIndex === query.length) { + console.log("Matched with " + query + ": " + text); + } + + return queryIndex === query.length; }; + const filteredAliases = searchQuery + ? allAliases.filter(alias => matchesSubarray(alias.name, searchQuery)) + : allAliases; + + // Pagination calculations + const totalPages = Math.max(1, Math.ceil(filteredAliases.length / ALIASES_PER_PAGE)); + + // Adjust current page if it's now out of bounds + useEffect(() => { + if (currentPage > totalPages) { + setCurrentPage(Math.max(1, totalPages)); + } + }, [totalPages, currentPage]); + const handleLoginSuccess = () => { setIsLoginMode(false); loadData(); @@ -108,31 +159,34 @@ function App() { const handleDeleteImage = async (imageId: number) => { await api.deleteImage(imageId); await loadData(); - // After deletion, check if current page is still valid - // This will be handled by the useEffect below }; + // Show error modal if login failed - don't load page elements + if (loginError) { + return ( + { + setLoginError(null); + window.location.href = '/'; + }} + /> + ); + } + // Show login page if in login mode if (isLoginMode) { return ; } - // Filter aliases based on search - const filteredAliases = searchQuery - ? allAliases.filter(alias => - alias.name.toLowerCase().includes(searchQuery.toLowerCase()) - ) - : allAliases; - - // Pagination calculations - const totalPages = Math.max(1, Math.ceil(filteredAliases.length / ALIASES_PER_PAGE)); - - // Adjust current page if it's now out of bounds - useEffect(() => { - if (currentPage > totalPages) { - setCurrentPage(Math.max(1, totalPages)); - } - }, [totalPages, currentPage]); + // Don't render page while initializing + if (isInitializing) { + return ( +
+
Loading...
+
+ ); + } const startIndex = (currentPage - 1) * ALIASES_PER_PAGE; const endIndex = startIndex + ALIASES_PER_PAGE; diff --git a/webpage/src/components/ErrorModal.tsx b/webpage/src/components/ErrorModal.tsx new file mode 100644 index 0000000..a1e8e2c --- /dev/null +++ b/webpage/src/components/ErrorModal.tsx @@ -0,0 +1,42 @@ +interface ErrorModalProps { + message: string; + onClose: () => void; +} + +export default function ErrorModal({ message, onClose }: ErrorModalProps) { + return ( +
+
+
+
+
+ + + +
+
+

+ Error +

+

{message}

+ +
+
+
+ ); +} diff --git a/webpage/src/components/ImageGrid.tsx b/webpage/src/components/ImageGrid.tsx index 8a80a8d..9561972 100644 --- a/webpage/src/components/ImageGrid.tsx +++ b/webpage/src/components/ImageGrid.tsx @@ -30,7 +30,10 @@ export default function ImageGrid({ images, aliases, onImageClick }: ImageGridPr return acc; }, {} as Record); - const filteredGroups = Object.entries(groupedImages); + // Maintain the same order as the aliases prop + const filteredGroups = aliases + .map(alias => [alias.name, groupedImages[alias.name]] as [string, Image[]]) + .filter(([_, imgs]) => imgs && imgs.length > 0); return (