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,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">