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",
|
||||
"description": "2025 Golang Final Project",
|
||||
"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 UploadModal from './components/UploadModal';
|
||||
import Pagination from './components/Pagination';
|
||||
import { api, ALIASES_PER_PAGE } from './api-mock';
|
||||
import type { Image } from './types';
|
||||
import Login from './pages/Login';
|
||||
import { api, ALIASES_PER_PAGE } from './api';
|
||||
import type { Image, Alias } from './types';
|
||||
|
||||
function App() {
|
||||
const [isLoginMode, setIsLoginMode] = useState(false);
|
||||
const [images, setImages] = useState<Image[]>([]);
|
||||
const [allAliases, setAllAliases] = useState<string[]>([]);
|
||||
const [allAliases, setAllAliases] = useState<Alias[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedImage, setSelectedImage] = useState<Image | null>(null);
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
console.log("Hello");
|
||||
|
||||
// Check if URL has a login token
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [searchQuery]);
|
||||
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();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Login failed:", error);
|
||||
setIsLoginMode(true); // Show login page on error
|
||||
});
|
||||
} else {
|
||||
loadData();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoginMode) {
|
||||
loadData();
|
||||
}
|
||||
}, [searchQuery, isLoginMode]);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset to page 1 when search query changes
|
||||
@@ -30,15 +61,19 @@ function App() {
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [imagesData, aliasesData] = await Promise.all([
|
||||
api.getImages({
|
||||
search: searchQuery || undefined,
|
||||
}),
|
||||
api.getAllAliases(),
|
||||
]);
|
||||
const aliasesData = await api.getAllAliases();
|
||||
setAllAliases(aliasesData);
|
||||
|
||||
// Get images for all aliases or filtered aliases
|
||||
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);
|
||||
setAllAliases(aliasesData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load data:', error);
|
||||
} finally {
|
||||
@@ -46,25 +81,41 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginSuccess = () => {
|
||||
setIsLoginMode(false);
|
||||
loadData();
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
const handleSaveImage = async (imageId: string, aliases: string[]) => {
|
||||
const handleSaveImage = async (imageId: number, aliases: string[]) => {
|
||||
await api.updateImageAliases(imageId, aliases);
|
||||
await loadData();
|
||||
};
|
||||
|
||||
const handleDeleteImage = async (imageId: string) => {
|
||||
const handleDeleteImage = async (imageId: number) => {
|
||||
await api.deleteImage(imageId);
|
||||
await loadData();
|
||||
};
|
||||
|
||||
// Show login page if in login mode
|
||||
if (isLoginMode) {
|
||||
return <Login onLoginSuccess={handleLoginSuccess} />;
|
||||
}
|
||||
|
||||
// Filter aliases based on search
|
||||
const filteredAliases = searchQuery
|
||||
? allAliases.filter(alias =>
|
||||
alias.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
alias.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: allAliases;
|
||||
|
||||
@@ -75,9 +126,10 @@ function App() {
|
||||
const currentPageAliases = filteredAliases.slice(startIndex, endIndex);
|
||||
|
||||
// Filter images to only show those with current page aliases
|
||||
const currentPageAliasIds = currentPageAliases.map(a => a.id);
|
||||
const currentPageImages = images.filter(img =>
|
||||
img.aliases.some(alias => currentPageAliases.includes(alias)) ||
|
||||
(img.aliases.length === 0 && currentPageAliases.length > 0)
|
||||
img.aliasesIds.some(aliasId => currentPageAliasIds.includes(aliasId)) ||
|
||||
(img.aliasesIds.length === 0 && currentPageAliases.length > 0)
|
||||
);
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
@@ -154,115 +206,4 @@ function 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;
|
||||
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';
|
||||
|
||||
@@ -6,97 +6,136 @@ const API_BASE_URL = 'http://localhost:8080';
|
||||
export const ALIASES_PER_PAGE = 5; // Number of aliases to show per page
|
||||
|
||||
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?: {
|
||||
search?: string;
|
||||
null_alias?: boolean;
|
||||
limit?: number;
|
||||
page?: number;
|
||||
imageIds?: number[];
|
||||
aliasIds?: number[];
|
||||
}): Promise<Image[]> {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.search) queryParams.append('search', params.search);
|
||||
if (params?.null_alias) queryParams.append('null_alias', 'true');
|
||||
if (params?.limit) queryParams.append('limit', params.limit.toString());
|
||||
if (params?.page) queryParams.append('page', params.page.toString());
|
||||
if (params?.imageIds && params.imageIds.length > 0) {
|
||||
queryParams.append('images', params.imageIds.join(','));
|
||||
}
|
||||
if (params?.aliasIds && params.aliasIds.length > 0) {
|
||||
queryParams.append('aliases', params.aliasIds.join(','));
|
||||
}
|
||||
|
||||
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');
|
||||
const data = await response.json();
|
||||
return data.images || [];
|
||||
return data || [];
|
||||
}
|
||||
|
||||
async getImage(id: string): Promise<Image> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/images/${id}`);
|
||||
// GET /api/images?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');
|
||||
return response.json();
|
||||
const data = await response.json();
|
||||
return data[0];
|
||||
}
|
||||
|
||||
async uploadImage(file: File, aliases: string[]): Promise<Image> {
|
||||
const formData = new FormData();
|
||||
formData.append('imgfile', file);
|
||||
aliases.forEach((alias) => formData.append('aliases', alias));
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/images`, {
|
||||
// POST /api/image
|
||||
// Content-Type: image/png, image/jpeg, image/gif
|
||||
// Returns: {id, uploadedUserId, uploadedAt, extension}
|
||||
async uploadImage(file: File): Promise<Image> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/image`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Content-Type': file.type,
|
||||
},
|
||||
credentials: 'include',
|
||||
body: file,
|
||||
});
|
||||
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> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/images/${id}`, {
|
||||
// DELETE /api/image/{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',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to delete image');
|
||||
}
|
||||
|
||||
// Aliases
|
||||
async getAllAliases(): Promise<string[]> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/aliases`);
|
||||
if (!response.ok) throw new Error('Failed to fetch aliases');
|
||||
// GET /api/aliases
|
||||
// Returns: Alias[] with {id, name}
|
||||
async getAllAliases(): Promise<Alias[]> {
|
||||
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();
|
||||
return data.aliases || [];
|
||||
return data || [];
|
||||
}
|
||||
|
||||
async updateImageAliases(id: string, aliases: string[]): Promise<Image> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/images/${id}/aliases`, {
|
||||
// PUT /api/image/{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',
|
||||
headers: {
|
||||
'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');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async addImageAlias(id: string, alias: string): Promise<Image> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/images/${id}/aliases`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ alias }),
|
||||
// DELETE /api/alias/{id}
|
||||
// Deletes alias along with the links
|
||||
async deleteAlias(id: number): Promise<void> {
|
||||
const response = await fetch(`${API_BASE_URL}/api/alias/${id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) throw new Error('Failed to add alias');
|
||||
return response.json();
|
||||
if (!response.ok) throw new Error('Failed to delete alias');
|
||||
}
|
||||
|
||||
async removeImageAlias(id: string, alias: string): Promise<void> {
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/api/images/${id}/aliases?alias=${encodeURIComponent(alias)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
if (!response.ok) throw new Error('Failed to remove alias');
|
||||
}
|
||||
|
||||
getImageUrl(id: string): string {
|
||||
return `${API_BASE_URL}/api/images/${id}/file`;
|
||||
// Construct image URL from image ID and extension
|
||||
// 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 {
|
||||
images: Image[];
|
||||
aliases: string[];
|
||||
aliases: Alias[];
|
||||
onImageClick: (image: Image) => void;
|
||||
}
|
||||
|
||||
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
|
||||
const groupedImages = images.reduce((acc, image) => {
|
||||
if (image.aliases.length === 0) {
|
||||
if (image.aliasesIds.length === 0) {
|
||||
if (!acc['__no_alias__']) acc['__no_alias__'] = [];
|
||||
acc['__no_alias__'].push(image);
|
||||
} else {
|
||||
image.aliases.forEach((alias) => {
|
||||
if (!acc[alias]) acc[alias] = [];
|
||||
acc[alias].push(image);
|
||||
image.aliasesIds.forEach((aliasId) => {
|
||||
const aliasName = aliasMap.get(aliasId) || `Alias #${aliasId}`;
|
||||
if (!acc[aliasName]) acc[aliasName] = [];
|
||||
acc[aliasName].push(image);
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, Image[]>);
|
||||
|
||||
// Filter to only show aliases on current page
|
||||
const filteredGroups = Object.entries(groupedImages).filter(([alias]) =>
|
||||
aliases.includes(alias) || alias === '__no_alias__'
|
||||
);
|
||||
const filteredGroups = Object.entries(groupedImages);
|
||||
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
<img
|
||||
src={`http://localhost:8080${image.url}`}
|
||||
alt={image.aliases.join(', ')}
|
||||
src={api.getImageUrl(image.id, image.extension)}
|
||||
alt={`Image ${image.id}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { Image } from '../types';
|
||||
import { api } from '../api';
|
||||
|
||||
interface ImageModalProps {
|
||||
image: Image;
|
||||
onClose: () => void;
|
||||
onSave: (imageId: string, aliases: string[]) => Promise<void>;
|
||||
onDelete: (imageId: string) => Promise<void>;
|
||||
onSave: (imageId: number, aliases: string[]) => Promise<void>;
|
||||
onDelete: (imageId: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function ImageModal({
|
||||
@@ -14,12 +15,20 @@ export default function ImageModal({
|
||||
onSave,
|
||||
onDelete,
|
||||
}: ImageModalProps) {
|
||||
const [aliases, setAliases] = useState<string[]>(image.aliases);
|
||||
const [aliases, setAliases] = useState<string[]>([]);
|
||||
const [newAlias, setNewAlias] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [allAliases, setAllAliases] = useState<{id: number, name: string}[]>([]);
|
||||
|
||||
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]);
|
||||
|
||||
const handleAddAlias = () => {
|
||||
@@ -68,21 +77,24 @@ export default function ImageModal({
|
||||
<div className="p-6">
|
||||
<div className="mb-6 rounded-lg overflow-hidden bg-gray-100">
|
||||
<img
|
||||
src={`http://localhost:8080${image.url}`}
|
||||
alt={image.aliases.join(', ')}
|
||||
src={api.getImageUrl(image.id, image.extension)}
|
||||
alt={`Image ${image.id}`}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
<div className="text-sm text-gray-600">
|
||||
<p>
|
||||
<span className="font-medium">Image ID:</span> {image.id}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Uploaded Time:</span>{' '}
|
||||
{new Date(image.uploaded_at).toLocaleString()}
|
||||
{new Date(image.uploadedAt * 1000).toLocaleString()}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Uploaded By:</span>{' '}
|
||||
{image.uploaded_user_id}
|
||||
{image.uploadedUserId}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Alias } from '../types';
|
||||
|
||||
interface SidebarProps {
|
||||
aliases: string[];
|
||||
aliases: Alias[];
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
}
|
||||
@@ -23,9 +25,10 @@ export default function Sidebar({
|
||||
</div>
|
||||
<ul className="space-y-1">
|
||||
{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">
|
||||
{alias}
|
||||
<span className="text-xs text-blue-500 mr-2">#{alias.id}</span>
|
||||
{alias.name}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -57,6 +57,10 @@ export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
|
||||
alert('Please select a file');
|
||||
return;
|
||||
}
|
||||
if (aliases.length === 0) {
|
||||
alert('Please add at least one alias');
|
||||
return;
|
||||
}
|
||||
setIsUploading(true);
|
||||
try {
|
||||
await onUpload(file, aliases);
|
||||
@@ -100,6 +104,7 @@ export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
|
||||
alt="Preview"
|
||||
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
|
||||
onClick={() => {
|
||||
setFile(null);
|
||||
@@ -166,6 +171,7 @@ export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
|
||||
<div className="space-y-2">
|
||||
{aliases.map((alias) => (
|
||||
<div key={alias} className="flex items-center gap-2">
|
||||
{/* Red "X" Button: Remove this alias from the list */}
|
||||
<button
|
||||
onClick={() => handleRemoveAlias(alias)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
@@ -193,6 +199,7 @@ export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Green "+" Button: Add new alias to the list */}
|
||||
<button
|
||||
onClick={handleAddAlias}
|
||||
className="text-green-600 hover:text-green-700"
|
||||
@@ -224,15 +231,17 @@ export default function UploadModal({ onClose, onUpload }: UploadModalProps) {
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4 border-t border-gray-200">
|
||||
{/* Cancel Button: Close modal without uploading */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors font-medium"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{/* Upload Button: Upload image with aliases to server */}
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{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 {
|
||||
id: string;
|
||||
uploaded_user_id: string;
|
||||
uploaded_at: string;
|
||||
aliases: string[];
|
||||
url: string;
|
||||
export interface Alias {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
export interface Image {
|
||||
id: number;
|
||||
uploadedUserId: string;
|
||||
uploadedAt: number;
|
||||
aliasesIds: number[];
|
||||
extension: string;
|
||||
}
|
||||
@@ -6,4 +6,8 @@ export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
],
|
||||
server: {
|
||||
port: 48763,
|
||||
strictPort: false,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user