Fixed pagination problem: The numbering of aliases were incorrect

This commit is contained in:
Penguin-71630
2025-12-13 00:53:41 +08:00
parent a05adb982f
commit 31598a9c96
29 changed files with 229 additions and 247 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
.DS_Store .DS_Store
tests/*
################################ ################################
# Node.js/React (web/) 專屬 # Node.js/React (web/) 專屬
################################ ################################

126
webpage/bug-report.md Normal file
View 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)

View File

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

View File

@@ -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);

View File

@@ -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: {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

View File

@@ -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"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -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;

View File

@@ -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'}

View File

@@ -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>

View File

@@ -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>