From f3108b3c80e7dea59786525b12261cc8f238ba50 Mon Sep 17 00:00:00 2001 From: Penguin-71630 Date: Sat, 13 Dec 2025 05:10:30 +0800 Subject: [PATCH] Added Dockerfile --- .dockerignore | 15 +++ .gitignore | 1 + Dockerfile | 23 +++++ nginx.conf | 24 +++++ webpage/nginx.conf | 24 +++++ webpage/src/api-mock.ts | 141 -------------------------- webpage/src/components/ImageModal.tsx | 2 - webpage/src/components/Navbar.tsx | 1 - 8 files changed, 87 insertions(+), 144 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 nginx.conf create mode 100644 webpage/nginx.conf delete mode 100644 webpage/src/api-mock.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..41d407b --- /dev/null +++ b/.dockerignore @@ -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/ diff --git a/.gitignore b/.gitignore index c1a2613..af8190d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store tests/* +frontend-web ################################ # Node.js/React (web/) 專屬 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ce90931 --- /dev/null +++ b/Dockerfile @@ -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;"] diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..674fe8c --- /dev/null +++ b/nginx.conf @@ -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; +} diff --git a/webpage/nginx.conf b/webpage/nginx.conf new file mode 100644 index 0000000..674fe8c --- /dev/null +++ b/webpage/nginx.conf @@ -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; +} diff --git a/webpage/src/api-mock.ts b/webpage/src/api-mock.ts deleted file mode 100644 index 67e0b93..0000000 --- a/webpage/src/api-mock.ts +++ /dev/null @@ -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 { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - // Images - async getImages(params?: { - search?: string; - null_alias?: boolean; - limit?: number; - page?: number; - }): Promise { - 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 { - 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 { - 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 { - 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 { - await this.delay(); - - // Get unique aliases from all images - const aliasSet = new Set(); - this.images.forEach(img => { - img.aliases.forEach(alias => aliasSet.add(alias)); - }); - return Array.from(aliasSet).sort(); - } - - async updateImageAliases(id: string, aliases: string[]): Promise { - 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 { - 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 { - 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(); \ No newline at end of file diff --git a/webpage/src/components/ImageModal.tsx b/webpage/src/components/ImageModal.tsx index 7e806a8..34d4dc5 100644 --- a/webpage/src/components/ImageModal.tsx +++ b/webpage/src/components/ImageModal.tsx @@ -18,12 +18,10 @@ export default function ImageModal({ const [aliases, setAliases] = useState([]); 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); diff --git a/webpage/src/components/Navbar.tsx b/webpage/src/components/Navbar.tsx index 6e05c4a..75eb4d3 100644 --- a/webpage/src/components/Navbar.tsx +++ b/webpage/src/components/Navbar.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; interface NavbarProps { onUploadClick: () => void;