From 03b1b8fdf62d19ddd282aa8625bb6855c6409681 Mon Sep 17 00:00:00 2001 From: brandon Date: Wed, 4 Jun 2025 17:28:48 -0700 Subject: [PATCH] added settings menu that saves and reads to/from the browser localStorage minor design changes --- .gitignore | 2 +- categories.json | 4 +- positions.db | Bin 12288 -> 12288 bytes public/index.html | 93 +++++++++++++++++++++- public/script.js | 162 ++++++++++++++++++++++++++++++++++++++ public/style.css | 193 +++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 448 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 4ec9213..9909d06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ node_modules/ -positions.db +positions.db \ No newline at end of file diff --git a/categories.json b/categories.json index c79cd74..2e47c28 100644 --- a/categories.json +++ b/categories.json @@ -4,9 +4,9 @@ "fruits": [ "Brandon Brunson", "Eric Smithson", - "John Hammer", "Seth Lima", - "Rick Sanchez" + "Rick Sanchez", + "John Hammer" ] }, { diff --git a/positions.db b/positions.db index 3db3987d9c9bd3035e940a9d7f36d80816a73936..16b16dd74f077309c866ee914b58cded88cb267f 100644 GIT binary patch delta 274 zcmZojXh@hK&1gGO#+lJ}W5PmyL%t;pqI`KQ{IB@0@h9*wEA~KO;}UBQZBO zwFqc}7&|k+p^uyZC?cf98L~|CGO({~muS|3&_q%~A@X{GvdoF&bKeDI)_BFwb0g Ia+iJ?0QaIthX4Qo delta 181 zcmZojXh@hK%_uxk#+gxgW5PmyZ3YGgA->fN{IB@0@gL#e!oQHepTC?xiQkvso?nk& zil3eDGv7nL(|o)6R&Ok<=bOA*Zo9oO2eY82k&$m^azW@f?3C**C!g!sD{__y&- g=kMbG#s8WA4gXXAd;AwSD=38WPi&B$+@)Uz0QmYh(EtDd diff --git a/public/index.html b/public/index.html index 3020a4f..aa7c5b6 100644 --- a/public/index.html +++ b/public/index.html @@ -9,13 +9,29 @@
+ +
+ + + +
@@ -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