diff --git a/.gitignore b/.gitignore
index 40b878d..9909d06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-node_modules/
\ No newline at end of file
+node_modules/
+positions.db
\ No newline at end of file
diff --git a/categories.json b/categories.json
new file mode 100644
index 0000000..c79cd74
--- /dev/null
+++ b/categories.json
@@ -0,0 +1,41 @@
+[
+ {
+ "name": "Ted's Team",
+ "fruits": [
+ "Brandon Brunson",
+ "Eric Smithson",
+ "John Hammer",
+ "Seth Lima",
+ "Rick Sanchez"
+ ]
+ },
+ {
+ "name": "Ariel's Team",
+ "fruits": [
+ "Jerry Smith",
+ "Charles Carmichael",
+ "Michael Westen",
+ "Shawn Spencer",
+ "Eliot Alderson",
+ "Brian D"
+ ]
+ },
+ {
+ "name": "Elsa's Team",
+ "fruits": [
+ "John Dorian",
+ "Harvey Spectre",
+ "Juliet O'Hara",
+ "Fiona Glenanne"
+ ]
+ },
+ {
+ "name": "Ana's Team",
+ "fruits": [
+ "Neal Caffrey",
+ "Chuck Bartowski",
+ "Gus Burton",
+ "Mike Ross"
+ ]
+ }
+]
\ No newline at end of file
diff --git a/positions.db b/positions.db
index bb43a70..7a9d483 100644
Binary files a/positions.db and b/positions.db differ
diff --git a/public/index.html b/public/index.html
index abb5a8f..3020a4f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -26,59 +26,8 @@
-
@@ -110,6 +59,31 @@
});
mapEl.appendChild(rowEl);
});
+
+ // Dynamically load categories and fruits
+ async function loadCategories() {
+ const sidebar = document.getElementById('sidebar');
+ const res = await fetch('/api/categories');
+ const categories = await res.json();
+ sidebar.innerHTML = '';
+ categories.forEach(cat => {
+ const catDiv = document.createElement('div');
+ catDiv.className = 'categories';
+ catDiv.innerHTML = `
+
+
+ ${cat.fruits.map(fruit =>
+ `
${fruit}
`
+ ).join('')}
+
+ `;
+ sidebar.appendChild(catDiv);
+ });
+ }
+ loadCategories();
diff --git a/public/script.js b/public/script.js
index a2f9eb9..43bce58 100644
--- a/public/script.js
+++ b/public/script.js
@@ -12,7 +12,8 @@ function createDragImage(el) {
// helper to place a fruit div inside a square div
function placeFruitInSquare(squareEl, fruitName) {
- // clear existing content
+ // clear existing content except the square number
+ const squareNumber = squareEl.dataset.squareId;
squareEl.innerHTML = '';
// record the fruit in the square for tracking
squareEl.dataset.fruit = fruitName;
@@ -45,6 +46,11 @@ function placeFruitInSquare(squareEl, fruitName) {
e.stopPropagation(); // prevent event bubbling
squareEl.innerHTML = '';
delete squareEl.dataset.fruit;
+ // re-add the square number after clearing
+ const numDiv = document.createElement('div');
+ numDiv.className = 'square-number';
+ numDiv.textContent = squareNumber;
+ squareEl.appendChild(numDiv);
const squareId = squareEl.dataset.squareId;
await fetch('/api/positions', {
method: 'POST',
@@ -53,88 +59,286 @@ function placeFruitInSquare(squareEl, fruitName) {
});
});
squareEl.appendChild(clearBtn);
+
+ // add the square number in the bottom right
+ const numDiv = document.createElement('div');
+ numDiv.className = 'square-number';
+ numDiv.textContent = squareNumber;
+ squareEl.appendChild(numDiv);
}
-// initialize drag & drop on fruits
-document.querySelectorAll('.fruit').forEach(el => {
- el.addEventListener('dragstart', e => {
- e.dataTransfer.setData('text/plain', el.dataset.fruit);
- const dragImg = createDragImage(el);
- e.dataTransfer.setDragImage(dragImg, dragImg.offsetWidth / 2, dragImg.offsetHeight / 2);
- // cleanup after drag
- el.addEventListener('dragend', () => {
- dragImg.remove();
- }, { once: true });
- });
-
- // updated mouseover event
- el.addEventListener('mouseover', e => {
- const parentCategory = el.closest('.categories');
- if (parentCategory) {
- // Unhighlight all squares for fruits in this category first
- parentCategory.querySelectorAll('.fruit').forEach(sib => {
- const sq = document.querySelector(`.square[data-fruit="${sib.dataset.fruit}"]`);
- if (sq) sq.classList.remove('highlight');
- });
- }
- // Now highlight only the hovered fruit's square
- const square = document.querySelector(`.square[data-fruit="${el.dataset.fruit}"]`);
- if (square) square.classList.add('highlight');
- updateHighlightedBorders();
- });
-
- // updated mouseout event
- el.addEventListener('mouseout', e => {
- // Remove highlight from the hovered fruit's square
- const square = document.querySelector(`.square[data-fruit="${el.dataset.fruit}"]`);
- if (square) square.classList.remove('highlight');
- // If still inside the category container, restore highlighting to all fruits there
- const parentCategory = el.closest('.categories');
- if (parentCategory && parentCategory.contains(e.relatedTarget)) {
- parentCategory.querySelectorAll('.fruit').forEach(sib => {
- const sq = document.querySelector(`.square[data-fruit="${sib.dataset.fruit}"]`);
- if (sq) sq.classList.add('highlight');
- });
- }
- updateHighlightedBorders();
- });
-});
+// Helper to show an input box for custom fruit name
+function showCustomFruitInput(squareEl) {
+ // Prevent multiple inputs
+ if (squareEl.querySelector('.custom-fruit-input')) return;
-// make each square a drop target
-document.querySelectorAll('.square').forEach(sq => {
- sq.addEventListener('dragover', e => e.preventDefault());
- sq.addEventListener('drop', async e => {
- e.preventDefault();
- const fruit = e.dataTransfer.getData('text/plain');
+ const squareNumber = squareEl.dataset.squareId;
+ squareEl.innerHTML = '';
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.placeholder = 'Enter name...';
+ input.className = 'custom-fruit-input';
+ input.style.width = '90%';
+ input.style.fontSize = '1em';
+ input.style.textAlign = 'center';
+ input.style.marginTop = '30px';
+ squareEl.appendChild(input);
- // check if the fruit is already placed in another square
- const existingSquare = document.querySelector(`.square[data-fruit="${fruit}"]`);
- if (existingSquare && existingSquare !== sq) {
- existingSquare.innerHTML = '';
- delete existingSquare.dataset.fruit;
- const existingSquareId = existingSquare.dataset.squareId;
+ // Add the square number in the bottom right
+ const numDiv = document.createElement('div');
+ numDiv.className = 'square-number';
+ numDiv.textContent = squareNumber;
+ squareEl.appendChild(numDiv);
+
+ input.focus();
+
+ // Handler for when user presses Enter
+ input.addEventListener('keydown', function (e) {
+ if (e.key === 'Enter') {
+ input.blur();
+ }
+ });
+
+ input.addEventListener('blur', function () {
+ const customName = input.value.trim();
+ if (!customName) {
+ // Restore square if nothing entered
+ squareEl.innerHTML = '';
+ squareEl.appendChild(numDiv);
+ return;
+ }
+ // Wait for category selection
+ waitForCategorySelection(customName, squareEl, squareNumber);
+ });
+}
+
+// Wait for user to click a category, then send to server
+function waitForCategorySelection(customName, squareEl, squareNumber) {
+ // Highlight categories and show a message
+ document.querySelectorAll('.categories').forEach(cat => {
+ cat.classList.add('category-selectable');
+ });
+ const msg = document.createElement('div');
+ msg.textContent = 'Click a category to add "' + customName + '"';
+ msg.className = 'category-select-msg';
+ msg.style.position = 'absolute';
+ msg.style.top = '40%';
+ msg.style.left = '50%';
+ msg.style.transform = 'translate(-50%, -50%)';
+ msg.style.background = 'rgba(0,0,0,0.8)';
+ msg.style.color = 'white';
+ msg.style.padding = '8px 16px';
+ msg.style.borderRadius = '8px';
+ msg.style.zIndex = '1000';
+ msg.style.fontSize = '1.1em';
+ document.body.appendChild(msg);
+
+ // Handler for category click
+ function onCategoryClick(e) {
+ e.stopPropagation();
+ document.querySelectorAll('.categories').forEach(cat => {
+ cat.classList.remove('category-selectable');
+ cat.removeEventListener('click', onCategoryClick, true);
+ });
+ document.body.removeChild(msg);
+
+ // Find category name
+ const catHeader = e.currentTarget.querySelector('.category-header h4');
+ const categoryName = catHeader ? catHeader.textContent : null;
+ if (!categoryName) return;
+
+ // Send to server to add fruit to category
+ fetch('/api/add-fruit', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ category: categoryName, fruit: customName })
+ })
+ .then(res => res.json())
+ .then(async data => {
+ if (data.success) {
+ // Reload categories in sidebar
+ if (window.loadCategories) await window.loadCategories();
+ initializeFruitAndCategoryEvents();
+ // Place the fruit in the square
+ placeFruitInSquare(squareEl, customName);
+ // Save the new fruit's position to the database
+ const squareId = squareEl.dataset.squareId;
+ await fetch('/api/positions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ squareId, fruit: customName }),
+ });
+ } else {
+ alert('Could not add fruit: ' + (data.error || 'Unknown error'));
+ // Restore square
+ squareEl.innerHTML = '';
+ const numDiv = document.createElement('div');
+ numDiv.className = 'square-number';
+ numDiv.textContent = squareNumber;
+ squareEl.appendChild(numDiv);
+ }
+ });
+ }
+
+ // Listen for click on any category (use capture to get before toggle)
+ document.querySelectorAll('.categories').forEach(cat => {
+ cat.addEventListener('click', onCategoryClick, true);
+ });
+}
+
+// Wrap all fruit/category event logic in a function so it can be called after dynamic loading
+function initializeFruitAndCategoryEvents() {
+ // initialize drag & drop on fruits
+ document.querySelectorAll('.fruit').forEach(el => {
+ el.addEventListener('dragstart', e => {
+ e.dataTransfer.setData('text/plain', el.dataset.fruit);
+ const dragImg = createDragImage(el);
+ e.dataTransfer.setDragImage(dragImg, dragImg.offsetWidth / 2, dragImg.offsetHeight / 2);
+ // cleanup after drag
+ el.addEventListener('dragend', () => {
+ dragImg.remove();
+ }, { once: true });
+ });
+
+ // updated mouseover event
+ el.addEventListener('mouseover', e => {
+ const parentCategory = el.closest('.categories');
+ if (parentCategory) {
+ // Unhighlight all squares for fruits in this category first
+ parentCategory.querySelectorAll('.fruit').forEach(sib => {
+ const sq = document.querySelector(`.square[data-fruit="${sib.dataset.fruit}"]`);
+ if (sq) sq.classList.remove('highlight');
+ });
+ }
+ // Now highlight only the hovered fruit's square
+ const square = document.querySelector(`.square[data-fruit="${el.dataset.fruit}"]`);
+ if (square) square.classList.add('highlight');
+ updateHighlightedBorders();
+ });
+
+ // updated mouseout event
+ el.addEventListener('mouseout', e => {
+ // Remove highlight from the hovered fruit's square
+ const square = document.querySelector(`.square[data-fruit="${el.dataset.fruit}"]`);
+ if (square) square.classList.remove('highlight');
+ // If still inside the category container, restore highlighting to all fruits there
+ const parentCategory = el.closest('.categories');
+ if (parentCategory && parentCategory.contains(e.relatedTarget)) {
+ parentCategory.querySelectorAll('.fruit').forEach(sib => {
+ const sq = document.querySelector(`.square[data-fruit="${sib.dataset.fruit}"]`);
+ if (sq) sq.classList.add('highlight');
+ });
+ }
+ updateHighlightedBorders();
+ });
+
+ // Add click handler for delete popup (sidebar only)
+ el.addEventListener('click', function(e) {
+ // Only trigger for sidebar fruits (not dropped-fruit)
+ if (!el.classList.contains('fruit') || el.classList.contains('dropped-fruit')) return;
+ // Find category name
+ const catDiv = el.closest('.categories');
+ if (!catDiv) return;
+ const catHeader = catDiv.querySelector('.category-header h4');
+ const categoryName = catHeader ? catHeader.textContent : null;
+ if (!categoryName) return;
+ showDeleteFruitPopup(el.dataset.fruit, categoryName);
+ e.stopPropagation();
+ });
+ });
+
+ // make each square a drop target
+ document.querySelectorAll('.square').forEach(sq => {
+ sq.addEventListener('dragover', e => e.preventDefault());
+ sq.addEventListener('drop', async e => {
+ e.preventDefault();
+ const fruit = e.dataTransfer.getData('text/plain');
+
+ // check if the fruit is already placed in another square
+ const existingSquare = document.querySelector(`.square[data-fruit="${fruit}"]`);
+ if (existingSquare && existingSquare !== sq) {
+ existingSquare.innerHTML = '';
+ delete existingSquare.dataset.fruit;
+ const existingSquareId = existingSquare.dataset.squareId;
+ await fetch('/api/positions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ squareId: existingSquareId, fruit: '' }),
+ });
+ }
+
+ placeFruitInSquare(sq, fruit);
+
+ // save to server
+ const squareId = sq.dataset.squareId;
await fetch('/api/positions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ squareId: existingSquareId, fruit: '' }),
+ body: JSON.stringify({ squareId, fruit }),
});
- }
+ });
- placeFruitInSquare(sq, fruit);
-
- // save to server
- const squareId = sq.dataset.squareId;
- await fetch('/api/positions', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ squareId, fruit }),
+ // Add double-click handler for custom fruit
+ sq.addEventListener('dblclick', e => {
+ // Only allow if square is empty (no fruit)
+ if (!sq.dataset.fruit && !sq.querySelector('.dropped-fruit')) {
+ showCustomFruitInput(sq);
+ }
});
});
-});
+
+ // add highlighting for squares based on category hover
+ document.querySelectorAll('.categories').forEach(category => {
+ category.addEventListener('mouseenter', () => {
+ category.querySelectorAll('.fruit').forEach(fruitEl => {
+ const fruitName = fruitEl.dataset.fruit;
+ const square = document.querySelector(`.square[data-fruit="${fruitName}"]`);
+ if (square) {
+ square.classList.add('highlight');
+ }
+ });
+ updateHighlightedBorders();
+ });
+ category.addEventListener('mouseleave', () => {
+ category.querySelectorAll('.fruit').forEach(fruitEl => {
+ const fruitName = fruitEl.dataset.fruit;
+ const square = document.querySelector(`.square[data-fruit="${fruitName}"]`);
+ if (square) {
+ square.classList.remove('highlight');
+ }
+ });
+ updateHighlightedBorders();
+ });
+ });
+
+ // Collapsible categories logic
+ document.querySelectorAll('.categories').forEach(category => {
+ const toggleBtn = category.querySelector('.category-header .category-toggle');
+ const content = category.querySelector('.category-content');
+ if (toggleBtn && content) {
+ toggleBtn.addEventListener('click', () => {
+ const isOpen = content.style.display !== 'none';
+ content.style.display = isOpen ? 'none' : '';
+ toggleBtn.textContent = isOpen ? '►' : '▼';
+ });
+ }
+ });
+}
// on load, fetch saved positions and render
window.addEventListener('DOMContentLoaded', async () => {
try {
+ // Add square numbers to all squares on initial load
+ document.querySelectorAll('.square').forEach(sq => {
+ // Only add if not already present (avoid duplicates)
+ if (!sq.querySelector('.square-number')) {
+ const numDiv = document.createElement('div');
+ numDiv.className = 'square-number';
+ numDiv.textContent = sq.dataset.squareId;
+ sq.appendChild(numDiv);
+ }
+ });
+
const res = await fetch('/api/positions');
const mapping = await res.json();
for (const [squareId, fruit] of Object.entries(mapping)) {
@@ -148,6 +352,14 @@ window.addEventListener('DOMContentLoaded', async () => {
} catch (err) {
console.error('Could not load saved positions', err);
}
+
+ // After categories are loaded, initialize events
+ if (window.loadCategories) {
+ await window.loadCategories();
+ initializeFruitAndCategoryEvents();
+ } else {
+ initializeFruitAndCategoryEvents();
+ }
});
// this is the stuff that let's you drag the map around and zoom in and out
@@ -266,30 +478,6 @@ zoomOutBtn.addEventListener('click', () => {
});
})();
-// add highlighting for squares based on category hover
-document.querySelectorAll('.categories').forEach(category => {
- category.addEventListener('mouseenter', () => {
- category.querySelectorAll('.fruit').forEach(fruitEl => {
- const fruitName = fruitEl.dataset.fruit;
- const square = document.querySelector(`.square[data-fruit="${fruitName}"]`);
- if (square) {
- square.classList.add('highlight');
- }
- });
- updateHighlightedBorders();
- });
- category.addEventListener('mouseleave', () => {
- category.querySelectorAll('.fruit').forEach(fruitEl => {
- const fruitName = fruitEl.dataset.fruit;
- const square = document.querySelector(`.square[data-fruit="${fruitName}"]`);
- if (square) {
- square.classList.remove('highlight');
- }
- });
- updateHighlightedBorders();
- });
-});
-
/*
this is a helper to update highlighted square borders.
for each highlighted square, if a neighboring square is also highlighted,
@@ -375,6 +563,11 @@ socket.on('update', data => {
} else {
sq.innerHTML = '';
delete sq.dataset.fruit;
+ // re-add the square number after clearing
+ const numDiv = document.createElement('div');
+ numDiv.className = 'square-number';
+ numDiv.textContent = sq.dataset.squareId;
+ sq.appendChild(numDiv);
}
// add highlight and remove after 3 seconds
sq.classList.add('highlight-update');
@@ -384,15 +577,57 @@ socket.on('update', data => {
}
});
-// Collapsible categories logic
-document.querySelectorAll('.categories').forEach(category => {
- const toggleBtn = category.querySelector('.category-header .category-toggle');
- const content = category.querySelector('.category-content');
- if (toggleBtn && content) {
- toggleBtn.addEventListener('click', () => {
- const isOpen = content.style.display !== 'none';
- content.style.display = isOpen ? 'none' : '';
- toggleBtn.textContent = isOpen ? '►' : '▼';
+// Helper to show a delete confirmation popup for a fruit
+function showDeleteFruitPopup(fruitName, categoryName) {
+ // Remove any existing popup
+ const existing = document.getElementById('delete-fruit-popup');
+ if (existing) existing.remove();
+
+ const popup = document.createElement('div');
+ popup.id = 'delete-fruit-popup';
+ popup.style.position = 'fixed';
+ popup.style.top = '50%';
+ popup.style.left = '50%';
+ popup.style.transform = 'translate(-50%, -50%)';
+ popup.style.background = '#222';
+ popup.style.color = 'white';
+ popup.style.padding = '24px 32px';
+ popup.style.borderRadius = '12px';
+ popup.style.boxShadow = '0 4px 32px #000b';
+ popup.style.zIndex = '2000';
+ popup.style.textAlign = 'center';
+
+ popup.innerHTML = `
+
+ Remove "${fruitName}" from ${categoryName}?
+
+
+
+ `;
+
+ document.body.appendChild(popup);
+
+ document.getElementById('delete-fruit-cancel').onclick = () => popup.remove();
+
+ document.getElementById('delete-fruit-confirm').onclick = async () => {
+ // Send delete request
+ await fetch('/api/delete-fruit', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ category: categoryName, fruit: fruitName })
});
- }
-});
+ popup.remove();
+ if (window.loadCategories) await window.loadCategories();
+ initializeFruitAndCategoryEvents();
+ // Remove fruit from any square if present
+ document.querySelectorAll(`.square[data-fruit="${fruitName}"]`).forEach(sq => {
+ sq.innerHTML = '';
+ delete sq.dataset.fruit;
+ // re-add the square number after clearing
+ const numDiv = document.createElement('div');
+ numDiv.className = 'square-number';
+ numDiv.textContent = sq.dataset.squareId;
+ sq.appendChild(numDiv);
+ });
+ };
+}
diff --git a/public/style.css b/public/style.css
index 7c53e2b..9d9bc34 100644
--- a/public/style.css
+++ b/public/style.css
@@ -27,6 +27,7 @@ body, html {
background: rgb(19, 19, 19);
overflow-y: auto; /* make it scrollable */
width: 12%; /* fixed width */
+ box-sizing: border-box; /* include padding/border in width */
}
.sidebar::-webkit-scrollbar {
@@ -91,7 +92,7 @@ body, html {
position: absolute;
top: 2px;
right: 2px;
- background: red;
+ background: rgba(0, 0, 0, 0);
border: none;
color: white;
border-radius: 50%;
@@ -118,8 +119,9 @@ body, html {
.categories {
/* make sidebar wider */
padding: 10px;
+ padding-bottom: 0px;
border: 1px solid white; /* sidebar border color */
- border-radius: 4px;
+ border-radius: 10px;
}
.fruit {
@@ -231,4 +233,43 @@ body, html {
height: 28px;
align-items: center;
display: flex;
+}
+
+.square-number {
+ position: absolute;
+ bottom: 4px;
+ right: 6px;
+ font-size: 0.9em;
+ color: #bdbdbd;
+ opacity: 0.7;
+ pointer-events: none;
+ z-index: 2;
+}
+
+.custom-fruit-input {
+ border: 1px solid #d103f9;
+ border-radius: 5px;
+ padding: 6px 10px;
+ outline: none;
+ background: #181818;
+ color: white;
+ width: 90%;
+ box-sizing: border-box;
+}
+
+.category-selectable {
+ box-shadow: 0 0 0 3px #d103f9, 0 0 10px 2px #d103f9;
+ border-color: #d103f9 !important;
+ cursor: pointer;
+ transition: box-shadow 0.2s;
+}
+
+.category-select-msg {
+ /* see JS for inline styles */
+}
+
+#delete-fruit-popup {
+ /* see JS for inline styles, but you can add more here if needed */
+ box-shadow: 0 4px 32px #000b;
+ z-index: 2000;
}
\ No newline at end of file
diff --git a/server.js b/server.js
index 0d89cfc..a7ec9d9 100644
--- a/server.js
+++ b/server.js
@@ -5,6 +5,7 @@ const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const http = require('http');
const { Server } = require('socket.io');
+const fs = require('fs');
const app = express();
const server = http.createServer(app);
@@ -63,6 +64,76 @@ app.post('/api/positions', (req, res) => {
);
});
+// Serve categories and fruits from a JSON file
+app.get('/api/categories', (req, res) => {
+ const categoriesPath = path.join(__dirname, 'categories.json');
+ fs.readFile(categoriesPath, 'utf8', (err, data) => {
+ if (err) return res.status(500).json({ error: 'Could not load categories' });
+ try {
+ const categories = JSON.parse(data);
+ res.json(categories);
+ } catch (e) {
+ res.status(500).json({ error: 'Invalid categories file' });
+ }
+ });
+});
+
+// Add a fruit to a category in categories.json
+app.post('/api/add-fruit', (req, res) => {
+ const { category, fruit } = req.body;
+ if (!category || !fruit) {
+ return res.status(400).json({ error: 'category and fruit required' });
+ }
+ const categoriesPath = path.join(__dirname, 'categories.json');
+ fs.readFile(categoriesPath, 'utf8', (err, data) => {
+ if (err) return res.status(500).json({ error: 'Could not load categories' });
+ let categories;
+ try {
+ categories = JSON.parse(data);
+ } catch (e) {
+ return res.status(500).json({ error: 'Invalid categories file' });
+ }
+ const cat = categories.find(c => c.name === category);
+ if (!cat) return res.status(404).json({ error: 'Category not found' });
+ // Prevent duplicates
+ if (cat.fruits.includes(fruit)) {
+ return res.status(400).json({ error: 'Fruit already exists in category' });
+ }
+ cat.fruits.push(fruit);
+ fs.writeFile(categoriesPath, JSON.stringify(categories, null, 2), err2 => {
+ if (err2) return res.status(500).json({ error: 'Could not save categories' });
+ res.json({ success: true });
+ });
+ });
+});
+
+// Delete a fruit from a category in categories.json
+app.post('/api/delete-fruit', (req, res) => {
+ const { category, fruit } = req.body;
+ if (!category || !fruit) {
+ return res.status(400).json({ error: 'category and fruit required' });
+ }
+ const categoriesPath = path.join(__dirname, 'categories.json');
+ fs.readFile(categoriesPath, 'utf8', (err, data) => {
+ if (err) return res.status(500).json({ error: 'Could not load categories' });
+ let categories;
+ try {
+ categories = JSON.parse(data);
+ } catch (e) {
+ return res.status(500).json({ error: 'Invalid categories file' });
+ }
+ const cat = categories.find(c => c.name === category);
+ if (!cat) return res.status(404).json({ error: 'Category not found' });
+ const idx = cat.fruits.indexOf(fruit);
+ if (idx === -1) return res.status(404).json({ error: 'Fruit not found in category' });
+ cat.fruits.splice(idx, 1);
+ fs.writeFile(categoriesPath, JSON.stringify(categories, null, 2), err2 => {
+ if (err2) return res.status(500).json({ error: 'Could not save categories' });
+ res.json({ success: true });
+ });
+ });
+});
+
// start server
const PORT = process.env.PORT || 3085;
server.listen(PORT, () => {