Added Dockerfile
This commit is contained in:
15
.dockerignore
Normal file
15
.dockerignore
Normal file
@@ -0,0 +1,15 @@
|
||||
webpage/node_modules
|
||||
webpage/npm-debug.log
|
||||
webpage/.DS_Store
|
||||
webpage/dist
|
||||
.git
|
||||
.gitignore
|
||||
webpage/.env
|
||||
webpage/.env.local
|
||||
webpage/.env.*.local
|
||||
webpage/coverage
|
||||
.vscode
|
||||
.idea
|
||||
*.log
|
||||
tests/
|
||||
api/
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
.DS_Store
|
||||
|
||||
tests/*
|
||||
frontend-web
|
||||
|
||||
################################
|
||||
# Node.js/React (web/) 專屬
|
||||
|
||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY webpage/package*.json ./
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY webpage/ .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 48763
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
24
nginx.conf
Normal file
24
nginx.conf
Normal file
@@ -0,0 +1,24 @@
|
||||
server {
|
||||
listen 48763;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
|
||||
}
|
||||
24
webpage/nginx.conf
Normal file
24
webpage/nginx.conf
Normal file
@@ -0,0 +1,24 @@
|
||||
server {
|
||||
listen 48763;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://backend:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import type { Image } from './types';
|
||||
import fakeDb from './assets/tests/fakedb.json';
|
||||
|
||||
export const ALIASES_PER_PAGE = 5;
|
||||
|
||||
class MockApiService {
|
||||
private images: Image[] = [];
|
||||
private nextId: number = 18;
|
||||
|
||||
constructor() {
|
||||
// Load initial data from fakedb.json
|
||||
this.images = JSON.parse(JSON.stringify(fakeDb.images));
|
||||
}
|
||||
|
||||
// Helper to simulate network delay
|
||||
private delay(ms: number = 300): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Images
|
||||
async getImages(params?: {
|
||||
search?: string;
|
||||
null_alias?: boolean;
|
||||
limit?: number;
|
||||
page?: number;
|
||||
}): Promise<Image[]> {
|
||||
await this.delay();
|
||||
|
||||
let filtered = [...this.images];
|
||||
|
||||
// Filter by search query (search in aliases)
|
||||
if (params?.search) {
|
||||
const searchLower = params.search.toLowerCase();
|
||||
filtered = filtered.filter(img =>
|
||||
img.aliases.some(alias => alias.toLowerCase().includes(searchLower))
|
||||
);
|
||||
}
|
||||
|
||||
// Filter images without aliases
|
||||
if (params?.null_alias) {
|
||||
filtered = filtered.filter(img => img.aliases.length === 0);
|
||||
}
|
||||
|
||||
// Pagination
|
||||
const page = params?.page || 1;
|
||||
const limit = params?.limit || 20;
|
||||
const start = (page - 1) * limit;
|
||||
const end = start + limit;
|
||||
|
||||
return filtered.slice(start, end);
|
||||
}
|
||||
|
||||
async getImage(id: string): Promise<Image> {
|
||||
await this.delay();
|
||||
|
||||
const image = this.images.find(img => img.id === id);
|
||||
if (!image) {
|
||||
throw new Error('Image not found');
|
||||
}
|
||||
return { ...image };
|
||||
}
|
||||
|
||||
async uploadImage(file: File, aliases: string[]): Promise<Image> {
|
||||
await this.delay(500);
|
||||
|
||||
// Create a new image object
|
||||
const newImage: Image = {
|
||||
id: String(this.nextId++),
|
||||
uploaded_user_id: 'konchin.shih',
|
||||
uploaded_at: new Date().toISOString(),
|
||||
aliases: aliases,
|
||||
url: `/api/images/${this.nextId - 1}/file`,
|
||||
};
|
||||
|
||||
this.images.push(newImage);
|
||||
return { ...newImage };
|
||||
}
|
||||
|
||||
async deleteImage(id: string): Promise<void> {
|
||||
await this.delay();
|
||||
|
||||
const index = this.images.findIndex(img => img.id === id);
|
||||
if (index === -1) {
|
||||
throw new Error('Image not found');
|
||||
}
|
||||
this.images.splice(index, 1);
|
||||
}
|
||||
|
||||
// Aliases
|
||||
async getAllAliases(): Promise<string[]> {
|
||||
await this.delay();
|
||||
|
||||
// Get unique aliases from all images
|
||||
const aliasSet = new Set<string>();
|
||||
this.images.forEach(img => {
|
||||
img.aliases.forEach(alias => aliasSet.add(alias));
|
||||
});
|
||||
return Array.from(aliasSet).sort();
|
||||
}
|
||||
|
||||
async updateImageAliases(id: string, aliases: string[]): Promise<Image> {
|
||||
await this.delay();
|
||||
|
||||
const image = this.images.find(img => img.id === id);
|
||||
if (!image) {
|
||||
throw new Error('Image not found');
|
||||
}
|
||||
image.aliases = [...aliases];
|
||||
return { ...image };
|
||||
}
|
||||
|
||||
async addImageAlias(id: string, alias: string): Promise<Image> {
|
||||
await this.delay();
|
||||
|
||||
const image = this.images.find(img => img.id === id);
|
||||
if (!image) {
|
||||
throw new Error('Image not found');
|
||||
}
|
||||
if (!image.aliases.includes(alias)) {
|
||||
image.aliases.push(alias);
|
||||
}
|
||||
return { ...image };
|
||||
}
|
||||
|
||||
async removeImageAlias(id: string, alias: string): Promise<void> {
|
||||
await this.delay();
|
||||
|
||||
const image = this.images.find(img => img.id === id);
|
||||
if (!image) {
|
||||
throw new Error('Image not found');
|
||||
}
|
||||
image.aliases = image.aliases.filter(a => a !== alias);
|
||||
}
|
||||
|
||||
getImageUrl(id: string): string {
|
||||
// For mock, return a placeholder image URL
|
||||
return `https://picsum.photos/seed/${id}/400/400`;
|
||||
}
|
||||
}
|
||||
|
||||
export const api = new MockApiService();
|
||||
@@ -18,12 +18,10 @@ export default function ImageModal({
|
||||
const [aliases, setAliases] = useState<string[]>([]);
|
||||
const [newAlias, setNewAlias] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [allAliases, setAllAliases] = useState<{id: number, name: string}[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// 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);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface NavbarProps {
|
||||
onUploadClick: () => void;
|
||||
|
||||
Reference in New Issue
Block a user