refactored categories code, now a seperate modifiable file
style changes and refinements
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
positions.db
|
||||||
41
categories.json
Normal file
41
categories.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
positions.db
BIN
positions.db
Binary file not shown.
@@ -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>
|
||||||
|
|||||||
305
public/script.js
305
public/script.js
@@ -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,8 +59,135 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to show an input box for custom fruit name
|
||||||
|
function showCustomFruitInput(squareEl) {
|
||||||
|
// Prevent multiple inputs
|
||||||
|
if (squareEl.querySelector('.custom-fruit-input')) return;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
// initialize drag & drop on fruits
|
||||||
document.querySelectorAll('.fruit').forEach(el => {
|
document.querySelectorAll('.fruit').forEach(el => {
|
||||||
el.addEventListener('dragstart', e => {
|
el.addEventListener('dragstart', e => {
|
||||||
@@ -98,6 +231,20 @@ document.querySelectorAll('.fruit').forEach(el => {
|
|||||||
}
|
}
|
||||||
updateHighlightedBorders();
|
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
|
// make each square a drop target
|
||||||
@@ -130,11 +277,68 @@ document.querySelectorAll('.square').forEach(sq => {
|
|||||||
body: JSON.stringify({ squareId, fruit }),
|
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
|
// 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);
|
||||||
|
});
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
71
server.js
71
server.js
@@ -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, () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user