diff --git a/docker-compose.yml b/docker-compose.yml index 1ff8b79..d4a314c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,4 @@ services: - # --- Backend Service --- backend: build: context: ./backend @@ -18,26 +17,23 @@ services: labels: - "traefik.enable=true" - "traefik.docker.network=narl" - # Route /api, /admin, /static and /media traffic to the backend - "traefik.http.routers.holy-backend.priority=20" - "traefik.http.routers.holy-backend.rule=Host(`holy.narl.io`) && (PathPrefix(`/api`) || PathPrefix(`/admin`) || PathPrefix(`/static`) || PathPrefix(`/media`))" - "traefik.http.routers.holy-backend.entrypoints=websecure" - "traefik.http.routers.holy-backend.tls.certresolver=https_resolver" - "traefik.http.services.holy-backend.loadbalancer.server.port=8000" - # --- Frontend Service --- frontend: build: context: ./frontend + args: + # Bake the API URL into the production build + - VITE_API_URL=https://holy.narl.io/api/ container_name: holy-frontend restart: unless-stopped networks: - narl - env_file: - - ./.env - volumes: - - ./frontend:/app - - /app/node_modules + # In production, we don't mount volumes for code to ensure container integrity depends_on: - backend labels: @@ -47,7 +43,7 @@ services: - "traefik.http.routers.holy-frontend.rule=Host(`holy.narl.io`)" - "traefik.http.routers.holy-frontend.entrypoints=websecure" - "traefik.http.routers.holy-frontend.tls.certresolver=https_resolver" - - "traefik.http.services.holy-frontend.loadbalancer.server.port=5173" + - "traefik.http.services.holy-frontend.loadbalancer.server.port=80" networks: narl: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 9899f66..acbe73a 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,20 +1,30 @@ -# Use an official Node runtime as a parent image -FROM node:lts-alpine - -# Set the working directory in the container +# --- Build Stage --- +FROM node:lts-alpine AS build-stage WORKDIR /app - -# Copy package.json and package-lock.json COPY package*.json ./ - -# Install dependencies RUN npm install - -# Copy the rest of the frontend code COPY . . -# Expose the port the app runs on -EXPOSE 5173 +# Pass VITE environment variables during build +ARG VITE_API_URL +ENV VITE_API_URL=${VITE_API_URL} -# Start the development server -CMD ["npm", "run", "dev", "--", "--host"] +RUN npm run build + +# --- Production Stage --- +FROM nginx:stable-alpine +# Copy built files +COPY --from=build-stage /app/dist /usr/share/nginx/html + +# Custom Nginx config to handle React Router (SPA) +RUN printf 'server {\n\ + listen 80;\n\ + location / {\n\ + root /usr/share/nginx/html;\n\ + index index.html index.htm;\n\ + try_files $uri $uri/ /index.html;\n\ + }\n\ +}' > /etc/nginx/conf.d/default.conf + +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a4e44b1..e0d2976 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -32,7 +32,7 @@ import ExpandMore from '@mui/icons-material/ExpandMore'; import { getTheme, type CatppuccinTheme } from './theme'; import api from './api'; -// Code splitting for routes +// Lazy load components for production performance const CategoryList = lazy(() => import('./pages/CategoryList')); const CategoryFlavors = lazy(() => import('./pages/categories/CategoryFlavors')); const FlavorDetail = lazy(() => import('./pages/flavors/FlavorDetail')); @@ -58,7 +58,7 @@ const GlobalSearch = () => { const fetchFlavors = async () => { try { const res = await api.get('flavors/'); - setOptions(res.data); + setOptions(Array.isArray(res.data) ? res.data : (res.data.results || [])); } catch (err) { console.error('Failed to fetch search options'); } @@ -104,7 +104,7 @@ const GlobalSearch = () => { { useEffect(() => { const fetchData = async () => { try { + const token = localStorage.getItem('token'); const [userRes, catRes] = await Promise.all([ - localStorage.getItem('token') ? api.get('users/me/') : Promise.resolve({ data: null }), + token ? api.get('users/me/') : Promise.resolve({ data: null }), api.get('categories/') ]); - if (userRes.data) { + if (userRes.data && userRes.data.username) { setUser(userRes.data); if (userRes.data.theme) setThemeName(userRes.data.theme); + } else { + setUser(null); } - setCategories(catRes.data); + + const catData = Array.isArray(catRes.data) ? catRes.data : (catRes.data.results || []); + setCategories(catData); } catch (err) { setUser(null); } finally { @@ -199,7 +204,7 @@ const App: React.FC = () => { - {user && !user.avatar && user.username.charAt(0).toUpperCase()} + {user?.username && !user.avatar && user.username.charAt(0).toUpperCase()} setDrawerOpen(false)} style={{ color: 'inherit', textDecoration: 'none' }}>Holy Flavors @@ -277,7 +282,6 @@ const App: React.FC = () => { zIndex: (theme) => theme.zIndex.drawer + 1 }}> - {/* Brand Logo - Responsive */} Holy Flavors @@ -285,7 +289,6 @@ const App: React.FC = () => { - {/* Categories Dropdown - Desktop Only */} } /> +