From 9bf808c761a273271996ae9ed3074ecaea2a6fbf Mon Sep 17 00:00:00 2001 From: brandon Date: Wed, 4 Jun 2025 15:06:27 -0700 Subject: [PATCH] refactored categories code, now a seperate modifiable file style changes and refinements --- .gitignore | 3 +- categories.json | 41 +++++ positions.db | Bin 12288 -> 12288 bytes public/index.html | 80 +++------ public/script.js | 441 +++++++++++++++++++++++++++++++++++----------- public/style.css | 45 ++++- server.js | 71 ++++++++ 7 files changed, 522 insertions(+), 159 deletions(-) create mode 100644 categories.json 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 bb43a70c6f302351ae1fb2d65cc82c6c1ca42f06..7a9d483eadac49e1a271f74f29f75e1147958a96 100644 GIT binary patch delta 260 zcmZojXh@hK&G=-Zj5FhtjR_0+P5I_9i1T^z@*m|}$9I*Vm9L$D6aNyvBL2zzar~wH zQhfjTd->n;yYnUSoAVv#d(F3i|0bW;#=?KRVj{fEg4#v~PDP1%DfxK{PDQ18#rb)Y zz2t%{`Svle@Q5-CN*h{+q^2nNq^9yQF$>BYngyqpWGMJ#<|gu;W8hRaH1*176=oLX zH8khfXJF?wG@X1wPRvVCgjrD4&>|=^Ia?t(F)ukIwTcyJ7q5|l01LAqSXV}#f=6O* zZfX&)p`{QHGry*}TV{Sc0f4ul$TW1&_qs+|(iwW@bTcLu03+#JrUJJO!ts(!ApQ zJYG>|L0Lmf=hEcT+@#bZZeC_VNh5=R)PjQ4B7S`aR#8C_W2A&3=BZ+n - @@ -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.name}

+ +
+
+ ${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, () => {