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/ 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> </div>
<!-- sidebar with the categories --> <!-- sidebar with the categories -->
<div class="sidebar"> <div class="sidebar" id="sidebar">
<div class="categories"> <!-- Categories will be dynamically loaded here -->
<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> </div>
</div> </div>
@@ -110,6 +59,31 @@
}); });
mapEl.appendChild(rowEl); 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>
<script src="/socket.io/socket.io.js"></script> <script src="/socket.io/socket.io.js"></script>
<script src="script.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 // helper to place a fruit div inside a square div
function placeFruitInSquare(squareEl, fruitName) { function placeFruitInSquare(squareEl, fruitName) {
// clear existing content // clear existing content except the square number
const squareNumber = squareEl.dataset.squareId;
squareEl.innerHTML = ''; squareEl.innerHTML = '';
// record the fruit in the square for tracking // record the fruit in the square for tracking
squareEl.dataset.fruit = fruitName; squareEl.dataset.fruit = fruitName;
@@ -45,6 +46,11 @@ function placeFruitInSquare(squareEl, fruitName) {
e.stopPropagation(); // prevent event bubbling e.stopPropagation(); // prevent event bubbling
squareEl.innerHTML = ''; squareEl.innerHTML = '';
delete squareEl.dataset.fruit; 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; const squareId = squareEl.dataset.squareId;
await fetch('/api/positions', { await fetch('/api/positions', {
method: 'POST', method: 'POST',
@@ -53,88 +59,286 @@ function placeFruitInSquare(squareEl, fruitName) {
}); });
}); });
squareEl.appendChild(clearBtn); 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 // Helper to show an input box for custom fruit name
document.querySelectorAll('.fruit').forEach(el => { function showCustomFruitInput(squareEl) {
el.addEventListener('dragstart', e => { // Prevent multiple inputs
e.dataTransfer.setData('text/plain', el.dataset.fruit); if (squareEl.querySelector('.custom-fruit-input')) return;
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 const squareNumber = squareEl.dataset.squareId;
el.addEventListener('mouseover', e => { squareEl.innerHTML = '';
const parentCategory = el.closest('.categories'); const input = document.createElement('input');
if (parentCategory) { input.type = 'text';
// Unhighlight all squares for fruits in this category first input.placeholder = 'Enter name...';
parentCategory.querySelectorAll('.fruit').forEach(sib => { input.className = 'custom-fruit-input';
const sq = document.querySelector(`.square[data-fruit="${sib.dataset.fruit}"]`); input.style.width = '90%';
if (sq) sq.classList.remove('highlight'); 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 input.addEventListener('blur', function () {
el.addEventListener('mouseout', e => { const customName = input.value.trim();
// Remove highlight from the hovered fruit's square if (!customName) {
const square = document.querySelector(`.square[data-fruit="${el.dataset.fruit}"]`); // Restore square if nothing entered
if (square) square.classList.remove('highlight'); squareEl.innerHTML = '';
// If still inside the category container, restore highlighting to all fruits there squareEl.appendChild(numDiv);
const parentCategory = el.closest('.categories'); return;
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(); // Wait for category selection
waitForCategorySelection(customName, squareEl, squareNumber);
}); });
}); }
// make each square a drop target // Wait for user to click a category, then send to server
document.querySelectorAll('.square').forEach(sq => { function waitForCategorySelection(customName, squareEl, squareNumber) {
sq.addEventListener('dragover', e => e.preventDefault()); // Highlight categories and show a message
sq.addEventListener('drop', async e => { document.querySelectorAll('.categories').forEach(cat => {
e.preventDefault(); cat.classList.add('category-selectable');
const fruit = e.dataTransfer.getData('text/plain'); });
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 // Handler for category click
const existingSquare = document.querySelector(`.square[data-fruit="${fruit}"]`); function onCategoryClick(e) {
if (existingSquare && existingSquare !== sq) { e.stopPropagation();
existingSquare.innerHTML = ''; document.querySelectorAll('.categories').forEach(cat => {
delete existingSquare.dataset.fruit; cat.classList.remove('category-selectable');
const existingSquareId = existingSquare.dataset.squareId; 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', { await fetch('/api/positions', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ squareId: existingSquareId, fruit: '' }), body: JSON.stringify({ squareId, fruit }),
}); });
} });
placeFruitInSquare(sq, fruit); // Add double-click handler for custom fruit
sq.addEventListener('dblclick', e => {
// save to server // Only allow if square is empty (no fruit)
const squareId = sq.dataset.squareId; if (!sq.dataset.fruit && !sq.querySelector('.dropped-fruit')) {
await fetch('/api/positions', { showCustomFruitInput(sq);
method: 'POST', }
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ squareId, fruit }),
}); });
}); });
});
// 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 // on load, fetch saved positions and render
window.addEventListener('DOMContentLoaded', async () => { window.addEventListener('DOMContentLoaded', async () => {
try { 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 res = await fetch('/api/positions');
const mapping = await res.json(); const mapping = await res.json();
for (const [squareId, fruit] of Object.entries(mapping)) { for (const [squareId, fruit] of Object.entries(mapping)) {
@@ -148,6 +352,14 @@ window.addEventListener('DOMContentLoaded', async () => {
} catch (err) { } catch (err) {
console.error('Could not load saved positions', 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 // 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. this is a helper to update highlighted square borders.
for each highlighted square, if a neighboring square is also highlighted, for each highlighted square, if a neighboring square is also highlighted,
@@ -375,6 +563,11 @@ socket.on('update', data => {
} else { } else {
sq.innerHTML = ''; sq.innerHTML = '';
delete sq.dataset.fruit; 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 // add highlight and remove after 3 seconds
sq.classList.add('highlight-update'); sq.classList.add('highlight-update');
@@ -384,15 +577,57 @@ socket.on('update', data => {
} }
}); });
// Collapsible categories logic // Helper to show a delete confirmation popup for a fruit
document.querySelectorAll('.categories').forEach(category => { function showDeleteFruitPopup(fruitName, categoryName) {
const toggleBtn = category.querySelector('.category-header .category-toggle'); // Remove any existing popup
const content = category.querySelector('.category-content'); const existing = document.getElementById('delete-fruit-popup');
if (toggleBtn && content) { if (existing) existing.remove();
toggleBtn.addEventListener('click', () => {
const isOpen = content.style.display !== 'none'; const popup = document.createElement('div');
content.style.display = isOpen ? 'none' : ''; popup.id = 'delete-fruit-popup';
toggleBtn.textContent = isOpen ? '►' : '▼'; 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); background: rgb(19, 19, 19);
overflow-y: auto; /* make it scrollable */ overflow-y: auto; /* make it scrollable */
width: 12%; /* fixed width */ width: 12%; /* fixed width */
box-sizing: border-box; /* include padding/border in width */
} }
.sidebar::-webkit-scrollbar { .sidebar::-webkit-scrollbar {
@@ -91,7 +92,7 @@ body, html {
position: absolute; position: absolute;
top: 2px; top: 2px;
right: 2px; right: 2px;
background: red; background: rgba(0, 0, 0, 0);
border: none; border: none;
color: white; color: white;
border-radius: 50%; border-radius: 50%;
@@ -118,8 +119,9 @@ body, html {
.categories { .categories {
/* make sidebar wider */ /* make sidebar wider */
padding: 10px; padding: 10px;
padding-bottom: 0px;
border: 1px solid white; /* sidebar border color */ border: 1px solid white; /* sidebar border color */
border-radius: 4px; border-radius: 10px;
} }
.fruit { .fruit {
@@ -232,3 +234,42 @@ body, html {
align-items: center; align-items: center;
display: flex; 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 path = require('path');
const http = require('http'); const http = require('http');
const { Server } = require('socket.io'); const { Server } = require('socket.io');
const fs = require('fs');
const app = express(); const app = express();
const server = http.createServer(app); 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 // start server
const PORT = process.env.PORT || 3085; const PORT = process.env.PORT || 3085;
server.listen(PORT, () => { server.listen(PORT, () => {