Fixed pagination problem: The numbering of aliases were incorrect
@@ -12,6 +12,7 @@ import type { Image, Alias } from './types';
|
||||
|
||||
function App() {
|
||||
const [isLoginMode, setIsLoginMode] = useState(false);
|
||||
const [isInitializing, setIsInitializing] = useState(true);
|
||||
const [images, setImages] = useState<Image[]>([]);
|
||||
const [allAliases, setAllAliases] = useState<Alias[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
@@ -20,8 +21,6 @@ function App() {
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
console.log("Hello");
|
||||
|
||||
// Check if URL has a login token
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -36,22 +35,26 @@ function App() {
|
||||
window.history.replaceState({}, document.title, '/');
|
||||
// Exit login mode and load data
|
||||
setIsLoginMode(false);
|
||||
setIsInitializing(false);
|
||||
loadData();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Login failed:", error);
|
||||
setIsLoginMode(true); // Show login page on error
|
||||
setIsLoginMode(true);
|
||||
setIsInitializing(false);
|
||||
});
|
||||
} else {
|
||||
setIsInitializing(false);
|
||||
loadData();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoginMode) {
|
||||
// Don't load data during initial mount when login is in progress
|
||||
if (!isLoginMode && !isInitializing) {
|
||||
loadData();
|
||||
}
|
||||
}, [searchQuery, isLoginMode]);
|
||||
}, [searchQuery, isLoginMode, isInitializing]);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset to page 1 when search query changes
|
||||
@@ -105,6 +108,8 @@ 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 login page if in login mode
|
||||
@@ -121,6 +126,14 @@ function App() {
|
||||
|
||||
// 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 startIndex = (currentPage - 1) * ALIASES_PER_PAGE;
|
||||
const endIndex = startIndex + ALIASES_PER_PAGE;
|
||||
const currentPageAliases = filteredAliases.slice(startIndex, endIndex);
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Image, Alias } from './types';
|
||||
const API_BASE_URL = 'http://localhost:8080';
|
||||
|
||||
// Pagination configuration
|
||||
export const ALIASES_PER_PAGE = 5; // Number of aliases to show per page
|
||||
export const ALIASES_PER_PAGE = 10; // Number of aliases to show per page
|
||||
|
||||
class ApiService {
|
||||
// Authentication
|
||||
@@ -111,6 +111,8 @@ class ApiService {
|
||||
// PUT /api/image/{id}/aliases
|
||||
// Body: {aliases: string[]}
|
||||
async updateImageAliases(id: number, aliasNames: string[]): Promise<void> {
|
||||
console.log("Update aliases of image ", id);
|
||||
console.log("Alias names: ", aliasNames);
|
||||
const response = await fetch(`${API_BASE_URL}/api/image/${id}/aliases`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
|
||||
|
Before Width: | Height: | Size: 3.6 MiB |
@@ -1,137 +0,0 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"id": "1",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-20T12:00:00Z",
|
||||
"aliases": ["daisuke"],
|
||||
"url": "/api/images/1/file"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-20T12:05:00Z",
|
||||
"aliases": ["huh"],
|
||||
"url": "/api/images/2/file"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-21T09:00:00Z",
|
||||
"aliases": ["killme"],
|
||||
"url": "/api/images/3/file"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-21T09:30:00Z",
|
||||
"aliases": ["nofriend"],
|
||||
"url": "/api/images/4/file"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-21T10:00:00Z",
|
||||
"aliases": ["orz"],
|
||||
"url": "/api/images/5/file"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-22T11:20:00Z",
|
||||
"aliases": ["ramen"],
|
||||
"url": "/api/images/6/file"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-22T14:00:00Z",
|
||||
"aliases": ["rrrr"],
|
||||
"url": "/api/images/7/file"
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-23T08:00:00Z",
|
||||
"aliases": ["sleep"],
|
||||
"url": "/api/images/8/file"
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-23T08:05:00Z",
|
||||
"aliases": ["sleep"],
|
||||
"url": "/api/images/9/file"
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-24T15:00:00Z",
|
||||
"aliases": ["你要出多少"],
|
||||
"url": "/api/images/10/file"
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-24T16:00:00Z",
|
||||
"aliases": ["好ㄘ"],
|
||||
"url": "/api/images/11/file"
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-25T10:00:00Z",
|
||||
"aliases": ["宅斃了"],
|
||||
"url": "/api/images/12/file"
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-25T10:10:00Z",
|
||||
"aliases": ["宅斃了"],
|
||||
"url": "/api/images/13/file"
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-25T10:20:00Z",
|
||||
"aliases": ["宅斃了"],
|
||||
"url": "/api/images/14/file"
|
||||
},
|
||||
{
|
||||
"id": "15",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-26T12:00:00Z",
|
||||
"aliases": ["幹波大的"],
|
||||
"url": "/api/images/15/file"
|
||||
},
|
||||
{
|
||||
"id": "16",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-26T13:00:00Z",
|
||||
"aliases": ["我什麼都沒有"],
|
||||
"url": "/api/images/16/file"
|
||||
},
|
||||
{
|
||||
"id": "17",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-27T09:00:00Z",
|
||||
"aliases": ["欸嘿"],
|
||||
"url": "/api/images/17/file"
|
||||
},
|
||||
{
|
||||
"id": "18",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-25T10:20:00Z",
|
||||
"aliases": ["宅斃了"],
|
||||
"url": "/api/images/18/file"
|
||||
},
|
||||
{
|
||||
"id": "19",
|
||||
"uploaded_user_id": "konchin.shih",
|
||||
"uploaded_at": "2023-10-25T10:20:00Z",
|
||||
"aliases": ["宅斃了"],
|
||||
"url": "/api/images/19/file"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 448 KiB |
|
Before Width: | Height: | Size: 450 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 3.6 MiB |
|
Before Width: | Height: | Size: 423 KiB |
|
Before Width: | Height: | Size: 758 KiB |
|
Before Width: | Height: | Size: 408 KiB |
|
Before Width: | Height: | Size: 758 KiB |
|
Before Width: | Height: | Size: 758 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
@@ -8,19 +8,23 @@ interface ImageGridProps {
|
||||
}
|
||||
|
||||
export default function ImageGrid({ images, aliases, onImageClick }: ImageGridProps) {
|
||||
// Create a map of alias ID to alias name
|
||||
// Create a map of alias ID to alias name for current page aliases only
|
||||
const aliasMap = new Map(aliases.map(a => [a.id, a.name]));
|
||||
const currentPageAliasIds = new Set(aliases.map(a => a.id));
|
||||
|
||||
// Group images by alias
|
||||
// Group images by alias, but only for aliases on the current page
|
||||
const groupedImages = images.reduce((acc, image) => {
|
||||
if (image.aliasesIds.length === 0) {
|
||||
if (!acc['__no_alias__']) acc['__no_alias__'] = [];
|
||||
acc['__no_alias__'].push(image);
|
||||
} else {
|
||||
// Only process aliases that are on the current page
|
||||
image.aliasesIds.forEach((aliasId) => {
|
||||
const aliasName = aliasMap.get(aliasId) || `Alias #${aliasId}`;
|
||||
if (!acc[aliasName]) acc[aliasName] = [];
|
||||
acc[aliasName].push(image);
|
||||
if (currentPageAliasIds.has(aliasId)) {
|
||||
const aliasName = aliasMap.get(aliasId) || `Alias #${aliasId}`;
|
||||
if (!acc[aliasName]) acc[aliasName] = [];
|
||||
acc[aliasName].push(image);
|
||||
}
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
|
||||
@@ -105,64 +105,36 @@ export default function ImageModal({
|
||||
<div className="space-y-2">
|
||||
{aliases.map((alias) => (
|
||||
<div key={alias} className="flex items-center gap-2">
|
||||
{(
|
||||
<button
|
||||
onClick={() => handleRemoveAlias(alias)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
type="text"
|
||||
value={alias}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg bg-gray-50"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleRemoveAlias(alias)}
|
||||
className="w-20 px-3 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors font-medium"
|
||||
>
|
||||
REMOVE
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{(
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleAddAlias}
|
||||
className="text-green-600 hover:text-green-700"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Add a new alias"
|
||||
value={newAlias}
|
||||
onChange={(e) => setNewAlias(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddAlias()}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="press Enter or click ADD to add alias"
|
||||
value={newAlias}
|
||||
onChange={(e) => setNewAlias(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddAlias()}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddAlias}
|
||||
className="w-20 px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium"
|
||||
>
|
||||
ADD
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,7 +160,7 @@ export default function ImageModal({
|
||||
{(
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
disabled={isSaving || aliases.length === 0}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50"
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save'}
|
||||
|
||||
@@ -24,10 +24,10 @@ export default function Sidebar({
|
||||
</div>
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{aliases.map((alias) => (
|
||||
{aliases.map((alias, index) => (
|
||||
<li key={alias.id}>
|
||||
<div className="w-full text-left px-3 py-2 rounded-lg bg-blue-50 text-blue-700">
|
||||
<span className="text-xs text-blue-500 mr-2">#{alias.id}</span>
|
||||
<span className="text-xs text-blue-500 mr-2">#{index + 1}</span>
|
||||
{alias.name}
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -171,61 +171,35 @@ export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
|
||||
<div className="space-y-2">
|
||||
{aliases.map((alias) => (
|
||||
<div key={alias} className="flex items-center gap-2">
|
||||
{/* Red "X" Button: Remove this alias from the list */}
|
||||
<button
|
||||
onClick={() => handleRemoveAlias(alias)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
value={alias}
|
||||
readOnly
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg bg-gray-50"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleRemoveAlias(alias)}
|
||||
className="w-20 px-3 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors font-medium"
|
||||
>
|
||||
REMOVE
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Green "+" Button: Add new alias to the list */}
|
||||
<button
|
||||
onClick={handleAddAlias}
|
||||
className="text-green-600 hover:text-green-700"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M12 4v16m8-8H4"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Add a new alias"
|
||||
placeholder="press Enter or click ADD to add alias"
|
||||
value={newAlias}
|
||||
onChange={(e) => setNewAlias(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddAlias()}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddAlias}
|
||||
className="w-20 px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium"
|
||||
>
|
||||
ADD
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||