diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..3edde31 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.git +dist +build +*.log diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..4cee512 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +EXPOSE 5000 + +CMD ["node", "server.js"] diff --git a/backend/package-lock.json b/backend/package-lock.json index 9d38eab..729a8ef 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "Costco-Grocery-List", + "name": "backend", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "Costco-Grocery-List", "dependencies": { + "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", "pg": "^8.16.0" @@ -904,6 +904,18 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "dev": true }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cpx": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz", @@ -2144,6 +2156,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index 51ffa63..bf3ce45 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,5 +1,6 @@ { "dependencies": { + "cors": "^2.8.5", "dotenv": "^17.2.3", "express": "^5.1.0", "pg": "^8.16.0" diff --git a/backend/server.js b/backend/server.js index e26997b..eeac8c2 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,9 +1,11 @@ require('dotenv').config(); -const express = require('express'); +const express = require('express'); +const cors = require('cors'); const { Pool } = require('pg'); + const app = express(); -const port = 5001; +const port = 5000; const pool = new Pool({ user: process.env.DB_USER, @@ -14,10 +16,22 @@ const pool = new Pool({ }); app.use(express.json()); -// app.use(express.static('public')); +app.use(cors({ + origin: "http://localhost:3000", + methods: ["GET", "POST"], +})); + +app.get('/', async (req, res) => { + const { query } = req.query; + const { rows } = await pool.query( + 'SELECT DISTINCT item_name FROM grocery_list WHERE item_name ILIKE $1 LIMIT 10', + [`%${query}%`] + ); + res.status(200).send('Grocery List API is running.'); +}); -app.get('/api/suggest', async (req, res) => { +app.get('/suggest', async (req, res) => { const { query } = req.query; const { rows } = await pool.query( 'SELECT DISTINCT item_name FROM grocery_list WHERE item_name ILIKE $1 LIMIT 10', @@ -27,7 +41,7 @@ app.get('/api/suggest', async (req, res) => { }); -app.post('/api/add', async (req, res) => { +app.post('/add', async (req, res) => { const { item_name, quantity } = req.body; const result = await pool.query( 'SELECT id, bought FROM grocery_list WHERE item_name = $1', @@ -58,14 +72,14 @@ app.post('/api/add', async (req, res) => { }); -app.post('/api/mark-bought', async (req, res) => { +app.post('/mark-bought', async (req, res) => { const { id } = req.body; await pool.query('UPDATE grocery_list SET bought = TRUE WHERE id = $1', [id]); res.json({ message: 'Item marked as bought.' }); }); -app.get('/api/list', async (req, res) => { +app.get('/list', async (req, res) => { const { rows } = await pool.query('SELECT * FROM grocery_list WHERE bought = FALSE'); res.json(rows); }); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..38e40f8 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,21 @@ +services: + frontend: + build: + context: ./frontend + dockerfile: Dockerfile.dev + environment: + - NODE_ENV=development + volumes: + - ./frontend:/app + - /app/node_modules + ports: + - "3000:5173" + depends_on: + - backend + + backend: + build: ./backend + ports: + - "5000:5000" + env_file: + - ./backend/.env \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..c37f1c7 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,16 @@ +services: + frontend: + build: ./frontend + environment: + - MODE_ENV=production + ports: + - "3000:80" + depends_on: + - backend + + backend: + build: ./backend + ports: + - "5000:5000" + env_file: + - ./backend/.env \ No newline at end of file diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..3edde31 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.git +dist +build +*.log diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..5a17e8e --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,15 @@ +FROM node:20-alpine AS build + +WORKDIR /app +COPY package*.json ./ +RUN npm install + +COPY . . +RUN npm run build + +FROM nginx:alpine +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev new file mode 100644 index 0000000..6bb1779 --- /dev/null +++ b/frontend/Dockerfile.dev @@ -0,0 +1,16 @@ +# FROM node:20-alpine +FROM node:20-slim +# FROM node:20 + + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . + +ENV CHOKIDAR_USEPOLLING=true + +EXPOSE 5173 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 876c7bf..a1bacff 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,7 +14,7 @@ "devDependencies": { "@eslint/js": "^9.39.1", "@types/node": "^24.10.0", - "@types/react": "^19.2.2", + "@types/react": "^19.2.5", "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^5.1.0", "eslint": "^9.39.1", @@ -1766,9 +1766,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001754", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", - "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "version": "1.0.30001755", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", + "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", "dev": true, "funding": [ { @@ -1846,9 +1846,9 @@ } }, "node_modules/csstype": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.0.tgz", - "integrity": "sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.1.tgz", + "integrity": "sha512-98XGutrXoh75MlgLihlNxAGbUuFQc7l1cqcnEZlLNKc0UrVdPndgmaDmYTDDh929VS/eqTZV0rozmhu2qqT1/g==", "dev": true }, "node_modules/debug": { @@ -1875,9 +1875,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.5.253", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.253.tgz", - "integrity": "sha512-O0tpQ/35rrgdiGQ0/OFWhy1itmd9A6TY9uQzlqj3hKSu/aYpe7UIn5d7CU2N9myH6biZiWF3VMZVuup8pw5U9w==", + "version": "1.5.254", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", + "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==", "dev": true }, "node_modules/esbuild": { diff --git a/frontend/package.json b/frontend/package.json index 2f9c85b..fe51ca6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@eslint/js": "^9.39.1", "@types/node": "^24.10.0", - "@types/react": "^19.2.2", + "@types/react": "^19.2.5", "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^5.1.0", "eslint": "^9.39.1", diff --git a/frontend/src/App.css b/frontend/src/App.css index b9d355d..99a803c 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -40,3 +40,5 @@ .read-the-docs { color: #888; } + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 22e6d6d..a376c75 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,6 +3,9 @@ import SuggestionList from "./components/SuggestionList"; import GroceryItem from "./components/GroceryItem"; import type { GroceryItemType } from "./types"; +const API_BASE_URL = import.meta.env.VITE_API_URL; +console.log("API_BASE_URL:", API_BASE_URL); + export default function App() { const [items, setItems] = useState([]); const [suggestions, setSuggestions] = useState([]); @@ -11,7 +14,7 @@ export default function App() { // Load grocery list const loadList = async () => { - const res = await fetch("/api/list"); + const res = await fetch(`${API_BASE_URL}/list`); const data: GroceryItemType[] = await res.json(); setItems(data); }; @@ -26,7 +29,7 @@ export default function App() { if (!query.trim()) return setSuggestions([]); - const res = await fetch("/api/suggest?query=" + encodeURIComponent(query)); + const res = await fetch(`${API_BASE_URL}/suggest?query=${encodeURIComponent(query)}`); const data: string[] = await res.json(); setSuggestions(data); }; @@ -42,7 +45,7 @@ export default function App() { if (existing) { if (!confirm("Item already exists. Add more?")) return; - await fetch("/api/add", { + await fetch(`${API_BASE_URL}/add`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -51,7 +54,7 @@ export default function App() { }), }); } else { - await fetch("/api/add", { + await fetch(`${API_BASE_URL}/add`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ item_name: itemInput, quantity }), @@ -68,7 +71,7 @@ export default function App() { const markBought = async (id: number) => { if (!confirm("Mark this item as bought?")) return; - await fetch("/api/mark-bought", { + await fetch(`${API_BASE_URL}/mark-bought`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id }), diff --git a/frontend/src/config.ts b/frontend/src/config.ts new file mode 100644 index 0000000..deb7f5b --- /dev/null +++ b/frontend/src/config.ts @@ -0,0 +1 @@ +export const API_BASE_URL = import.meta.env.VITE_API_URL || "/api"; \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 9b193b9..49fe66e 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,9 +4,14 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { + watch: { + usePolling: true, + }, + host: true, + strictPort: true, proxy: { '/api': { - target: 'http://localhost:5001', + target: 'http://localhost:5000', changeOrigin: true, secure: false, }, diff --git a/index.html b/index.html deleted file mode 100644 index 7ff0109..0000000 --- a/index.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - Costco Grocery List - - - - -
-

Costco Grocery List

- - - - - -
    -
    - - - - - \ No newline at end of file