Working react with docker deployment
This commit is contained in:
parent
1908b2aa70
commit
e3b83786c5
5
backend/.dockerignore
Normal file
5
backend/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
*.log
|
||||||
12
backend/Dockerfile
Normal file
12
backend/Dockerfile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
24
backend/package-lock.json
generated
24
backend/package-lock.json
generated
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "Costco-Grocery-List",
|
"name": "backend",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "Costco-Grocery-List",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"pg": "^8.16.0"
|
"pg": "^8.16.0"
|
||||||
@ -904,6 +904,18 @@
|
|||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/cpx": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz",
|
||||||
@ -2144,6 +2156,14 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/object-copy": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"pg": "^8.16.0"
|
"pg": "^8.16.0"
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
const { Pool } = require('pg');
|
const { Pool } = require('pg');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = 5001;
|
const port = 5000;
|
||||||
|
|
||||||
const pool = new Pool({
|
const pool = new Pool({
|
||||||
user: process.env.DB_USER,
|
user: process.env.DB_USER,
|
||||||
@ -14,10 +16,22 @@ const pool = new Pool({
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.use(express.json());
|
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 { query } = req.query;
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
'SELECT DISTINCT item_name FROM grocery_list WHERE item_name ILIKE $1 LIMIT 10',
|
'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 { item_name, quantity } = req.body;
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
'SELECT id, bought FROM grocery_list WHERE item_name = $1',
|
'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;
|
const { id } = req.body;
|
||||||
await pool.query('UPDATE grocery_list SET bought = TRUE WHERE id = $1', [id]);
|
await pool.query('UPDATE grocery_list SET bought = TRUE WHERE id = $1', [id]);
|
||||||
res.json({ message: 'Item marked as bought.' });
|
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');
|
const { rows } = await pool.query('SELECT * FROM grocery_list WHERE bought = FALSE');
|
||||||
res.json(rows);
|
res.json(rows);
|
||||||
});
|
});
|
||||||
|
|||||||
21
docker-compose.dev.yml
Normal file
21
docker-compose.dev.yml
Normal file
@ -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
|
||||||
16
docker-compose.prod.yml
Normal file
16
docker-compose.prod.yml
Normal file
@ -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
|
||||||
5
frontend/.dockerignore
Normal file
5
frontend/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
.git
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
*.log
|
||||||
15
frontend/Dockerfile
Normal file
15
frontend/Dockerfile
Normal file
@ -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;"]
|
||||||
16
frontend/Dockerfile.dev
Normal file
16
frontend/Dockerfile.dev
Normal file
@ -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"]
|
||||||
20
frontend/package-lock.json
generated
20
frontend/package-lock.json
generated
@ -14,7 +14,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.10.0",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@vitejs/plugin-react": "^5.1.0",
|
"@vitejs/plugin-react": "^5.1.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
@ -1766,9 +1766,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001754",
|
"version": "1.0.30001755",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz",
|
||||||
"integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
|
"integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@ -1846,9 +1846,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.1.tgz",
|
||||||
"integrity": "sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg==",
|
"integrity": "sha512-98XGutrXoh75MlgLihlNxAGbUuFQc7l1cqcnEZlLNKc0UrVdPndgmaDmYTDDh929VS/eqTZV0rozmhu2qqT1/g==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
@ -1875,9 +1875,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.253",
|
"version": "1.5.254",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.253.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz",
|
||||||
"integrity": "sha512-O0tpQ/35rrgdiGQ0/OFWhy1itmd9A6TY9uQzlqj3hKSu/aYpe7UIn5d7CU2N9myH6biZiWF3VMZVuup8pw5U9w==",
|
"integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
"@types/node": "^24.10.0",
|
"@types/node": "^24.10.0",
|
||||||
"@types/react": "^19.2.2",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.2",
|
"@types/react-dom": "^19.2.2",
|
||||||
"@vitejs/plugin-react": "^5.1.0",
|
"@vitejs/plugin-react": "^5.1.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
|
|||||||
@ -40,3 +40,5 @@
|
|||||||
.read-the-docs {
|
.read-the-docs {
|
||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import SuggestionList from "./components/SuggestionList";
|
|||||||
import GroceryItem from "./components/GroceryItem";
|
import GroceryItem from "./components/GroceryItem";
|
||||||
import type { GroceryItemType } from "./types";
|
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() {
|
export default function App() {
|
||||||
const [items, setItems] = useState<GroceryItemType[]>([]);
|
const [items, setItems] = useState<GroceryItemType[]>([]);
|
||||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||||
@ -11,7 +14,7 @@ export default function App() {
|
|||||||
|
|
||||||
// Load grocery list
|
// Load grocery list
|
||||||
const loadList = async () => {
|
const loadList = async () => {
|
||||||
const res = await fetch("/api/list");
|
const res = await fetch(`${API_BASE_URL}/list`);
|
||||||
const data: GroceryItemType[] = await res.json();
|
const data: GroceryItemType[] = await res.json();
|
||||||
setItems(data);
|
setItems(data);
|
||||||
};
|
};
|
||||||
@ -26,7 +29,7 @@ export default function App() {
|
|||||||
|
|
||||||
if (!query.trim()) return setSuggestions([]);
|
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();
|
const data: string[] = await res.json();
|
||||||
setSuggestions(data);
|
setSuggestions(data);
|
||||||
};
|
};
|
||||||
@ -42,7 +45,7 @@ export default function App() {
|
|||||||
if (existing) {
|
if (existing) {
|
||||||
if (!confirm("Item already exists. Add more?")) return;
|
if (!confirm("Item already exists. Add more?")) return;
|
||||||
|
|
||||||
await fetch("/api/add", {
|
await fetch(`${API_BASE_URL}/add`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@ -51,7 +54,7 @@ export default function App() {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await fetch("/api/add", {
|
await fetch(`${API_BASE_URL}/add`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ item_name: itemInput, quantity }),
|
body: JSON.stringify({ item_name: itemInput, quantity }),
|
||||||
@ -68,7 +71,7 @@ export default function App() {
|
|||||||
const markBought = async (id: number) => {
|
const markBought = async (id: number) => {
|
||||||
if (!confirm("Mark this item as bought?")) return;
|
if (!confirm("Mark this item as bought?")) return;
|
||||||
|
|
||||||
await fetch("/api/mark-bought", {
|
await fetch(`${API_BASE_URL}/mark-bought`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ id }),
|
body: JSON.stringify({ id }),
|
||||||
|
|||||||
1
frontend/src/config.ts
Normal file
1
frontend/src/config.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const API_BASE_URL = import.meta.env.VITE_API_URL || "/api";
|
||||||
@ -4,9 +4,14 @@ import react from '@vitejs/plugin-react'
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
|
watch: {
|
||||||
|
usePolling: true,
|
||||||
|
},
|
||||||
|
host: true,
|
||||||
|
strictPort: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:5001',
|
target: 'http://localhost:5000',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
|
|||||||
153
index.html
153
index.html
@ -1,153 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Costco Grocery List</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 1em;
|
|
||||||
background: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 480px;
|
|
||||||
margin: auto;
|
|
||||||
background: white;
|
|
||||||
padding: 1em;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
input,
|
|
||||||
button,
|
|
||||||
select {
|
|
||||||
font-size: 1em;
|
|
||||||
margin: 0.3em 0;
|
|
||||||
padding: 0.5em;
|
|
||||||
width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
padding: 0.5em;
|
|
||||||
background: #e9ecef;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
li:hover {
|
|
||||||
background: #dee2e6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Costco Grocery List</h1>
|
|
||||||
<input type="text" id="itemInput" placeholder="Item name" list="suggestions" autocomplete="off">
|
|
||||||
<!-- <ul id="suggestionList" style="display:none; background:#fff; border:1px solid #ccc; max-height:150px; overflow-y:auto; position:absolute; z-index:1000; width:calc(100% - 2em);"></ul> -->
|
|
||||||
<ul id="suggestionList"
|
|
||||||
style="display:none; background:#fff; border:1px solid #ccc; max-height:150px; overflow-y:auto; position:absolute; z-index:1000; left:1em; right:1em;">
|
|
||||||
</ul>
|
|
||||||
<input type="number" id="quantityInput" placeholder="Quantity" value="1" min="1">
|
|
||||||
<button onclick="addItem()">Add Item</button>
|
|
||||||
<ul id="groceryList"></ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
async function loadList() {
|
|
||||||
const res = await fetch('/list');
|
|
||||||
const list = await res.json();
|
|
||||||
const ul = document.getElementById('groceryList');
|
|
||||||
ul.innerHTML = '';
|
|
||||||
list.forEach(item => {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.textContent = `${item.item_name} (${item.quantity})`;
|
|
||||||
li.onclick = () => markBought(item.id);
|
|
||||||
ul.appendChild(li);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function suggest(query) {
|
|
||||||
if (!query.trim()) return document.getElementById('suggestionList').style.display = 'none';
|
|
||||||
const res = await fetch('/suggest?query=' + encodeURIComponent(query));
|
|
||||||
const items = await res.json();
|
|
||||||
const suggestionBox = document.getElementById('suggestionList');
|
|
||||||
suggestionBox.innerHTML = '';
|
|
||||||
items.forEach(i => {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.textContent = i;
|
|
||||||
li.style.padding = '0.5em';
|
|
||||||
li.onclick = () => {
|
|
||||||
document.getElementById('itemInput').value = i;
|
|
||||||
suggestionBox.style.display = 'none';
|
|
||||||
};
|
|
||||||
suggestionBox.appendChild(li);
|
|
||||||
});
|
|
||||||
suggestionBox.style.display = items.length ? 'block' : 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('itemInput').addEventListener('input', (e) => suggest(e.target.value));
|
|
||||||
|
|
||||||
async function addItem() {
|
|
||||||
const item = document.getElementById('itemInput').value.trim();
|
|
||||||
const quantity = parseInt(document.getElementById('quantityInput').value, 10);
|
|
||||||
if (!item || isNaN(quantity)) return;
|
|
||||||
|
|
||||||
const check = await fetch('/list');
|
|
||||||
const existing = await check.json();
|
|
||||||
const match = existing.find(i => i.item_name.toLowerCase() === item.toLowerCase());
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const confirmAdd = confirm("Item already exists. Add more?");
|
|
||||||
if (!confirmAdd) return;
|
|
||||||
await fetch('/add', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ item_name: item, quantity: match.quantity + quantity })
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await fetch('/add', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ item_name: item, quantity })
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById('itemInput').value = '';
|
|
||||||
document.getElementById('quantityInput').value = 1;
|
|
||||||
document.getElementById('suggestionList').style.display = 'none';
|
|
||||||
loadList();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function markBought(id) {
|
|
||||||
const confirmBuy = confirm("Mark this item as bought?");
|
|
||||||
if (!confirmBuy) return;
|
|
||||||
await fetch('/mark-bought', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ id })
|
|
||||||
});
|
|
||||||
loadList();
|
|
||||||
}
|
|
||||||
|
|
||||||
loadList();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
Loading…
Reference in New Issue
Block a user