+
+
+
+
@@ -84,6 +100,81 @@
});
}
loadCategories();
+
+ // Settings button/menu logic
+ const settingsBtn = document.getElementById('settingsBtn');
+ const settingsMenu = document.getElementById('settingsMenu');
+
+ settingsBtn.addEventListener('click', () => {
+ settingsMenu.style.display = 'block';
+ });
+ // Optional: click outside menu closes it
+ document.addEventListener('mousedown', (e) => {
+ if (settingsMenu.style.display === 'block' &&
+ !settingsMenu.contains(e.target) &&
+ e.target !== settingsBtn) {
+ settingsMenu.style.display = 'none';
+ }
+ });
+
+ // Filled/Outline setting logic (toggle)
+ function applySquareStyle(style) {
+ document.body.setAttribute('data-square-style', style);
+ const btn = document.getElementById('squareStyleToggle');
+ if (btn) {
+ btn.textContent = style === 'filled' ? 'filling' : 'outlining';
+ btn.className = style === 'filled' ? 'toggle-filled' : 'toggle-outline';
+ }
+ }
+ function saveSquareStyle(style) {
+ localStorage.setItem('squareStyle', style);
+ }
+ function loadSquareStyle() {
+ return localStorage.getItem('squareStyle') || 'filled';
+ }
+
+ function setupSquareStyleSetting() {
+ const toggleBtn = document.getElementById('squareStyleToggle');
+ if (!toggleBtn) return;
+ let style = loadSquareStyle();
+ applySquareStyle(style);
+ toggleBtn.addEventListener('click', () => {
+ style = (style === 'filled') ? 'outline' : 'filled';
+ applySquareStyle(style);
+ saveSquareStyle(style);
+ });
+ }
+
+ document.addEventListener('DOMContentLoaded', setupSquareStyleSetting);
+ if (document.getElementById('squareStyleToggle')) setupSquareStyleSetting();
+
+ // Sidebar toggle for mobile
+ function setupSidebarToggle() {
+ const sidebar = document.getElementById('sidebar');
+ const toggleBtn = document.getElementById('sidebarToggle');
+ function updateSidebarDisplay() {
+ if (window.innerWidth <= 900) {
+ toggleBtn.style.display = '';
+ sidebar.classList.remove('open');
+ } else {
+ toggleBtn.style.display = 'none';
+ sidebar.classList.remove('open');
+ }
+ }
+ toggleBtn.addEventListener('click', () => {
+ sidebar.classList.toggle('open');
+ });
+ // Close sidebar when clicking outside on mobile
+ document.addEventListener('mousedown', (e) => {
+ if (window.innerWidth > 900) return;
+ if (sidebar.classList.contains('open') && !sidebar.contains(e.target) && e.target !== toggleBtn) {
+ sidebar.classList.remove('open');
+ }
+ });
+ window.addEventListener('resize', updateSidebarDisplay);
+ updateSidebarDisplay();
+ }
+ document.addEventListener('DOMContentLoaded', setupSidebarToggle);
diff --git a/public/script.js b/public/script.js
index 974f693..b51c725 100644
--- a/public/script.js
+++ b/public/script.js
@@ -114,6 +114,34 @@ function showCustomFruitInput(squareEl) {
// Wait for user to click a category, then send to server
function waitForCategorySelection(customName, squareEl, squareNumber) {
+ // --- Check if fruit name already exists ---
+ const fruitExists = Array.from(document.querySelectorAll('.fruit'))
+ .some(fruitEl => fruitEl.dataset.fruit && fruitEl.dataset.fruit.toLowerCase() === customName.toLowerCase());
+ if (fruitExists) {
+ // If fruit is already in a different square, move it
+ const existingSquare = document.querySelector(`.square[data-fruit="${customName}"]`);
+ if (existingSquare && existingSquare !== squareEl) {
+ existingSquare.innerHTML = '';
+ delete existingSquare.dataset.fruit;
+ const existingSquareId = existingSquare.dataset.squareId;
+ fetch('/api/positions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ squareId: existingSquareId, fruit: '' }),
+ });
+ }
+ // Place the fruit in the new square
+ placeFruitInSquare(squareEl, customName);
+ // Save the new fruit's position to the database
+ const squareId = squareEl.dataset.squareId;
+ fetch('/api/positions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ squareId, fruit: customName }),
+ });
+ return;
+ }
+
// Highlight categories and show a message
document.querySelectorAll('.categories').forEach(cat => {
cat.classList.add('category-selectable');
@@ -243,6 +271,14 @@ function initializeFruitAndCategoryEvents() {
document.querySelectorAll('.fruit').forEach(el => {
el.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', el.dataset.fruit);
+ // Also store the source category name for moving
+ const catDiv = el.closest('.categories');
+ if (catDiv) {
+ const catHeader = catDiv.querySelector('.category-header h4');
+ if (catHeader) {
+ e.dataTransfer.setData('source-category', catHeader.textContent);
+ }
+ }
const dragImg = createDragImage(el);
e.dataTransfer.setDragImage(dragImg, dragImg.offsetWidth / 2, dragImg.offsetHeight / 2);
// cleanup after drag
@@ -298,6 +334,44 @@ function initializeFruitAndCategoryEvents() {
});
});
+ // --- Enable dropping fruits into other categories ---
+ document.querySelectorAll('.category-content').forEach(content => {
+ content.addEventListener('dragover', e => {
+ // Only allow drop if dragging a fruit
+ if (e.dataTransfer.types.includes('text/plain')) {
+ e.preventDefault();
+ }
+ });
+ content.addEventListener('drop', async e => {
+ e.preventDefault();
+ const fruit = e.dataTransfer.getData('text/plain');
+ const sourceCategory = e.dataTransfer.getData('source-category');
+ // Find the target category name
+ const catDiv = content.closest('.categories');
+ const catHeader = catDiv ? catDiv.querySelector('.category-header h4') : null;
+ const targetCategory = catHeader ? catHeader.textContent : null;
+ if (!fruit || !sourceCategory || !targetCategory || sourceCategory === targetCategory) return;
+
+ // Move fruit: remove from old category, add to new
+ // 1. Remove from old
+ await fetch('/api/delete-fruit', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ category: sourceCategory, fruit })
+ });
+ // 2. Add to new
+ await fetch('/api/add-fruit', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ category: targetCategory, fruit })
+ });
+ // 3. Reload categories and re-init events
+ if (window.loadCategories) await window.loadCategories();
+ initializeFruitAndCategoryEvents();
+ });
+ });
+ // --- end drag-to-category ---
+
// make each square a drop target
document.querySelectorAll('.square').forEach(sq => {
sq.addEventListener('dragover', e => e.preventDefault());
@@ -682,3 +756,91 @@ function showDeleteFruitPopup(fruitName, categoryName) {
});
};
}
+
+// --- Touch support for map pan/zoom ---
+(() => {
+ const mapWrapper = document.getElementById('mapWrapper');
+ const map = document.getElementById('map');
+ let isDragging = false;
+ let startX, startY, startPanX, startPanY;
+ let panX = 0, panY = 0;
+ let currentScale = 1;
+ const minScale = 0.5, maxScale = 3;
+ let lastTouchDist = null;
+
+ function updateTransform() {
+ map.style.transform = `translate(${panX}px, ${panY}px) scale(${currentScale})`;
+ }
+
+ mapWrapper.addEventListener('touchstart', (e) => {
+ if (e.target.closest('.fruit') || e.target.closest('.dropped-fruit')) return;
+ if (e.touches.length === 1) {
+ isDragging = true;
+ startX = e.touches[0].clientX;
+ startY = e.touches[0].clientY;
+ startPanX = panX;
+ startPanY = panY;
+ map.style.cursor = 'grabbing';
+ } else if (e.touches.length === 2) {
+ isDragging = false;
+ lastTouchDist = Math.hypot(
+ e.touches[0].clientX - e.touches[1].clientX,
+ e.touches[0].clientY - e.touches[1].clientY
+ );
+ }
+ });
+
+ mapWrapper.addEventListener('touchmove', (e) => {
+ if (e.touches.length === 1 && isDragging) {
+ const deltaX = e.touches[0].clientX - startX;
+ const deltaY = e.touches[0].clientY - startY;
+ panX = startPanX + deltaX;
+ panY = startPanY + deltaY;
+ updateTransform();
+ } else if (e.touches.length === 2) {
+ // Pinch zoom
+ const dist = Math.hypot(
+ e.touches[0].clientX - e.touches[1].clientX,
+ e.touches[0].clientY - e.touches[1].clientY
+ );
+ if (lastTouchDist) {
+ let scaleChange = dist / lastTouchDist;
+ let newScale = currentScale * scaleChange;
+ newScale = Math.min(maxScale, Math.max(minScale, newScale));
+ currentScale = newScale;
+ updateTransform();
+ }
+ lastTouchDist = dist;
+ }
+ });
+
+ mapWrapper.addEventListener('touchend', (e) => {
+ isDragging = false;
+ lastTouchDist = null;
+ map.style.cursor = 'grab';
+ });
+})();
+
+// --- Touch support for fruit drag (basic fallback for mobile) ---
+function enableTouchFruitDrag() {
+ document.querySelectorAll('.fruit').forEach(el => {
+ el.addEventListener('touchstart', function(e) {
+ if (e.touches.length !== 1) return;
+ const fruitName = el.dataset.fruit;
+ // Find first empty square
+ const emptySq = Array.from(document.querySelectorAll('.square')).find(sq => !sq.dataset.fruit && !sq.querySelector('.dropped-fruit'));
+ if (emptySq) {
+ placeFruitInSquare(emptySq, fruitName);
+ // Save to server
+ const squareId = emptySq.dataset.squareId;
+ fetch('/api/positions', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ squareId, fruit: fruitName }),
+ });
+ }
+ e.preventDefault();
+ });
+ });
+}
+document.addEventListener('DOMContentLoaded', enableTouchFruitDrag);
diff --git a/public/style.css b/public/style.css
index 9d9bc34..607f690 100644
--- a/public/style.css
+++ b/public/style.css
@@ -80,6 +80,7 @@ body, html {
.square.highlight {
border: 1px solid purple; /* highlight color when hovered over in sidebar */
+ background-color: purple;
}
.square.highlight-update {
@@ -128,7 +129,7 @@ body, html {
cursor: grab;
margin-bottom: 10px;
padding: 5px 10px;
- border: 1px solid white; /* cells inside sidebar border color */
+ border: 1px solid rgba(255, 255, 255, 0.5); /* cells inside sidebar border color */
border-radius: 4px;
user-select: none;
color: white;
@@ -271,5 +272,193 @@ body, html {
#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;
+}
+
+/* Settings button in top right of map */
+.settings-btn {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ z-index: 101;
+ background: rgba(30,30,30,0.95);
+ border: 1px solid #d103f9;
+ border-radius: 50%;
+ width: 44px;
+ height: 44px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #d103f9;
+ font-size: 1.5rem;
+ cursor: pointer;
+ box-shadow: 0 2px 8px #0007;
+ transition: background 0.2s, border-color 0.2s;
+}
+.settings-btn:hover {
+ background: #2a003a;
+ border-color: #fff;
+}
+
+/* Settings menu styles */
+.settings-menu {
+ position: absolute;
+ top: 70px;
+ right: 16px;
+ z-index: 102;
+ background: #181818;
+ border: 1px solid #d103f9;
+ border-radius: 10px;
+ box-shadow: 0 4px 32px #000b;
+ padding: 0;
+ color: white;
+ animation: fadeInSettings 0.2s;
+}
+@keyframes fadeInSettings {
+ from { opacity: 0; transform: translateY(-10px);}
+ to { opacity: 1; transform: translateY(0);}
+}
+.settings-menu-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 18px 10px 18px;
+ border-bottom: 1px solid #333;
+ font-size: 1.1em;
+ font-weight: bold;
+ color: #d103f9;
+}
+.close-settings-btn {
+ background: none;
+ border: none;
+ color: #d103f9;
+ font-size: 1.2em;
+ cursor: pointer;
+ padding: 0 4px;
+ border-radius: 4px;
+ transition: background 0.2s;
+}
+.close-settings-btn:hover {
+ background: #2a003a;
+}
+.settings-menu-content {
+ padding: 20px;
+ padding-bottom: 0px;
+ color: white;
+ font-size: 1em;
+}
+
+/* Square Style toggle button */
+#squareStyleToggle {
+ padding: 6px 18px;
+ border-radius: 6px;
+ border: 1.5px solid #d103f9;
+ background: #191919;
+ color: #d103f9;
+ font-weight: bold;
+ font-size: 1em;
+ cursor: pointer;
+ transition: background 0.15s, color 0.15s, border-color 0.15s;
+ outline: none;
+}
+#squareStyleToggle.toggle-filled {
+ background: #d103f9;
+ color: white;
+ border-color: #d103f9;
+}
+#squareStyleToggle.toggle-outline {
+ background: #191919;
+ color: #d103f9;
+ border-color: #d103f9;
+}
+#squareStyleToggle:hover {
+ background: #2a003a;
+ color: white;
+}
+
+/* Filled/Outline toggle logic */
+body[data-square-style="filled"] .square.highlight {
+ border: 1px solid purple;
+ background-color: purple;
+}
+body[data-square-style="filled"] .square.highlight-update {
+ border: 1px solid purple;
+ box-shadow: 0 0 10px 2px purple;
+}
+body[data-square-style="outline"] .square.highlight {
+ border: 1px solid #d103f9;
+ background-color: transparent !important;
+}
+body[data-square-style="outline"] .square.highlight-update {
+ border: 1px solid #d103f9;
+ background-color: transparent !important;
+ box-shadow: 0 0 10px 2px #d103f9;
+}
+
+/* Responsive adjustments for mobile */
+@media (max-width: 900px) {
+ .container {
+ flex-direction: column;
+ align-items: stretch;
+ padding: 0;
+ }
+ .sidebar {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 80vw;
+ max-width: 350px;
+ height: 100vh;
+ z-index: 1001;
+ background: #181818;
+ transform: translateX(-100%);
+ transition: transform 0.3s;
+ box-shadow: 2px 0 16px #000a;
+ margin: 0;
+ }
+ .sidebar.open {
+ transform: translateX(0);
+ }
+ .map-wrapper {
+ width: 100vw;
+ height: 100vh;
+ margin: 0;
+ }
+ #zoomControls {
+ right: 10px;
+ bottom: 10px;
+ }
+}
+
+/* Make squares and map responsive */
+@media (max-width: 900px) {
+ .map-row > .square,
+ .map-row > .empty {
+ width: 13vw;
+ height: 13vw;
+ min-width: 48px;
+ min-height: 48px;
+ max-width: 80px;
+ max-height: 80px;
+ }
+ .square {
+ font-size: 1em;
+ }
+}
+
+/* Touch-friendly buttons and inputs */
+button, input, .fruit, .clear-btn {
+ touch-action: manipulation;
+}
+
+.clear-btn {
+ width: 28px;
+ height: 28px;
+ font-size: 18px;
+}
+
+/* Make popup and overlays scrollable on mobile */
+#delete-fruit-popup, .settings-menu {
+ max-width: 95vw;
+ box-sizing: border-box;
+ word-break: break-word;
}
\ No newline at end of file