Added Dockerfile

This commit is contained in:
Penguin-71630
2025-12-13 05:10:30 +08:00
parent 4de4cc4a18
commit f3108b3c80
8 changed files with 87 additions and 144 deletions

15
.dockerignore Normal file
View 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
View File

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

23
Dockerfile Normal file
View 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
View 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
View 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;
}

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import { useState } from 'react';
interface NavbarProps {
onUploadClick: () => void;