refactored categories code, now a seperate modifiable file

style changes and refinements
This commit is contained in:
2025-06-04 15:06:27 -07:00
parent 6c88ec3b15
commit 9bf808c761
7 changed files with 522 additions and 159 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
node_modules/
positions.db

41
categories.json Normal file
View File

@@ -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"
]
}
]

Binary file not shown.

View File

@@ -26,59 +26,8 @@
</div>
<!-- sidebar with the categories -->
<div class="sidebar">
<div class="categories">
<div class="category-header">
<h4>Ted's Team</h4>
<button class="category-toggle" type="button"></button>
</div>
<div class="category-content">
<div class="fruit" draggable="true" data-fruit="Brandon Brunson">Brandon Brunson</div>
<div class="fruit" draggable="true" data-fruit="Eric Smithson">Eric Smithson</div>
<div class="fruit" draggable="true" data-fruit="John Hammer">John Hammer</div>
<div class="fruit" draggable="true" data-fruit="Seth Lima">Seth Lima</div>
<div class="fruit" draggable="true" data-fruit="Ed Edington">Ed Edington</div>
<div class="fruit" draggable="true" data-fruit="Rick Sanchez">Rick Sanchez</div>
</div>
</div>
<div class="categories">
<div class="category-header">
<h4>Ariel's Team</h4>
<button class="category-toggle" type="button"></button>
</div>
<div class="category-content">
<div class="fruit" draggable="true" data-fruit="Jerry Smith">Jerry Smith</div>
<div class="fruit" draggable="true" data-fruit="Charles Carmichael">Charles Carmichael</div>
<div class="fruit" draggable="true" data-fruit="Michael Westen">Michael Westen</div>
<div class="fruit" draggable="true" data-fruit="Shawn Spencer">Shawn Spencer</div>
<div class="fruit" draggable="true" data-fruit="Eliot Alderson">Eliot Alderson</div>
<div class="fruit" draggable="true" data-fruit="Brian D">Brian D</div>
</div>
</div>
<div class="categories">
<div class="category-header">
<h4>Elsa's Team</h4>
<button class="category-toggle" type="button"></button>
</div>
<div class="category-content">
<div class="fruit" draggable="true" data-fruit="John Dorian">John Dorian</div>
<div class="fruit" draggable="true" data-fruit="Harvey Spectre">Harvey Spectre</div>
<div class="fruit" draggable="true" data-fruit="Juliet O'Hara">Juliet O'Hara</div>
<div class="fruit" draggable="true" data-fruit="Fiona Glenanne">Fiona Glenanne</div>
</div>
</div>
<div class="categories">
<div class="category-header">
<h4>Ana's Team</h4>
<button class="category-toggle" type="button"></button>
</div>
<div class="category-content">
<div class="fruit" draggable="true" data-fruit="Neal Caffrey">Neal Caffrey</div>
<div class="fruit" draggable="true" data-fruit="Chuck Bartowski">Chuck Bartowski</div>
<div class="fruit" draggable="true" data-fruit="Gus Burton">Gus Burton</div>
<div class="fruit" draggable="true" data-fruit="Mike Ross">Mike Ross</div>
</div>
</div>
<div class="sidebar" id="sidebar">
<!-- Categories will be dynamically loaded here -->
</div>
</div>
@@ -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 = `
<div class="category-header">
<h4>${cat.name}</h4>
<button class="category-toggle" type="button">▼</button>
</div>
<div class="category-content">
${cat.fruits.map(fruit =>
`<div class="fruit" draggable="true" data-fruit="${fruit}">${fruit}</div>`
).join('')}
</div>
`;
sidebar.appendChild(catDiv);
});
}
loadCategories();
</script>
<script src="/socket.io/socket.io.js"></script>
<script src="script.js"></script>

View File

@@ -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 });
});
// Helper to show an input box for custom fruit name
function showCustomFruitInput(squareEl) {
// Prevent multiple inputs
if (squareEl.querySelector('.custom-fruit-input')) return;
// 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');
});
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);
// 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();
}
// 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');
});
input.addEventListener('blur', function () {
const customName = input.value.trim();
if (!customName) {
// Restore square if nothing entered
squareEl.innerHTML = '';
squareEl.appendChild(numDiv);
return;
}
updateHighlightedBorders();
// Wait for category selection
waitForCategorySelection(customName, squareEl, squareNumber);
});
});
}
// 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');
// 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);
// 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;
// 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 = `
<div style="margin-bottom:18px;font-size:1.1em;">
Remove "<b>${fruitName}</b>" from <b>${categoryName}</b>?
</div>
<button id="delete-fruit-confirm" style="margin-right:16px;padding:6px 18px;background:#d103f9;color:white;border:none;border-radius:5px;cursor:pointer;">Delete</button>
<button id="delete-fruit-cancel" style="padding:6px 18px;background:#444;color:white;border:none;border-radius:5px;cursor:pointer;">Cancel</button>
`;
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);
});
};
}

View File

@@ -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 {
@@ -232,3 +234,42 @@ body, html {
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;
}

View File

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