Fixed pagination problem: The numbering of aliases were incorrect
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
tests/*
|
||||||
|
|
||||||
################################
|
################################
|
||||||
# Node.js/React (web/) 專屬
|
# Node.js/React (web/) 專屬
|
||||||
################################
|
################################
|
||||||
|
|||||||
126
webpage/bug-report.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Bug Report File
|
||||||
|
|
||||||
|
|
||||||
|
## 2025/12/13 00:33
|
||||||
|
|
||||||
|
Trying to delete an alias from a image, but got error "500 Internal Server Error".
|
||||||
|
|
||||||
|
Using uptrace and here's the log message:
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"groupId": "9814518078643611785",
|
||||||
|
"system": "log:error",
|
||||||
|
"name": "exception",
|
||||||
|
"displayName": "github.com/uptrace/bun/driver/pgdriver.Error: ERROR: syntax error at or near \")\" (SQLSTATE=42601)",
|
||||||
|
"time": "2025-12-12T16:31:55.153Z",
|
||||||
|
"attrs": {
|
||||||
|
"service_name": "go2025-backend",
|
||||||
|
"service_version": "v0.0.1",
|
||||||
|
"telemetry_sdk_language": "go",
|
||||||
|
"telemetry_sdk_name": "opentelemetry",
|
||||||
|
"exception_param_SQLSTATE": "42601",
|
||||||
|
"exception_type": "github.com/uptrace/bun/driver/pgdriver.Error",
|
||||||
|
"otel_library_name": "github.com/uptrace/bun",
|
||||||
|
"telemetry_sdk_version": "1.38.0",
|
||||||
|
"exception_param_log_severity": "ERROR",
|
||||||
|
"host_name": "7d8c05cf36bb",
|
||||||
|
"log_severity": "ERROR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2025/12/12 23:41
|
||||||
|
|
||||||
|
getting 401 when trying to access backend API `GET /api/aliases` right after start up of docker compose, but after 1 ~ 2 minutes, it's ok.
|
||||||
|
|
||||||
|
Log message:
|
||||||
|
```
|
||||||
|
backend-1 | {"level":"info","ts":1765553837.4421663,"caller":"middlewares/accessLog.go:13","msg":"POST /auth/gen-login-url"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553839.3232543,"caller":"middlewares/accessLog.go:13","msg":"OPTIONS /*any"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553839.323296,"caller":"middlewares/accessLog.go:13","msg":"OPTIONS /*any"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553839.3233085,"caller":"middlewares/accessLog.go:13","msg":"GET /api/aliases"}
|
||||||
|
backend-1 | {"level":"warn","ts":1765553839.3233905,"caller":"middlewares/errorHandler.go:78","msg":"user did not login","error":"http: named cookie not present"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553839.324581,"caller":"middlewares/accessLog.go:13","msg":"GET /api/aliases"}
|
||||||
|
backend-1 | {"level":"warn","ts":1765553839.3246245,"caller":"middlewares/errorHandler.go:78","msg":"user did not login","error":"http: named cookie not present"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553839.3252084,"caller":"middlewares/accessLog.go:13","msg":"POST /auth/login"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553839.325493,"caller":"middlewares/accessLog.go:13","msg":"POST /auth/login"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553839.3793862,"caller":"middlewares/accessLog.go:13","msg":"GET /api/aliases"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553839.3855033,"caller":"middlewares/accessLog.go:13","msg":"GET /api/aliases"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553872.7971,"caller":"middlewares/accessLog.go:13","msg":"POST /auth/login"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553872.842072,"caller":"middlewares/accessLog.go:13","msg":"GET /api/aliases"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553905.793059,"caller":"middlewares/accessLog.go:13","msg":"GET /api/aliases"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553905.7976384,"caller":"middlewares/accessLog.go:13","msg":"GET /api/aliases"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553905.7985706,"caller":"middlewares/accessLog.go:13","msg":"GET /api/aliases"}
|
||||||
|
backend-1 | {"level":"info","ts":1765553905.7997246,"caller":"middlewares/accessLog.go:13","msg":"GET /api/aliases"}
|
||||||
|
```
|
||||||
|
|
||||||
|
I suspect it's the race condition happening at App.tsx (conflicts between "login" and "fetch aliases"). Please investigate the possible issue.
|
||||||
|
|
||||||
|
<!-- check if all other useEffect() are conflicting with the initial login process (getting cookie from backend) -->
|
||||||
|
|
||||||
|
|
||||||
|
## 2025/12/12 23:14
|
||||||
|
|
||||||
|
|
||||||
|
When I tried to delete an alias from a image, I got err "500 Internal Server Error". Log message:
|
||||||
|
```
|
||||||
|
:8080/api/image/2/aliases:1
|
||||||
|
Failed to load resource: the server responded with a status of 500 (Internal Server Error)
|
||||||
|
api.ts:114
|
||||||
|
PUT http://localhost:8080/api/image/2/aliases 500 (Internal Server Error)
|
||||||
|
updateImageAliases @ api.ts:114
|
||||||
|
handleSaveImage @ App.tsx:101
|
||||||
|
handleSave @ ImageModal.tsx:48
|
||||||
|
```
|
||||||
|
|
||||||
|
When an image is deleted, the alias entries at side barand buttons "previous page" and "next page" at top & bottom of main region of webpage are not updated synchronously.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2025/12/12 22:54
|
||||||
|
|
||||||
|
The aliases handling mechanism seems to have some bugs:
|
||||||
|
I test with single image huh.png, added 16 testing aliases: ["huh", "what", "huh2", "huh3", 4, 5, 6, 7, 8, 9, 0, 1, 2, 3].
|
||||||
|
- The side bar's numbering at page 1 isn't correct, it should be `#1, #2, #3, #4, #5`, but it's `#1, #3, #5, #6, #7`. Page 2, 3 are correct.
|
||||||
|
- There are more than 5 alias rows at the main region of webpage, and the alias string at each row is wrong.
|
||||||
|
- Page 1: `4, huh2, huh3, Alias #8, Alias #9, Alias #10, Alias #11, Alias #12, Alias #13, Alias #14, Alias #15, Alias #16`
|
||||||
|
- Page 2: `5, 6, 7, 8, 9, Alias #5, Alias #6, Alias #7, Alias #13, Alias #14, Alias #15, Alias #16`
|
||||||
|
- Page 3: `0, 1, 2, 3, Alias #5, Alias #6, Alias #7, Alias #8, Alias #9, Alias #10, Alias #11, Alias #12`
|
||||||
|
It seems I didn't modify the pagination mechanism correctly (originally you implemented 5 aliases per page, but I want it to be 10 aliases per page).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2025/12/12 22:01
|
||||||
|
|
||||||
|
It seems there is a permission issue when backend tried to run `make swagger`.
|
||||||
|
|
||||||
|
```
|
||||||
|
make 280ms Fri Dec 12 22:00:30 2025
|
||||||
|
go run github.com/swaggo/swag/cmd/swag@v1.16.4 fmt
|
||||||
|
go run github.com/swaggo/swag/cmd/swag@v1.16.4 init -o docs -g cmds/serve.go -pdl 1
|
||||||
|
2025/12/12 22:00:34 Generate swagger docs....
|
||||||
|
2025/12/12 22:00:34 Generate general API Info, search dir:./
|
||||||
|
2025/12/12 22:00:34 pkg /Users/polarbear03617/Documents/交大/大三上/Go程式設計/backend/cmds cannot find all dependencies, <nil>
|
||||||
|
go: writing stat cache: open /Users/polarbear03617/go/pkg/mod/cache/download/github.com/go-resty/resty/v2/@v/v2.17.0.info640656495.tmp: permission denied
|
||||||
|
exit status 1
|
||||||
|
make: *** [swagger] Error 1
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 2025/12/11 12:42
|
||||||
|
|
||||||
|
I correctly access the backend API with the cookie provided by backend, but why do I still see this error:
|
||||||
|
Failed to load resource: the server responded with a status of 401 (Unauthorized) (Login and seeing webpage is ok, but can't fetch aliases)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,12 +1,41 @@
|
|||||||
# Webpage for Memebot
|
# Webpage for Memebot
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Modify the uploadmodal's and imagemodal's alias adding/removing mechanism
|
||||||
|
|
||||||
|
Modify the uploadmodal's and imagemodal's alias adding/removing mechanism to the following:
|
||||||
|
[ (press Enter or click ADD to add alias) ] [ADD]
|
||||||
|
|
||||||
|
After typing alias and click ADD
|
||||||
|
[ test_alias ] [REMOVE]
|
||||||
|
[ (press Enter or click ADD to add alias) ] [ADD]
|
||||||
|
|
||||||
|
The [ADD] and [REMOVE] are buttons, and should both have the same width. Also, the protection mechanism (zero alias = not allow to save modifications / upload images) should be retained.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Modify the webpage
|
||||||
|
|
||||||
|
Modify the webpage: We don't want one page shows only an alias.
|
||||||
|
We want a paging feature on aliases, so that each page shows a fixed number of aliases and their corresponding images (the fixed number can be stored in a variable in api.ts). And the "next page" and "previous page" buttons should be located at both the top (below search bar) and the bottom of the page.
|
||||||
|
The sidebar is also modified to show the current page number, the aliases on the current page, and the total number of pages.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Initial Setup
|
||||||
|
|
||||||
This webpage is used to manage the images and aliases used by our Discord memebot.
|
This webpage is used to manage the images and aliases used by our Discord memebot.
|
||||||
|
|
||||||
Relationship between images and aliases: many to many.
|
Relationship between images and aliases: many to many.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Layout
|
### Layout
|
||||||
|
|
||||||
React + TypeScript (with Tailwind CSS)
|
React + TypeScript (with Tailwind CSS)
|
||||||
|
|
||||||
@@ -33,7 +62,7 @@ The top is the Navbar:
|
|||||||
- The right side of the Navbar contains function buttons, currently only Upload Image and Login.
|
- The right side of the Navbar contains function buttons, currently only Upload Image and Login.
|
||||||
|
|
||||||
|
|
||||||
### Find
|
#### Find
|
||||||
|
|
||||||
Find (Viewing and Searching)
|
Find (Viewing and Searching)
|
||||||
- The left sidebar serves as a table of content for all aliases, similar to the navigation feature in HackMD or mdBook.
|
- The left sidebar serves as a table of content for all aliases, similar to the navigation feature in HackMD or mdBook.
|
||||||
@@ -66,7 +95,7 @@ Find (Viewing and Searching)
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Upload Image
|
#### Upload Image
|
||||||
|
|
||||||
When the user clicks this button, a floating window (modal) appears:
|
When the user clicks this button, a floating window (modal) appears:
|
||||||
|
|
||||||
@@ -89,12 +118,9 @@ When the user clicks this button, a floating window (modal) appears:
|
|||||||
======================================
|
======================================
|
||||||
```
|
```
|
||||||
|
|
||||||
## Login (Authentication)
|
#### Login (Authentication)
|
||||||
|
|
||||||
Authentication is used to verify the current Discord user operating the frontend. Users who are not logged in can only perform viewing-related operations; they cannot edit, upload, or delete.
|
Authentication is used to verify the current Discord user operating the frontend. Users who are not logged in can only perform viewing-related operations; they cannot edit, upload, or delete.
|
||||||
|
|
||||||
|
|
||||||
<!-- Modify the webpage: We don't want one page shows only an alias.
|
|
||||||
We want a paging feature on aliases, so that each page shows a fixed number of aliases and their corresponding images (the fixed number can be stored in a variable in api.ts). And the "next page" and "previous page" buttons should be located at both the top (below search bar) and the bottom of the page.
|
|
||||||
The sidebar is also modified to show the current page number, the aliases on the current page, and the total number of pages. -->
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { Image, Alias } from './types';
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isLoginMode, setIsLoginMode] = useState(false);
|
const [isLoginMode, setIsLoginMode] = useState(false);
|
||||||
|
const [isInitializing, setIsInitializing] = useState(true);
|
||||||
const [images, setImages] = useState<Image[]>([]);
|
const [images, setImages] = useState<Image[]>([]);
|
||||||
const [allAliases, setAllAliases] = useState<Alias[]>([]);
|
const [allAliases, setAllAliases] = useState<Alias[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -20,8 +21,6 @@ function App() {
|
|||||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
console.log("Hello");
|
|
||||||
|
|
||||||
// Check if URL has a login token
|
// Check if URL has a login token
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
@@ -36,22 +35,26 @@ function App() {
|
|||||||
window.history.replaceState({}, document.title, '/');
|
window.history.replaceState({}, document.title, '/');
|
||||||
// Exit login mode and load data
|
// Exit login mode and load data
|
||||||
setIsLoginMode(false);
|
setIsLoginMode(false);
|
||||||
|
setIsInitializing(false);
|
||||||
loadData();
|
loadData();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error("Login failed:", error);
|
console.error("Login failed:", error);
|
||||||
setIsLoginMode(true); // Show login page on error
|
setIsLoginMode(true);
|
||||||
|
setIsInitializing(false);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
setIsInitializing(false);
|
||||||
loadData();
|
loadData();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoginMode) {
|
// Don't load data during initial mount when login is in progress
|
||||||
|
if (!isLoginMode && !isInitializing) {
|
||||||
loadData();
|
loadData();
|
||||||
}
|
}
|
||||||
}, [searchQuery, isLoginMode]);
|
}, [searchQuery, isLoginMode, isInitializing]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset to page 1 when search query changes
|
// Reset to page 1 when search query changes
|
||||||
@@ -105,6 +108,8 @@ function App() {
|
|||||||
const handleDeleteImage = async (imageId: number) => {
|
const handleDeleteImage = async (imageId: number) => {
|
||||||
await api.deleteImage(imageId);
|
await api.deleteImage(imageId);
|
||||||
await loadData();
|
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
|
// Show login page if in login mode
|
||||||
@@ -121,6 +126,14 @@ function App() {
|
|||||||
|
|
||||||
// Pagination calculations
|
// Pagination calculations
|
||||||
const totalPages = Math.max(1, Math.ceil(filteredAliases.length / ALIASES_PER_PAGE));
|
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 startIndex = (currentPage - 1) * ALIASES_PER_PAGE;
|
||||||
const endIndex = startIndex + ALIASES_PER_PAGE;
|
const endIndex = startIndex + ALIASES_PER_PAGE;
|
||||||
const currentPageAliases = filteredAliases.slice(startIndex, endIndex);
|
const currentPageAliases = filteredAliases.slice(startIndex, endIndex);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Image, Alias } from './types';
|
|||||||
const API_BASE_URL = 'http://localhost:8080';
|
const API_BASE_URL = 'http://localhost:8080';
|
||||||
|
|
||||||
// Pagination configuration
|
// 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 {
|
class ApiService {
|
||||||
// Authentication
|
// Authentication
|
||||||
@@ -111,6 +111,8 @@ class ApiService {
|
|||||||
// PUT /api/image/{id}/aliases
|
// PUT /api/image/{id}/aliases
|
||||||
// Body: {aliases: string[]}
|
// Body: {aliases: string[]}
|
||||||
async updateImageAliases(id: number, aliasNames: string[]): Promise<void> {
|
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`, {
|
const response = await fetch(`${API_BASE_URL}/api/image/${id}/aliases`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
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) {
|
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 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) => {
|
const groupedImages = images.reduce((acc, image) => {
|
||||||
if (image.aliasesIds.length === 0) {
|
if (image.aliasesIds.length === 0) {
|
||||||
if (!acc['__no_alias__']) acc['__no_alias__'] = [];
|
if (!acc['__no_alias__']) acc['__no_alias__'] = [];
|
||||||
acc['__no_alias__'].push(image);
|
acc['__no_alias__'].push(image);
|
||||||
} else {
|
} else {
|
||||||
|
// Only process aliases that are on the current page
|
||||||
image.aliasesIds.forEach((aliasId) => {
|
image.aliasesIds.forEach((aliasId) => {
|
||||||
|
if (currentPageAliasIds.has(aliasId)) {
|
||||||
const aliasName = aliasMap.get(aliasId) || `Alias #${aliasId}`;
|
const aliasName = aliasMap.get(aliasId) || `Alias #${aliasId}`;
|
||||||
if (!acc[aliasName]) acc[aliasName] = [];
|
if (!acc[aliasName]) acc[aliasName] = [];
|
||||||
acc[aliasName].push(image);
|
acc[aliasName].push(image);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
|
|||||||
@@ -105,64 +105,36 @@ export default function ImageModal({
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{aliases.map((alias) => (
|
{aliases.map((alias) => (
|
||||||
<div key={alias} className="flex items-center gap-2">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={alias}
|
value={alias}
|
||||||
readOnly
|
readOnly
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg bg-gray-50"
|
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>
|
||||||
))}
|
))}
|
||||||
{(
|
|
||||||
<div className="flex items-center gap-2">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Add a new alias"
|
placeholder="press Enter or click ADD to add alias"
|
||||||
value={newAlias}
|
value={newAlias}
|
||||||
onChange={(e) => setNewAlias(e.target.value)}
|
onChange={(e) => setNewAlias(e.target.value)}
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleAddAlias()}
|
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"
|
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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,7 +160,7 @@ export default function ImageModal({
|
|||||||
{(
|
{(
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
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"
|
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'}
|
{isSaving ? 'Saving...' : 'Save'}
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ export default function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{aliases.map((alias) => (
|
{aliases.map((alias, index) => (
|
||||||
<li key={alias.id}>
|
<li key={alias.id}>
|
||||||
<div className="w-full text-left px-3 py-2 rounded-lg bg-blue-50 text-blue-700">
|
<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}
|
{alias.name}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -171,61 +171,35 @@ export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{aliases.map((alias) => (
|
{aliases.map((alias) => (
|
||||||
<div key={alias} className="flex items-center gap-2">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={alias}
|
value={alias}
|
||||||
readOnly
|
readOnly
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg bg-gray-50"
|
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>
|
||||||
))}
|
))}
|
||||||
<div className="flex items-center gap-2">
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Add a new alias"
|
placeholder="press Enter or click ADD to add alias"
|
||||||
value={newAlias}
|
value={newAlias}
|
||||||
onChange={(e) => setNewAlias(e.target.value)}
|
onChange={(e) => setNewAlias(e.target.value)}
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleAddAlias()}
|
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"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||