initial commit
This commit is contained in:
commit
f5c86e2fff
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
# Environment variables (DO NOT COMMIT)
|
||||
.env
|
||||
|
||||
# Node dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output (if using a bundler or React later)
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
15
build.js
Normal file
15
build.js
Normal file
@ -0,0 +1,15 @@
|
||||
// build.js
|
||||
const esbuild = require('esbuild');
|
||||
const fs = require('fs');
|
||||
|
||||
// Ensure dist folder exists
|
||||
if (!fs.existsSync('dist')) fs.mkdirSync('dist', { recursive: true });
|
||||
|
||||
esbuild.build({
|
||||
entryPoints: ['server.js'],
|
||||
outfile: 'dist/server.js',
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
minify: false,
|
||||
}).catch(() => process.exit(1));
|
||||
3903
package-lock.json
generated
Normal file
3903
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
package.json
Normal file
16
package.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.1.0",
|
||||
"pg": "^8.16.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cpx": "^1.5.0",
|
||||
"esbuild": "^0.25.5",
|
||||
"rimraf": "^6.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf dist && node build.js && cpx \"public/**/*\" dist/public",
|
||||
"dev": "node server.js"
|
||||
}
|
||||
}
|
||||
153
public/index.html
Normal file
153
public/index.html
Normal file
@ -0,0 +1,153 @@
|
||||
<!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>
|
||||
73
server.js
Normal file
73
server.js
Normal file
@ -0,0 +1,73 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const { Pool } = require('pg');
|
||||
const app = express();
|
||||
const port = 5001;
|
||||
|
||||
const pool = new Pool({
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
host: process.env.DB_HOST,
|
||||
database: process.env.DB_NAME,
|
||||
port: 5432,
|
||||
});
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.static('public'));
|
||||
|
||||
|
||||
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',
|
||||
[`%${query}%`]
|
||||
);
|
||||
res.json(rows.map(r => r.item_name));
|
||||
});
|
||||
|
||||
|
||||
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',
|
||||
[item_name]
|
||||
);
|
||||
|
||||
let listItemId;
|
||||
if (result.rowCount > 0) {
|
||||
listItemId = result.rows[0].id;
|
||||
await pool.query(
|
||||
'UPDATE grocery_list SET quantity = $1, bought = FALSE WHERE id = $2',
|
||||
[quantity, listItemId]
|
||||
);
|
||||
res.json({ message: 'Item re-added with updated quantity.' });
|
||||
} else {
|
||||
const insertResult = await pool.query(
|
||||
'INSERT INTO grocery_list (item_name, quantity) VALUES ($1, $2) RETURNING id',
|
||||
[item_name, quantity]
|
||||
);
|
||||
listItemId = insertResult.rows[0].id;
|
||||
res.json({ message: 'Item added to list.' });
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO grocery_history (list_item_id, quantity, added_on) VALUES ($1, $2, NOW())',
|
||||
[listItemId, quantity]
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
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('/list', async (req, res) => {
|
||||
const { rows } = await pool.query('SELECT * FROM grocery_list WHERE bought = FALSE');
|
||||
res.json(rows);
|
||||
});
|
||||
|
||||
app.listen(port, () => console.log(`Listening at http://localhost:${port}`));
|
||||
Loading…
Reference in New Issue
Block a user