finished uploading image
This commit is contained in:
309
api/doc.json
Normal file
309
api/doc.json
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
{
|
||||||
|
"schemes": [],
|
||||||
|
"swagger": "2.0",
|
||||||
|
"info": {
|
||||||
|
"description": "",
|
||||||
|
"title": "Golang 2025 Final Project",
|
||||||
|
"termsOfService": "http://swagger.io/terms",
|
||||||
|
"contact": {},
|
||||||
|
"license": {
|
||||||
|
"name": "0BSD"
|
||||||
|
},
|
||||||
|
"version": "0.0.1"
|
||||||
|
},
|
||||||
|
"host": "",
|
||||||
|
"basePath": "/",
|
||||||
|
"paths": {
|
||||||
|
"/api/alias/{id}": {
|
||||||
|
"delete": {
|
||||||
|
"description": "delete alias along with the links",
|
||||||
|
"summary": "Delete alias",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Alias Id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/aliases": {
|
||||||
|
"get": {
|
||||||
|
"description": "get alias ids and names",
|
||||||
|
"summary": "Get aliases",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/api.getAliasesOutputAlias"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/image": {
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"image/png",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/gif"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "userinfo from /auth/gen-login-url",
|
||||||
|
"name": "userinfo",
|
||||||
|
"in": "header",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/image/{id}": {
|
||||||
|
"delete": {
|
||||||
|
"description": "delete image along with the links",
|
||||||
|
"summary": "Delete image",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Image Id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/image/{id}/aliases": {
|
||||||
|
"put": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Image Id",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Payload",
|
||||||
|
"name": "payload",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/api.putImageAliasesInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Not Found"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/images": {
|
||||||
|
"get": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"collectionFormat": "csv",
|
||||||
|
"description": "Image Ids",
|
||||||
|
"name": "images",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"collectionFormat": "csv",
|
||||||
|
"description": "Alias Ids",
|
||||||
|
"name": "aliases",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/api.getImagesOutputImage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/gen-login-url": {
|
||||||
|
"post": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Payload",
|
||||||
|
"name": "payload",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.postGenLoginUrlInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Payload",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.postGenLoginUrlOutput"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/auth/login": {
|
||||||
|
"post": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "payload",
|
||||||
|
"name": "payload",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/auth.postLoginInput"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"api.getAliasesOutputAlias": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api.getImagesOutputImage": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"aliasesIds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"uploadedAt": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"uploadedUserId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"api.putImageAliasesInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"aliases": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth.postGenLoginUrlInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"userId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth.postGenLoginUrlOutput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"loginUrl": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"auth.postLoginInput": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"token": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "final-project",
|
"name": "frontend-web",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "2025 Golang Final Project",
|
"description": "2025 Golang Final Project",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|||||||
5
tests/cookies.txt
Normal file
5
tests/cookies.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Netscape HTTP Cookie File
|
||||||
|
# https://curl.se/docs/http-cookies.html
|
||||||
|
# This file was generated by libcurl! Edit at your own risk.
|
||||||
|
|
||||||
|
localhost FALSE / FALSE 1765468235 refresh_token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjU0NjgyMzUsImlhdCI6MTc2NTQ2NDYzNSwidXNlcl9pZCI6IjYyNzQ1OTI4MDA0MzM3NjY4OSJ9.lOYJVE1GSOmDozP55xLQwa2bFEpzJUHt4vzJKGdzNec
|
||||||
22
tests/ping-get-aliases.bash
Executable file
22
tests/ping-get-aliases.bash
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check if TOKEN is provided
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "Error: TOKEN environment variable is required"
|
||||||
|
echo "Usage: TOKEN=your_token_here ./ping-get-aliases.bash"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -X POST http://localhost:8080/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"token\":\"$TOKEN\"}" \
|
||||||
|
-c cookies.txt \
|
||||||
|
-v > test.log 2>&1
|
||||||
|
|
||||||
|
# Parse refresh_token from cookies.txt
|
||||||
|
REFRESH_TOKEN=$(grep refresh_token cookies.txt | awk '{print $7}')
|
||||||
|
|
||||||
|
curl -b "refresh_token=$REFRESH_TOKEN" http://localhost:8080/api/aliases -v >> test.log 2>&1
|
||||||
|
|
||||||
|
# Display the result
|
||||||
|
echo "Test completed. Check test.log for details."
|
||||||
26
tests/ping-post-image.bash
Executable file
26
tests/ping-post-image.bash
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check if TOKEN is provided
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
echo "Error: TOKEN environment variable is required"
|
||||||
|
echo "Usage: TOKEN=your_token_here ./ping-get-aliases.bash"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -X POST http://localhost:8080/auth/login \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"token\":\"$TOKEN\"}" \
|
||||||
|
-c cookies.txt \
|
||||||
|
-v > test.log 2>&1
|
||||||
|
|
||||||
|
# Parse refresh_token from cookies.txt
|
||||||
|
REFRESH_TOKEN=$(grep refresh_token cookies.txt | awk '{print $7}')
|
||||||
|
|
||||||
|
curl \
|
||||||
|
-X POST http://localhost:8080/api/image \
|
||||||
|
-b "refresh_token=$REFRESH_TOKEN" \
|
||||||
|
-v \
|
||||||
|
>> test.log 2>&1
|
||||||
|
|
||||||
|
# Display the result
|
||||||
|
echo "Test completed. Check test.log for details."
|
||||||
54
tests/test.log
Normal file
54
tests/test.log
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
Note: Unnecessary use of -X or --request, POST is already inferred.
|
||||||
|
* Host localhost:8080 was resolved.
|
||||||
|
* IPv6: ::1
|
||||||
|
* IPv4: 127.0.0.1
|
||||||
|
% Total % Received % Xferd Average Speed Time Time Time Current
|
||||||
|
Dload Upload Total Spent Left Speed
|
||||||
|
|
||||||
|
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying [::1]:8080...
|
||||||
|
* Connected to localhost (::1) port 8080
|
||||||
|
> POST /auth/login HTTP/1.1
|
||||||
|
> Host: localhost:8080
|
||||||
|
> User-Agent: curl/8.7.1
|
||||||
|
> Accept: */*
|
||||||
|
> Content-Type: application/json
|
||||||
|
> Content-Length: 36
|
||||||
|
>
|
||||||
|
} [36 bytes data]
|
||||||
|
* upload completely sent off: 36 bytes
|
||||||
|
< HTTP/1.1 200 OK
|
||||||
|
< Access-Control-Allow-Credentials: true
|
||||||
|
< Access-Control-Allow-Origin: http://localhost:48763
|
||||||
|
* Added cookie refresh_token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjU0NjgyMzUsImlhdCI6MTc2NTQ2NDYzNSwidXNlcl9pZCI6IjYyNzQ1OTI4MDA0MzM3NjY4OSJ9.lOYJVE1GSOmDozP55xLQwa2bFEpzJUHt4vzJKGdzNec" for domain localhost, path /, expire 1765468235
|
||||||
|
< Set-Cookie: refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjU0NjgyMzUsImlhdCI6MTc2NTQ2NDYzNSwidXNlcl9pZCI6IjYyNzQ1OTI4MDA0MzM3NjY4OSJ9.lOYJVE1GSOmDozP55xLQwa2bFEpzJUHt4vzJKGdzNec; Path=/; Expires=Thu, 11 Dec 2025 15:50:35 GMT
|
||||||
|
< Date: Thu, 11 Dec 2025 14:50:35 GMT
|
||||||
|
< Content-Length: 35
|
||||||
|
< Content-Type: text/plain; charset=utf-8
|
||||||
|
<
|
||||||
|
{ [35 bytes data]
|
||||||
|
|
||||||
|
100 71 100 35 100 36 1724 1773 --:--:-- --:--:-- --:--:-- 3550
|
||||||
|
* Connection #0 to host localhost left intact
|
||||||
|
{"code":200, "message": "success"}
|
||||||
|
* Host localhost:8080 was resolved.
|
||||||
|
* IPv6: ::1
|
||||||
|
* IPv4: 127.0.0.1
|
||||||
|
% Total % Received % Xferd Average Speed Time Time Time Current
|
||||||
|
Dload Upload Total Spent Left Speed
|
||||||
|
|
||||||
|
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying [::1]:8080...
|
||||||
|
* Connected to localhost (::1) port 8080
|
||||||
|
> POST /api/image HTTP/1.1
|
||||||
|
> Host: localhost:8080
|
||||||
|
> User-Agent: curl/8.7.1
|
||||||
|
> Accept: */*
|
||||||
|
> Cookie: refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NjU0NjgyMzUsImlhdCI6MTc2NTQ2NDYzNSwidXNlcl9pZCI6IjYyNzQ1OTI4MDA0MzM3NjY4OSJ9.lOYJVE1GSOmDozP55xLQwa2bFEpzJUHt4vzJKGdzNec
|
||||||
|
>
|
||||||
|
* Request completely sent off
|
||||||
|
< HTTP/1.1 404 Not Found
|
||||||
|
< Content-Type: text/plain; charset=utf-8
|
||||||
|
< X-Content-Type-Options: nosniff
|
||||||
|
< Date: Thu, 11 Dec 2025 14:50:35 GMT
|
||||||
|
< Content-Length: 19
|
||||||
|
<
|
||||||
|
{ [19 bytes data]
|
||||||
@@ -6,21 +6,52 @@ import ImageGrid from './components/ImageGrid';
|
|||||||
import ImageModal from './components/ImageModal';
|
import ImageModal from './components/ImageModal';
|
||||||
import UploadModal from './components/UploadModal';
|
import UploadModal from './components/UploadModal';
|
||||||
import Pagination from './components/Pagination';
|
import Pagination from './components/Pagination';
|
||||||
import { api, ALIASES_PER_PAGE } from './api-mock';
|
import Login from './pages/Login';
|
||||||
import type { Image } from './types';
|
import { api, ALIASES_PER_PAGE } from './api';
|
||||||
|
import type { Image, Alias } from './types';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [isLoginMode, setIsLoginMode] = useState(false);
|
||||||
const [images, setImages] = useState<Image[]>([]);
|
const [images, setImages] = useState<Image[]>([]);
|
||||||
const [allAliases, setAllAliases] = useState<string[]>([]);
|
const [allAliases, setAllAliases] = useState<Alias[]>([]);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [selectedImage, setSelectedImage] = useState<Image | null>(null);
|
const [selectedImage, setSelectedImage] = useState<Image | null>(null);
|
||||||
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
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const token = urlParams.get('token');
|
||||||
|
if (token) {
|
||||||
|
console.log("Access token: ", token);
|
||||||
|
// Call the login API to set the refresh_token cookie
|
||||||
|
api.login(token)
|
||||||
|
.then(() => {
|
||||||
|
console.log("Login successful");
|
||||||
|
// Remove token from URL for security
|
||||||
|
window.history.replaceState({}, document.title, '/');
|
||||||
|
// Exit login mode and load data
|
||||||
|
setIsLoginMode(false);
|
||||||
loadData();
|
loadData();
|
||||||
}, [searchQuery]);
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Login failed:", error);
|
||||||
|
setIsLoginMode(true); // Show login page on error
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoginMode) {
|
||||||
|
loadData();
|
||||||
|
}
|
||||||
|
}, [searchQuery, isLoginMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset to page 1 when search query changes
|
// Reset to page 1 when search query changes
|
||||||
@@ -30,15 +61,19 @@ function App() {
|
|||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [imagesData, aliasesData] = await Promise.all([
|
const aliasesData = await api.getAllAliases();
|
||||||
api.getImages({
|
setAllAliases(aliasesData);
|
||||||
search: searchQuery || undefined,
|
|
||||||
}),
|
// Get images for all aliases or filtered aliases
|
||||||
api.getAllAliases(),
|
const aliasIds = searchQuery
|
||||||
]);
|
? aliasesData.filter(a => a.name.toLowerCase().includes(searchQuery.toLowerCase())).map(a => a.id)
|
||||||
|
: aliasesData.map(a => a.id);
|
||||||
|
|
||||||
|
const imagesData = aliasIds.length > 0
|
||||||
|
? await api.getImages({ aliasIds })
|
||||||
|
: [];
|
||||||
|
|
||||||
setImages(imagesData);
|
setImages(imagesData);
|
||||||
setAllAliases(aliasesData);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load data:', error);
|
console.error('Failed to load data:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -46,25 +81,41 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLoginSuccess = () => {
|
||||||
|
setIsLoginMode(false);
|
||||||
|
loadData();
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpload = async (file: File, aliases: string[]) => {
|
const handleUpload = async (file: File, aliases: string[]) => {
|
||||||
await api.uploadImage(file, aliases);
|
console.log("Trying to upload image:");
|
||||||
|
console.log(file.name);
|
||||||
|
console.log(aliases);
|
||||||
|
const image = await api.uploadImage(file);
|
||||||
|
if (aliases.length > 0) {
|
||||||
|
await api.updateImageAliases(image.id, aliases);
|
||||||
|
}
|
||||||
await loadData();
|
await loadData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveImage = async (imageId: string, aliases: string[]) => {
|
const handleSaveImage = async (imageId: number, aliases: string[]) => {
|
||||||
await api.updateImageAliases(imageId, aliases);
|
await api.updateImageAliases(imageId, aliases);
|
||||||
await loadData();
|
await loadData();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteImage = async (imageId: string) => {
|
const handleDeleteImage = async (imageId: number) => {
|
||||||
await api.deleteImage(imageId);
|
await api.deleteImage(imageId);
|
||||||
await loadData();
|
await loadData();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Show login page if in login mode
|
||||||
|
if (isLoginMode) {
|
||||||
|
return <Login onLoginSuccess={handleLoginSuccess} />;
|
||||||
|
}
|
||||||
|
|
||||||
// Filter aliases based on search
|
// Filter aliases based on search
|
||||||
const filteredAliases = searchQuery
|
const filteredAliases = searchQuery
|
||||||
? allAliases.filter(alias =>
|
? allAliases.filter(alias =>
|
||||||
alias.toLowerCase().includes(searchQuery.toLowerCase())
|
alias.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
)
|
)
|
||||||
: allAliases;
|
: allAliases;
|
||||||
|
|
||||||
@@ -75,9 +126,10 @@ function App() {
|
|||||||
const currentPageAliases = filteredAliases.slice(startIndex, endIndex);
|
const currentPageAliases = filteredAliases.slice(startIndex, endIndex);
|
||||||
|
|
||||||
// Filter images to only show those with current page aliases
|
// Filter images to only show those with current page aliases
|
||||||
|
const currentPageAliasIds = currentPageAliases.map(a => a.id);
|
||||||
const currentPageImages = images.filter(img =>
|
const currentPageImages = images.filter(img =>
|
||||||
img.aliases.some(alias => currentPageAliases.includes(alias)) ||
|
img.aliasesIds.some(aliasId => currentPageAliasIds.includes(aliasId)) ||
|
||||||
(img.aliases.length === 0 && currentPageAliases.length > 0)
|
(img.aliasesIds.length === 0 && currentPageAliases.length > 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
@@ -155,114 +207,3 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
// import { useState, useEffect } from 'react';
|
|
||||||
// import Navbar from './components/Navbar';
|
|
||||||
// import Sidebar from './components/Sidebar';
|
|
||||||
// import SearchBar from './components/SearchBar';
|
|
||||||
// import ImageGrid from './components/ImageGrid';
|
|
||||||
// import ImageModal from './components/ImageModal';
|
|
||||||
// import UploadModal from './components/UploadModal';
|
|
||||||
// import { api } from './api-mock';
|
|
||||||
// import type { Image } from './types';
|
|
||||||
|
|
||||||
// function App() {
|
|
||||||
// const [images, setImages] = useState<Image[]>([]);
|
|
||||||
// const [allAliases, setAllAliases] = useState<string[]>([]);
|
|
||||||
// const [searchQuery, setSearchQuery] = useState('');
|
|
||||||
// const [selectedAlias, setSelectedAlias] = useState<string | null>(null);
|
|
||||||
// const [selectedImage, setSelectedImage] = useState<Image | null>(null);
|
|
||||||
// const [showUploadModal, setShowUploadModal] = useState(false);
|
|
||||||
// const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// loadData();
|
|
||||||
// }, [searchQuery, selectedAlias]);
|
|
||||||
|
|
||||||
// const loadData = async () => {
|
|
||||||
// setLoading(true);
|
|
||||||
// try {
|
|
||||||
// const [imagesData, aliasesData] = await Promise.all([
|
|
||||||
// api.getImages({
|
|
||||||
// search: searchQuery || undefined,
|
|
||||||
// null_alias: selectedAlias === null ? true : undefined,
|
|
||||||
// }),
|
|
||||||
// api.getAllAliases(),
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
// let filteredImages = imagesData;
|
|
||||||
// if (selectedAlias) {
|
|
||||||
// filteredImages = imagesData.filter((img) =>
|
|
||||||
// img.aliases.includes(selectedAlias)
|
|
||||||
// );
|
|
||||||
// } else if (selectedAlias === null) {
|
|
||||||
// filteredImages = imagesData.filter((img) => img.aliases.length === 0);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setImages(filteredImages);
|
|
||||||
// setAllAliases(aliasesData);
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Failed to load data:', error);
|
|
||||||
// } finally {
|
|
||||||
// setLoading(false);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleUpload = async (file: File, aliases: string[]) => {
|
|
||||||
// await api.uploadImage(file, aliases);
|
|
||||||
// await loadData();
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleSaveImage = async (imageId: string, aliases: string[]) => {
|
|
||||||
// await api.updateImageAliases(imageId, aliases);
|
|
||||||
// await loadData();
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const handleDeleteImage = async (imageId: string) => {
|
|
||||||
// await api.deleteImage(imageId);
|
|
||||||
// await loadData();
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <div className="h-screen flex flex-col">
|
|
||||||
// <Navbar
|
|
||||||
// onUploadClick={() => setShowUploadModal(true)}
|
|
||||||
// />
|
|
||||||
// <div className="flex-1 flex overflow-hidden">
|
|
||||||
// <Sidebar
|
|
||||||
// aliases={allAliases}
|
|
||||||
// selectedAlias={selectedAlias}
|
|
||||||
// onAliasClick={setSelectedAlias}
|
|
||||||
// />
|
|
||||||
// <main className="flex-1 overflow-y-auto">
|
|
||||||
// <SearchBar value={searchQuery} onChange={setSearchQuery} />
|
|
||||||
// {loading ? (
|
|
||||||
// <div className="flex items-center justify-center h-64">
|
|
||||||
// <div className="text-gray-500">Loading...</div>
|
|
||||||
// </div>
|
|
||||||
// ) : (
|
|
||||||
// <ImageGrid images={images} onImageClick={setSelectedImage} />
|
|
||||||
// )}
|
|
||||||
// </main>
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// {selectedImage && (
|
|
||||||
// <ImageModal
|
|
||||||
// image={selectedImage}
|
|
||||||
// onClose={() => setSelectedImage(null)}
|
|
||||||
// onSave={handleSaveImage}
|
|
||||||
// onDelete={handleDeleteImage}
|
|
||||||
// />
|
|
||||||
// )}
|
|
||||||
|
|
||||||
// {showUploadModal && (
|
|
||||||
// <UploadModal
|
|
||||||
// onClose={() => setShowUploadModal(false)}
|
|
||||||
// onUpload={handleUpload}
|
|
||||||
// />
|
|
||||||
// )}
|
|
||||||
// </div>
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// export default App;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Image } from './types';
|
import type { Image, Alias } from './types';
|
||||||
|
|
||||||
const API_BASE_URL = 'http://localhost:8080';
|
const API_BASE_URL = 'http://localhost:8080';
|
||||||
|
|
||||||
@@ -6,97 +6,136 @@ const API_BASE_URL = 'http://localhost:8080';
|
|||||||
export const ALIASES_PER_PAGE = 5; // Number of aliases to show per page
|
export const ALIASES_PER_PAGE = 5; // Number of aliases to show per page
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
// Images
|
// Authentication
|
||||||
|
async login(token: string): Promise<void> {
|
||||||
|
console.log("Hello, someone is trying to access the web page.");
|
||||||
|
console.log("Token: ", token);
|
||||||
|
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include', // IMPORTANT: Allows cookies to be set
|
||||||
|
body: JSON.stringify({ token }),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.log("Login failed");
|
||||||
|
throw new Error(errorText || 'Login failed');
|
||||||
|
}
|
||||||
|
console.log("Login successful");
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/images?images=1,2,3&aliases=1,2,3
|
||||||
|
// Returns: Image[] with {id, uploadedUserId, uploadedAt, aliasesIds, extension}
|
||||||
async getImages(params?: {
|
async getImages(params?: {
|
||||||
search?: string;
|
imageIds?: number[];
|
||||||
null_alias?: boolean;
|
aliasIds?: number[];
|
||||||
limit?: number;
|
|
||||||
page?: number;
|
|
||||||
}): Promise<Image[]> {
|
}): Promise<Image[]> {
|
||||||
const queryParams = new URLSearchParams();
|
const queryParams = new URLSearchParams();
|
||||||
if (params?.search) queryParams.append('search', params.search);
|
if (params?.imageIds && params.imageIds.length > 0) {
|
||||||
if (params?.null_alias) queryParams.append('null_alias', 'true');
|
queryParams.append('images', params.imageIds.join(','));
|
||||||
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
}
|
||||||
if (params?.page) queryParams.append('page', params.page.toString());
|
if (params?.aliasIds && params.aliasIds.length > 0) {
|
||||||
|
queryParams.append('aliases', params.aliasIds.join(','));
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${API_BASE_URL}/api/images?${queryParams}`
|
`${API_BASE_URL}/api/images?${queryParams}`,
|
||||||
|
{ credentials: 'include' }
|
||||||
);
|
);
|
||||||
if (!response.ok) throw new Error('Failed to fetch images');
|
if (!response.ok) throw new Error('Failed to fetch images');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.images || [];
|
return data || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getImage(id: string): Promise<Image> {
|
// GET /api/images?images={id}
|
||||||
const response = await fetch(`${API_BASE_URL}/api/images/${id}`);
|
// Returns single image by querying with image ID
|
||||||
|
async getImage(id: number): Promise<Image> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/images?images=${id}`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
if (!response.ok) throw new Error('Failed to fetch image');
|
if (!response.ok) throw new Error('Failed to fetch image');
|
||||||
return response.json();
|
const data = await response.json();
|
||||||
|
return data[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadImage(file: File, aliases: string[]): Promise<Image> {
|
// POST /api/image
|
||||||
const formData = new FormData();
|
// Content-Type: image/png, image/jpeg, image/gif
|
||||||
formData.append('imgfile', file);
|
// Returns: {id, uploadedUserId, uploadedAt, extension}
|
||||||
aliases.forEach((alias) => formData.append('aliases', alias));
|
async uploadImage(file: File): Promise<Image> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/image`, {
|
||||||
const response = await fetch(`${API_BASE_URL}/api/images`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
headers: {
|
||||||
|
'Content-Type': file.type,
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
body: file,
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('Failed to upload image');
|
if (!response.ok) throw new Error('Failed to upload image');
|
||||||
return response.json();
|
const result = await response.json();
|
||||||
|
// Convert uploadedAt from string to number if needed
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
aliasesIds: [],
|
||||||
|
uploadedAt: typeof result.uploadedAt === 'string'
|
||||||
|
? new Date(result.uploadedAt).getTime() / 1000
|
||||||
|
: result.uploadedAt
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteImage(id: string): Promise<void> {
|
// DELETE /api/image/{id}
|
||||||
const response = await fetch(`${API_BASE_URL}/api/images/${id}`, {
|
// Deletes image along with the links
|
||||||
|
async deleteImage(id: number): Promise<void> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/image/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('Failed to delete image');
|
if (!response.ok) throw new Error('Failed to delete image');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aliases
|
// GET /api/aliases
|
||||||
async getAllAliases(): Promise<string[]> {
|
// Returns: Alias[] with {id, name}
|
||||||
const response = await fetch(`${API_BASE_URL}/api/aliases`);
|
async getAllAliases(): Promise<Alias[]> {
|
||||||
if (!response.ok) throw new Error('Failed to fetch aliases');
|
const response = await fetch(`${API_BASE_URL}/api/aliases`, {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.log("Failed to fetch aliases");
|
||||||
|
throw new Error('Failed to fetch aliases');
|
||||||
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data.aliases || [];
|
return data || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateImageAliases(id: string, aliases: string[]): Promise<Image> {
|
// PUT /api/image/{id}/aliases
|
||||||
const response = await fetch(`${API_BASE_URL}/api/images/${id}/aliases`, {
|
// Body: {aliases: string[]}
|
||||||
|
async updateImageAliases(id: number, aliasNames: string[]): Promise<void> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/image/${id}/aliases`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ aliases }),
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ aliases: aliasNames }),
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error('Failed to update aliases');
|
if (!response.ok) throw new Error('Failed to update aliases');
|
||||||
return response.json();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async addImageAlias(id: string, alias: string): Promise<Image> {
|
// DELETE /api/alias/{id}
|
||||||
const response = await fetch(`${API_BASE_URL}/api/images/${id}/aliases`, {
|
// Deletes alias along with the links
|
||||||
method: 'POST',
|
async deleteAlias(id: number): Promise<void> {
|
||||||
headers: {
|
const response = await fetch(`${API_BASE_URL}/api/alias/${id}`, {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ alias }),
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error('Failed to add alias');
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeImageAlias(id: string, alias: string): Promise<void> {
|
|
||||||
const response = await fetch(
|
|
||||||
`${API_BASE_URL}/api/images/${id}/aliases?alias=${encodeURIComponent(alias)}`,
|
|
||||||
{
|
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
}
|
credentials: 'include',
|
||||||
);
|
});
|
||||||
if (!response.ok) throw new Error('Failed to remove alias');
|
if (!response.ok) throw new Error('Failed to delete alias');
|
||||||
}
|
}
|
||||||
|
|
||||||
getImageUrl(id: string): string {
|
// Construct image URL from image ID and extension
|
||||||
return `${API_BASE_URL}/api/images/${id}/file`;
|
// Format: /img/{id}.{extension}
|
||||||
|
getImageUrl(id: number, extension: string): string {
|
||||||
|
return `${API_BASE_URL}/img/${id}.${extension}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
import type { Image } from '../types';
|
import type { Image, Alias } from '../types';
|
||||||
|
import { api } from '../api';
|
||||||
|
|
||||||
interface ImageGridProps {
|
interface ImageGridProps {
|
||||||
images: Image[];
|
images: Image[];
|
||||||
aliases: string[];
|
aliases: Alias[];
|
||||||
onImageClick: (image: Image) => void;
|
onImageClick: (image: Image) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
|
const aliasMap = new Map(aliases.map(a => [a.id, a.name]));
|
||||||
|
|
||||||
// Group images by alias
|
// Group images by alias
|
||||||
const groupedImages = images.reduce((acc, image) => {
|
const groupedImages = images.reduce((acc, image) => {
|
||||||
if (image.aliases.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 {
|
||||||
image.aliases.forEach((alias) => {
|
image.aliasesIds.forEach((aliasId) => {
|
||||||
if (!acc[alias]) acc[alias] = [];
|
const aliasName = aliasMap.get(aliasId) || `Alias #${aliasId}`;
|
||||||
acc[alias].push(image);
|
if (!acc[aliasName]) acc[aliasName] = [];
|
||||||
|
acc[aliasName].push(image);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, Image[]>);
|
}, {} as Record<string, Image[]>);
|
||||||
|
|
||||||
// Filter to only show aliases on current page
|
const filteredGroups = Object.entries(groupedImages);
|
||||||
const filteredGroups = Object.entries(groupedImages).filter(([alias]) =>
|
|
||||||
aliases.includes(alias) || alias === '__no_alias__'
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-8">
|
<div className="p-6 space-y-8">
|
||||||
@@ -41,8 +43,8 @@ export default function ImageGrid({ images, aliases, onImageClick }: ImageGridPr
|
|||||||
className="aspect-square rounded-lg overflow-hidden bg-gray-100 hover:ring-2 hover:ring-blue-500 transition-all"
|
className="aspect-square rounded-lg overflow-hidden bg-gray-100 hover:ring-2 hover:ring-blue-500 transition-all"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`http://localhost:8080${image.url}`}
|
src={api.getImageUrl(image.id, image.extension)}
|
||||||
alt={image.aliases.join(', ')}
|
alt={`Image ${image.id}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import type { Image } from '../types';
|
import type { Image } from '../types';
|
||||||
|
import { api } from '../api';
|
||||||
|
|
||||||
interface ImageModalProps {
|
interface ImageModalProps {
|
||||||
image: Image;
|
image: Image;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (imageId: string, aliases: string[]) => Promise<void>;
|
onSave: (imageId: number, aliases: string[]) => Promise<void>;
|
||||||
onDelete: (imageId: string) => Promise<void>;
|
onDelete: (imageId: number) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ImageModal({
|
export default function ImageModal({
|
||||||
@@ -14,12 +15,20 @@ export default function ImageModal({
|
|||||||
onSave,
|
onSave,
|
||||||
onDelete,
|
onDelete,
|
||||||
}: ImageModalProps) {
|
}: ImageModalProps) {
|
||||||
const [aliases, setAliases] = useState<string[]>(image.aliases);
|
const [aliases, setAliases] = useState<string[]>([]);
|
||||||
const [newAlias, setNewAlias] = useState('');
|
const [newAlias, setNewAlias] = useState('');
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [allAliases, setAllAliases] = useState<{id: number, name: string}[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAliases(image.aliases);
|
// Load all aliases and map image's aliasesIds to names
|
||||||
|
api.getAllAliases().then(allAliasesData => {
|
||||||
|
setAllAliases(allAliasesData);
|
||||||
|
const aliasNames = image.aliasesIds
|
||||||
|
.map(id => allAliasesData.find(a => a.id === id)?.name)
|
||||||
|
.filter((name): name is string => name !== undefined);
|
||||||
|
setAliases(aliasNames);
|
||||||
|
});
|
||||||
}, [image]);
|
}, [image]);
|
||||||
|
|
||||||
const handleAddAlias = () => {
|
const handleAddAlias = () => {
|
||||||
@@ -68,21 +77,24 @@ export default function ImageModal({
|
|||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="mb-6 rounded-lg overflow-hidden bg-gray-100">
|
<div className="mb-6 rounded-lg overflow-hidden bg-gray-100">
|
||||||
<img
|
<img
|
||||||
src={`http://localhost:8080${image.url}`}
|
src={api.getImageUrl(image.id, image.extension)}
|
||||||
alt={image.aliases.join(', ')}
|
alt={`Image ${image.id}`}
|
||||||
className="w-full h-auto"
|
className="w-full h-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 mb-6">
|
<div className="space-y-4 mb-6">
|
||||||
<div className="text-sm text-gray-600">
|
<div className="text-sm text-gray-600">
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Image ID:</span> {image.id}
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">Uploaded Time:</span>{' '}
|
<span className="font-medium">Uploaded Time:</span>{' '}
|
||||||
{new Date(image.uploaded_at).toLocaleString()}
|
{new Date(image.uploadedAt * 1000).toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">Uploaded By:</span>{' '}
|
<span className="font-medium">Uploaded By:</span>{' '}
|
||||||
{image.uploaded_user_id}
|
{image.uploadedUserId}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import type { Alias } from '../types';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
aliases: string[];
|
aliases: Alias[];
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
}
|
}
|
||||||
@@ -23,9 +25,10 @@ export default function Sidebar({
|
|||||||
</div>
|
</div>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1">
|
||||||
{aliases.map((alias) => (
|
{aliases.map((alias) => (
|
||||||
<li key={alias}>
|
<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">
|
||||||
{alias}
|
<span className="text-xs text-blue-500 mr-2">#{alias.id}</span>
|
||||||
|
{alias.name}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
|
|||||||
alert('Please select a file');
|
alert('Please select a file');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (aliases.length === 0) {
|
||||||
|
alert('Please add at least one alias');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
try {
|
try {
|
||||||
await onUpload(file, aliases);
|
await onUpload(file, aliases);
|
||||||
@@ -100,6 +104,7 @@ export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
|
|||||||
alt="Preview"
|
alt="Preview"
|
||||||
className="max-h-64 mx-auto rounded-lg"
|
className="max-h-64 mx-auto rounded-lg"
|
||||||
/>
|
/>
|
||||||
|
{/* Red-Rounded "X" Button at Top-Right Corner of Image Window: Remove selected image and clear preview */}
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFile(null);
|
setFile(null);
|
||||||
@@ -166,6 +171,7 @@ 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
|
<button
|
||||||
onClick={() => handleRemoveAlias(alias)}
|
onClick={() => handleRemoveAlias(alias)}
|
||||||
className="text-red-600 hover:text-red-700"
|
className="text-red-600 hover:text-red-700"
|
||||||
@@ -193,6 +199,7 @@ export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
|
|||||||
</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
|
<button
|
||||||
onClick={handleAddAlias}
|
onClick={handleAddAlias}
|
||||||
className="text-green-600 hover:text-green-700"
|
className="text-green-600 hover:text-green-700"
|
||||||
@@ -224,15 +231,17 @@ export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4 border-t border-gray-200">
|
<div className="flex justify-end gap-2 pt-4 border-t border-gray-200">
|
||||||
|
{/* Cancel Button: Close modal without uploading */}
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors font-medium"
|
className="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors font-medium"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
{/* Upload Button: Upload image with aliases to server */}
|
||||||
<button
|
<button
|
||||||
onClick={handleUpload}
|
onClick={handleUpload}
|
||||||
disabled={!file || isUploading}
|
disabled={!file || aliases.length === 0 || isUploading}
|
||||||
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"
|
||||||
>
|
>
|
||||||
{isUploading ? 'Uploading...' : 'Upload'}
|
{isUploading ? 'Uploading...' : 'Upload'}
|
||||||
|
|||||||
89
webpage/src/pages/Login.tsx
Normal file
89
webpage/src/pages/Login.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// src/pages/Login.tsx
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface LoginProps {
|
||||||
|
onLoginSuccess: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Login({ onLoginSuccess }: LoginProps) {
|
||||||
|
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
|
||||||
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const token = urlParams.get('token');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
setStatus('error');
|
||||||
|
setErrorMessage('No token provided in URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call backend login endpoint
|
||||||
|
fetch('http://localhost:8080/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include', // IMPORTANT: This allows cookies to be set
|
||||||
|
body: JSON.stringify({ token }),
|
||||||
|
})
|
||||||
|
.then(async (res) => {
|
||||||
|
if (res.ok) {
|
||||||
|
setStatus('success');
|
||||||
|
// Remove token from URL for security
|
||||||
|
window.history.replaceState({}, document.title, '/');
|
||||||
|
// Wait a moment then notify parent
|
||||||
|
setTimeout(() => {
|
||||||
|
onLoginSuccess();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
const error = await res.text();
|
||||||
|
setStatus('error');
|
||||||
|
setErrorMessage(error || 'Login failed');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setStatus('error');
|
||||||
|
setErrorMessage(err.message);
|
||||||
|
});
|
||||||
|
}, [onLoginSuccess]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||||
|
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8">
|
||||||
|
<div className="text-center">
|
||||||
|
{status === 'loading' && (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">Logging in...</h1>
|
||||||
|
<p className="text-gray-600">Please wait while we authenticate you.</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'success' && (
|
||||||
|
<>
|
||||||
|
<div className="text-green-500 text-5xl mb-4">✓</div>
|
||||||
|
<h1 className="text-2xl font-bold text-green-600 mb-2">Login Successful!</h1>
|
||||||
|
<p className="text-gray-600">Redirecting to the app...</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{status === 'error' && (
|
||||||
|
<>
|
||||||
|
<div className="text-red-500 text-5xl mb-4">✗</div>
|
||||||
|
<h1 className="text-2xl font-bold text-red-600 mb-2">Login Failed</h1>
|
||||||
|
<p className="text-gray-600 mb-4">{errorMessage}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = '/'}
|
||||||
|
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
|
||||||
|
>
|
||||||
|
Go to Home
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
export interface Image {
|
export interface Alias {
|
||||||
id: string;
|
id: number;
|
||||||
uploaded_user_id: string;
|
name: string;
|
||||||
uploaded_at: string;
|
|
||||||
aliases: string[];
|
|
||||||
url: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface Image {
|
||||||
id: string;
|
id: number;
|
||||||
username: string;
|
uploadedUserId: string;
|
||||||
|
uploadedAt: number;
|
||||||
|
aliasesIds: number[];
|
||||||
|
extension: string;
|
||||||
}
|
}
|
||||||
@@ -6,4 +6,8 @@ export default defineConfig({
|
|||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
],
|
],
|
||||||
|
server: {
|
||||||
|
port: 48763,
|
||||||
|
strictPort: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user