Updated the filtering policy at search bar

This commit is contained in:
Penguin-71630
2025-12-13 03:46:56 +08:00
parent ea7477f7ce
commit 4de4cc4a18
5 changed files with 226 additions and 34 deletions

View File

@@ -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 <App> 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".

View File

@@ -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".
<!-- The current implementation could be slightly improved by:
1. Not using toLowerCase() for text and query initially
2. Instead, scan the text and query character by character, and break the loop as soon as the queryIndex equals lowerQuery.length
-->
## 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

View File

@@ -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<string | null>(null);
const [shouldLoadData, setShouldLoadData] = useState(false);
const [images, setImages] = useState<Image[]>([]);
const [allAliases, setAllAliases] = useState<Alias[]>([]);
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 (
<ErrorModal
message={loginError}
onClose={() => {
setLoginError(null);
window.location.href = '/';
}}
/>
);
}
// Show login page if in login mode
if (isLoginMode) {
return <Login onLoginSuccess={handleLoginSuccess} />;
}
// 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 (
<div className="h-screen flex items-center justify-center">
<div className="text-gray-500">Loading...</div>
</div>
);
}
const startIndex = (currentPage - 1) * ALIASES_PER_PAGE;
const endIndex = startIndex + ALIASES_PER_PAGE;

View File

@@ -0,0 +1,42 @@
interface ErrorModalProps {
message: string;
onClose: () => void;
}
export default function ErrorModal({ message, onClose }: ErrorModalProps) {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
<div className="p-6">
<div className="flex items-center justify-center mb-4">
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
<svg
className="w-6 h-6 text-red-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</div>
</div>
<h3 className="text-lg font-semibold text-center text-gray-900 mb-2">
Error
</h3>
<p className="text-center text-gray-600 mb-6">{message}</p>
<button
onClick={onClose}
className="w-full bg-red-600 text-white py-2 px-4 rounded-lg hover:bg-red-700 transition-colors"
>
Close
</button>
</div>
</div>
</div>
);
}

View File

@@ -30,7 +30,10 @@ export default function ImageGrid({ images, aliases, onImageClick }: ImageGridPr
return acc;
}, {} as Record<string, Image[]>);
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 (
<div className="p-6 space-y-8">