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

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 });
});
// 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 = `
<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);
});
};
}