diff --git a/beango/css/beango.css b/beango/css/beango.css new file mode 100644 index 0000000..418721c --- /dev/null +++ b/beango/css/beango.css @@ -0,0 +1,72 @@ +/* Basic styling for the board */ +.centered h1 { + margin-bottom: 0px; +} + +html { + background: linear-gradient(135deg, #ff7e5f, #feb47b); /* Modern gradient background */ +} + +.bingo-board { + display: grid; + gap: 4px; /* Adjust gap as needed */ +} +.centered { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; +} +#bean { + width: 60px; + height: 60px; +} +.bingo-cell { + border: 1px solid #8B4513; /* Brown border */ + text-align: center; + padding: 8px; + background-color: #F5F5DC; /* Beige background */ + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + min-width: 80px; /* Add minimum width */ + aspect-ratio: 1 / 1; /* Ensure square cells */ + /* Add transition for background color changes */ + transition: background-color 0.2s ease-in-out; + display: flex; + justify-content: center; + align-items: center; +} +.config-pane { + transition: max-height 0.3s ease-out, opacity 0.3s ease-out; + max-height: 500px; /* Adjust as needed */ + overflow: hidden; + opacity: 1; +} +.config-pane.minimized { + max-height: 0; + opacity: 0; + padding-top: 0; + padding-bottom: 0; + margin-top: 0; + margin-bottom: 0; + border-width: 0; +} + +/* Configuration Pane Styling */ +.config-pane-closed { + transform: translateX(100%); /* Hide off-screen to the right */ +} + +.config-pane-open { + transform: translateX(0); /* Slide in from the right */ +} + +/* Ensure transition is applied (already added in HTML via Tailwind classes) */ +/* #config-pane { + transition: transform 0.3s ease-in-out; +} */ + +/* Basic styling for the board */ +#bingo-board-container { +} diff --git a/beango/index.html b/beango/index.html new file mode 100644 index 0000000..761845c --- /dev/null +++ b/beango/index.html @@ -0,0 +1,79 @@ + + + + + + Beango! + + + + + + +
+ Notification Message +
+ + +
+
+

Beango!

+ Bean +
+ + +
+
+ +
Loading...
+
+
+ + +
+ + + + +
+
+ + +
+

Configuration

+
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + + + diff --git a/beango/js/beango.js b/beango/js/beango.js new file mode 100644 index 0000000..ceb3ba7 --- /dev/null +++ b/beango/js/beango.js @@ -0,0 +1,443 @@ +let currentItems = []; // Holds the original list of items provided by the user +let displayedItems = []; // Holds the items currently displayed on the board +let notificationTimeout = null; // To manage hiding notifications + +// --- localStorage Keys --- +const LS_BOARD_SIZE = 'beango_boardSize'; +const LS_CELL_ITEMS = 'beango_cellItems'; // Original items from textarea/file +const LS_DISPLAYED_ITEMS = 'beango_displayedItems'; // Items currently shown on the board +const LS_MARKED_INDICES = 'beango_markedIndices'; +const LS_CONFIG_OPEN = 'beango_configOpen'; // Changed from minimized + +// --- Notification Function --- +function showNotification(message, type = 'info', duration = 3000) { + const notificationArea = document.getElementById('notification-area'); + if (!notificationArea) return; + + // Clear any existing timeout + if (notificationTimeout) { + clearTimeout(notificationTimeout); + } + + notificationArea.textContent = message; + // Apply Tailwind classes based on type + notificationArea.className = 'fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 px-4 py-2 rounded shadow-lg text-white text-sm transition-opacity duration-300'; // Reset classes + switch (type) { + case 'success': + notificationArea.classList.add('bg-green-500'); + break; + case 'warning': + notificationArea.classList.add('bg-yellow-500'); + break; + case 'error': + notificationArea.classList.add('bg-red-500'); + break; + default: // info + notificationArea.classList.add('bg-blue-500'); + break; + } + + // Make it visible + notificationArea.classList.add('opacity-100'); + + // Set timeout to hide + notificationTimeout = setTimeout(() => { + notificationArea.classList.remove('opacity-100'); + notificationArea.classList.add('opacity-0'); + }, duration); +} + +// --- Functions --- + +function toggleConfig() { + const pane = document.getElementById('config-pane'); + const isOpen = pane.classList.contains('config-pane-open'); + + if (isOpen) { + pane.classList.remove('config-pane-open'); + pane.classList.add('config-pane-closed'); + localStorage.setItem(LS_CONFIG_OPEN, 'false'); + } else { + pane.classList.remove('config-pane-closed'); + pane.classList.add('config-pane-open'); + localStorage.setItem(LS_CONFIG_OPEN, 'true'); + } +} + +document.getElementById('file-input').addEventListener('change', function(event) { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = function(e) { + document.getElementById('cell-contents').value = e.target.result; + showNotification('File loaded. Click Generate/Update to use items.', 'success'); + }; + reader.onerror = function() { + showNotification('Error reading file.', 'error'); + }; + reader.readAsText(file); + } +}); + +function getItemsFromInput() { + const text = document.getElementById('cell-contents').value.trim(); + return text ? text.split('\n').map(item => item.trim()).filter(item => item) : []; +} + +// Helper to save board state +function saveBoardState() { + const sizeInput = document.getElementById('board-size'); + const size = sizeInput ? parseInt(sizeInput.value, 10) : 5; // Default to 5 if input missing + const markedIndices = getMarkedIndices(); + + localStorage.setItem(LS_BOARD_SIZE, size); + localStorage.setItem(LS_CELL_ITEMS, JSON.stringify(currentItems)); // Save original items + localStorage.setItem(LS_DISPLAYED_ITEMS, JSON.stringify(displayedItems)); // Save displayed items + localStorage.setItem(LS_MARKED_INDICES, JSON.stringify(markedIndices)); +} + +function generateBoard() { + const sizeInput = document.getElementById('board-size'); + const size = parseInt(sizeInput.value, 10); + if (isNaN(size) || size < 1) { + showNotification("Please enter a valid board size (minimum 1).", 'warning'); + return; + } + sizeInput.value = size; // Ensure value reflects parsed int + + // Update container width based on size + updateBoardContainerMaxWidth(size); + updateBoardContainerMaxWidth(size, '#board-header'); + + // Get items from input and store them as the canonical list + currentItems = getItemsFromInput(); + localStorage.setItem(LS_CELL_ITEMS, JSON.stringify(currentItems)); // Save original items immediately + + const requiredItems = size * size; + let itemsForBoard = [...currentItems]; // Start with a copy of original items + let notificationMessage = 'Board generated successfully!'; + let notificationType = 'success'; + + if (itemsForBoard.length < requiredItems) { + const diff = requiredItems - itemsForBoard.length; + notificationMessage = `Warning: Needed ${requiredItems} items, found ${itemsForBoard.length}. Padding with ${diff} placeholder(s).`; + notificationType = 'warning'; + for (let i = 0; i < diff; i++) { + itemsForBoard.push(`Placeholder ${i+1}`); + } + // Update currentItems if placeholders were added, so randomize uses them too + currentItems = [...itemsForBoard]; + localStorage.setItem(LS_CELL_ITEMS, JSON.stringify(currentItems)); + } else if (itemsForBoard.length > requiredItems) { + notificationMessage = `Info: Found ${itemsForBoard.length} items, randomly selecting ${requiredItems}.`; + notificationType = 'info'; + itemsForBoard = shuffleArray(itemsForBoard).slice(0, requiredItems); + // Update currentItems to reflect the selected subset + currentItems = [...itemsForBoard]; + localStorage.setItem(LS_CELL_ITEMS, JSON.stringify(currentItems)); + } + + // Shuffle the items specifically for display + displayedItems = shuffleArray([...itemsForBoard]); + + const board = document.getElementById('bingo-board'); + board.innerHTML = ''; // Clear previous board + board.style.gridTemplateColumns = `repeat(${size}, minmax(80px, 1fr))`; + + displayedItems.forEach((item, index) => { + const cell = document.createElement('div'); + cell.classList.add('bingo-cell', 'cursor-pointer', 'hover:bg-green-200'); + cell.textContent = item; + cell.dataset.index = index; // Add index for saving marks + cell.onclick = () => selectCell(cell); + board.appendChild(cell); + }); + + clearMarks(false); // Clear previous marks visually but don't save yet + saveBoardState(); // Save the newly generated state + showNotification(notificationMessage, notificationType); +} + +function randomizeBoard() { + const storedItemsRaw = localStorage.getItem(LS_CELL_ITEMS); + const sizeRaw = localStorage.getItem(LS_BOARD_SIZE); + + if (!storedItemsRaw || !sizeRaw) { + showNotification("Cannot randomize: Board state not found. Please generate the board first.", 'warning'); + return; + } + + const storedItems = JSON.parse(storedItemsRaw); + const size = parseInt(sizeRaw, 10); + const requiredItems = size * size; + + if (storedItems.length < requiredItems) { + showNotification(`Cannot randomize: Not enough items saved (${storedItems.length}/${requiredItems}). Please generate the board again.`, 'warning'); + return; + } + + // Ensure we use the correct set if placeholders were added or items were sliced + let itemsToShuffle = [...storedItems]; + if (itemsToShuffle.length > requiredItems) { + // This case implies items were sliced during generation, which is stored in currentItems + // Randomizing should still use the items that *could* be on the board + itemsToShuffle = itemsToShuffle.slice(0, requiredItems); + + } else if (itemsToShuffle.length < requiredItems) { + // This case shouldn't happen if generateBoard saved padded items + showNotification("Cannot randomize: Item count mismatch. Please regenerate the board.", 'error'); + return; + } + + displayedItems = shuffleArray([...itemsToShuffle]); // Shuffle the items for display + + const board = document.getElementById('bingo-board'); + const cells = board.querySelectorAll('.bingo-cell'); + + if (cells.length !== displayedItems.length) { + showNotification("Cannot randomize: Board size mismatch. Please generate the board again.", 'error'); + return; + } + + cells.forEach((cell, index) => { + cell.textContent = displayedItems[index]; + cell.classList.remove('bg-yellow-300', 'ring-2', 'ring-blue-500'); // Clear marks visually + }); + + clearMarks(false); // Clear visual marks + saveBoardState(); // Save the new randomized state (including cleared marks) + showNotification('Board randomized!', 'success'); +} + +function selectCell(cell) { + cell.classList.toggle('bg-yellow-300'); // Toggle mark style directly + saveBoardState(); // Save updated marks immediately after click +} + +function clearMarks(save = true) { + const cells = document.querySelectorAll('#bingo-board .bingo-cell'); + cells.forEach(cell => { + cell.classList.remove('bg-yellow-300', 'ring-2', 'ring-blue-500'); // Remove ring class here too + }); + if (save) { + saveBoardState(); // Save cleared marks + showNotification('Marks cleared.', 'info'); + } +} + +// Fisher-Yates (aka Knuth) Shuffle Algorithm +function shuffleArray(array) { + let currentIndex = array.length, randomIndex; + while (currentIndex !== 0) { + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + [array[currentIndex], array[randomIndex]] = [ + array[randomIndex], array[currentIndex]]; + } + return array; +} + +// Function to reset settings and clear board +function resetSettings() { + // Clear localStorage relevant to the board + localStorage.removeItem(LS_BOARD_SIZE); + localStorage.removeItem(LS_CELL_ITEMS); + localStorage.removeItem(LS_DISPLAYED_ITEMS); + localStorage.removeItem(LS_MARKED_INDICES); + // Optionally reset config pane state too? + // localStorage.removeItem(LS_CONFIG_OPEN); + + // Reset global variables + currentItems = []; + displayedItems = []; + + // Reset form inputs + document.getElementById('board-size').value = 5; // Default size + document.getElementById('cell-contents').value = ''; + document.getElementById('file-input').value = ''; // Clear file input + + // Clear the board display + const board = document.getElementById('bingo-board'); + board.innerHTML = '
Settings Reset. Generate a new board!
'; + board.style.gridTemplateColumns = ''; // Clear grid style + + // Reset container width to default + updateBoardContainerMaxWidth(5); // Assuming 5 is the default size + updateBoardContainerMaxWidth(5, '#board-header'); + showNotification('Settings and board reset.', 'success'); +} + +// --- Helper to get indices of marked cells --- +function getMarkedIndices() { + const markedCells = document.querySelectorAll('#bingo-board .bingo-cell.bg-yellow-300'); + const indices = []; + markedCells.forEach(cell => { + // Ensure dataset.index exists and is a valid number + if (cell.dataset && typeof cell.dataset.index !== 'undefined') { + const index = parseInt(cell.dataset.index, 10); + if (!isNaN(index)) { + indices.push(index); + } + } else { + console.warn('Marked cell found without a valid data-index:', cell); + } + }); + return indices; +} + +// --- Load State on Page Load --- +function loadFromLocalStorage() { + const savedSize = localStorage.getItem(LS_BOARD_SIZE) || '5'; // Default to 5 + const savedItemsText = localStorage.getItem(LS_CELL_ITEMS); + const savedDisplayedItems = localStorage.getItem(LS_DISPLAYED_ITEMS); + const savedMarkedIndices = localStorage.getItem(LS_MARKED_INDICES); + const configIsOpen = localStorage.getItem(LS_CONFIG_OPEN) === 'true'; + + // Restore Config Pane State + const pane = document.getElementById('config-pane'); + if (configIsOpen) { + pane.classList.remove('config-pane-closed'); + pane.classList.add('config-pane-open'); + } else { + pane.classList.remove('config-pane-open'); + pane.classList.add('config-pane-closed'); + } + + // Restore Config Inputs + const size = parseInt(savedSize, 10); // Parse size here + document.getElementById('board-size').value = savedSize; + + // Update container width based on loaded size + updateBoardContainerMaxWidth(size); + updateBoardContainerMaxWidth(size, '#board-header'); + + if (savedItemsText) { + try { + currentItems = JSON.parse(savedItemsText); // Restore original items array + document.getElementById('cell-contents').value = currentItems.join('\n'); + } catch (e) { + showNotification('Error parsing saved items. Resetting items.', 'error'); + localStorage.removeItem(LS_CELL_ITEMS); + currentItems = []; + document.getElementById('cell-contents').value = ''; + } + } + + // Restore Board only if essential data exists + if (savedDisplayedItems) { + try { + displayedItems = JSON.parse(savedDisplayedItems); // Restore displayed items array + } catch (e) { + showNotification('Error parsing saved board state. Please generate again.', 'error'); + localStorage.removeItem(LS_DISPLAYED_ITEMS); + localStorage.removeItem(LS_MARKED_INDICES); + displayedItems = []; + document.getElementById('bingo-board').innerHTML = '
Error loading board. Generate New.
'; + return; // Stop board restore + } + + const requiredItems = size * size; + + // Check if the loaded data is consistent + if (!Array.isArray(displayedItems) || displayedItems.length !== requiredItems) { + showNotification("Saved board data mismatch. Please generate again.", 'warning'); + localStorage.removeItem(LS_DISPLAYED_ITEMS); + localStorage.removeItem(LS_MARKED_INDICES); + document.getElementById('bingo-board').innerHTML = '
Data Mismatch. Generate New.
'; + return; // Stop loading the board state + } + + const board = document.getElementById('bingo-board'); + board.innerHTML = ''; // Clear placeholder + board.style.gridTemplateColumns = `repeat(${size}, minmax(80px, 1fr))`; + + const markedIndices = savedMarkedIndices ? JSON.parse(savedMarkedIndices) : []; + + displayedItems.forEach((item, index) => { + const cell = document.createElement('div'); + cell.classList.add('bingo-cell', 'cursor-pointer', 'hover:bg-green-200'); + cell.textContent = item; + cell.dataset.index = index; // Ensure index is set for loading marks correctly + cell.onclick = () => selectCell(cell); + if (markedIndices.includes(index)) { + cell.classList.add('bg-yellow-300'); // Apply saved mark + } + board.appendChild(cell); + }); + } else { + // If no saved board state, show the default message + const board = document.getElementById('bingo-board'); + // Check if board div exists and has no children before adding message + if (board && board.children.length === 0) { + board.innerHTML = '
Generate a board using the config panel!
'; + } + } +} + +// --- Helper to manage board container width --- +function updateBoardContainerMaxWidth(size, element = '#bingo-board-container') { + const container = document.querySelector(element); + + if (!container) return; + + // Remove existing max-w classes + const classList = container.classList; + const existingMaxWidthClasses = Array.from(classList).filter(cls => cls.startsWith('max-w-')); + classList.remove(...existingMaxWidthClasses); + + // Determine and add the new max-w class + let maxWidthClass = 'max-w-2xl'; // Default for 5x5 + if (size <= 3) { + maxWidthClass = 'max-w-lg'; + } else if (size === 4) { + maxWidthClass = 'max-w-xl'; + } else if (size === 5) { + maxWidthClass = 'max-w-2xl'; + } else if (size === 6) { + maxWidthClass = 'max-w-3xl'; + } else if (size === 7) { + maxWidthClass = 'max-w-4xl'; + } else if (size >= 8) { + maxWidthClass = 'max-w-5xl'; // Cap here, rely on scroll for larger + } + container.classList.add(maxWidthClass); +} + +function explodeBeans() { + const container = document.querySelector('.centered'); + for (let i = 0; i < 10; i++) { + const newBean = document.createElement('img'); + newBean.src = '../bean.svg'; + newBean.style.position = 'absolute'; + newBean.style.width = '50px'; + newBean.style.height = '50px'; + newBean.style.transition = 'transform 1s ease-out, opacity 1s ease-out'; + newBean.style.opacity = '0'; + + // Initial position at the center + newBean.style.transform = 'translate(0, 0) scale(0.5)'; + + container.appendChild(newBean); + + // Random position around the original bean + const angle = Math.random() * 2 * Math.PI; + const distance = Math.random() * 100 + 50; + + // Trigger the animation + setTimeout(() => { + newBean.style.opacity = '1'; + newBean.style.transform = `translate(${Math.cos(angle) * distance}px, ${Math.sin(angle) * distance}px) scale(1)`; + }, 0); + + // Remove the bean after animation + setTimeout(() => { + newBean.style.opacity = '0'; + setTimeout(() => { + container.removeChild(newBean); + }, 1000); + }, 1000); + } +} + +// --- Event Listener for Page Load --- +document.addEventListener('DOMContentLoaded', loadFromLocalStorage);