diff --git a/MOBILE_SETUP.md b/MOBILE_SETUP.md new file mode 100644 index 0000000..ac5bdf0 --- /dev/null +++ b/MOBILE_SETUP.md @@ -0,0 +1,55 @@ +# Mobile vs Desktop Setup + +The crossword puzzle now has separate optimized versions for mobile and desktop/tablet devices. + +## Files + +### Mobile Version (< 768px width) +- **mobile.html** - Simplified mobile-optimized layout +- **mobile.css** - Mobile-first CSS with compact design +- **mobile.js** - Simplified game logic optimized for touch + +### Desktop/Tablet Version (>= 768px width) +- **index.html** - Full-featured desktop layout +- **styles_v5.css** - Desktop/tablet responsive CSS +- **game.js** - Full game logic with advanced features + +### Shared Files +- **puzzleData.js** - Puzzle data used by both versions +- **redirect.js** - Automatic device detection and redirection + +## How It Works + +1. User visits the site (index.html or mobile.html) +2. `redirect.js` detects device type +3. Automatically redirects to appropriate version: + - Mobile devices → mobile.html + - Desktop/tablet → index.html + +## Key Differences + +### Mobile Version Features: +- Compact header (50px) +- Simplified controls +- Touch-optimized grid (30-42px cells) +- Minimal padding for maximum grid space +- Single-column layout +- Streamlined UI + +### Desktop Version Features: +- Full header with stats +- Larger grid (40-120px cells) +- More detailed UI elements +- Better spacing and padding +- Advanced features + +## Testing + +To test mobile version on desktop: +1. Open browser dev tools +2. Toggle device emulation +3. Refresh page - should auto-redirect to mobile.html + +To force a specific version: +- Mobile: Go directly to `/mobile.html` +- Desktop: Go directly to `/index.html` diff --git a/public/index.html b/public/index.html index 0ce6822..179d645 100644 --- a/public/index.html +++ b/public/index.html @@ -4,6 +4,7 @@ Kruiswoord Pro: Zweedse puzzel + diff --git a/public/mobile.css b/public/mobile.css new file mode 100644 index 0000000..d0a76f0 --- /dev/null +++ b/public/mobile.css @@ -0,0 +1,403 @@ +/* Mobile-First Reset */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + overflow-x: hidden; + touch-action: manipulation; +} + +.mobile-container { + width: 100vw; + height: 100vh; + background: white; + display: flex; + flex-direction: column; + overflow: hidden; +} + +/* Compact Header */ +.mobile-header { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; + padding: 8px 10px; + display: flex; + align-items: center; + gap: 10px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + flex-shrink: 0; +} + +.menu-icon { + background: rgba(255,255,255,0.2); + border: none; + color: white; + width: 36px; + height: 36px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + cursor: pointer; +} + +.header-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; +} + +.level { + font-weight: 600; + font-size: 14px; +} + +.resources { + display: flex; + gap: 12px; + font-size: 12px; +} + +.coins, .hints { + display: flex; + align-items: center; + gap: 4px; + background: rgba(255,255,255,0.2); + padding: 2px 8px; + border-radius: 10px; +} + +/* Compact Progress */ +.progress-compact { + background: #f8f9fa; + padding: 6px 10px; + display: flex; + align-items: center; + gap: 8px; + border-bottom: 1px solid #e9ecef; + flex-shrink: 0; +} + +.progress-bar { + flex: 1; + height: 6px; + background: #e9ecef; + border-radius: 3px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, #48bb78, #38a169); + width: 0%; + transition: width 0.3s ease; +} + +.progress-text { + font-size: 11px; + font-weight: 600; + color: #4a5568; + min-width: 35px; +} + +/* Grid Section - Optimized for Mobile */ +.grid-section { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 5px; + overflow: hidden; + background: #f8f9fa; +} + +.grid-wrapper { + display: grid; + background: #a0aec0; + padding: 3px; + border-radius: 6px; + gap: 1px; + max-width: 100%; + max-height: 100%; +} + +.grid-cell { + background: white; + border: 1px solid #cbd5e0; + display: flex; + align-items: center; + justify-content: center; + position: relative; + font-size: 16px; + font-weight: bold; +} + +.grid-cell.clue-cell { + background: linear-gradient(135deg, #4a5568, #2d3748); + color: white; + padding: 2px; + font-size: 7px; + font-weight: 600; + line-height: 1.1; + text-align: left; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + overflow: hidden; +} + +.clue-text { + font-size: 7px; + line-height: 1.1; + word-wrap: break-word; + width: 100%; + z-index: 1; +} + +.arrow { + position: absolute; + color: #4299e1; + font-weight: bold; + font-size: 12px; +} + +.arrow-right { + right: 2px; + top: 50%; + transform: translateY(-50%); +} + +.grid-cell.blocked-cell { + background: #2d3748; + border-color: #2d3748; +} + +.grid-cell.input-cell { + background: white; +} + +.grid-cell.input-cell.active { + background: #ebf8ff; + border-color: #3182ce; + box-shadow: 0 0 0 2px rgba(66, 153, 225, 0.3); +} + +.grid-cell.correct { + background: #c6f6d5; + border-color: #48bb78; +} + +.grid-cell.incorrect { + background: #fed7d7; + border-color: #f56565; +} + +.grid-cell input { + width: 100%; + height: 100%; + border: none; + background: transparent; + text-align: center; + font-weight: bold; + color: #2d3748; + text-transform: uppercase; + font-size: inherit; + outline: none; +} + +/* Mobile Controls */ +.mobile-controls { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 6px; + padding: 8px; + background: white; + border-top: 1px solid #e9ecef; + flex-shrink: 0; +} + +.control-btn { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + border: none; + border-radius: 8px; + padding: 10px 6px; + display: flex; + flex-direction: column; + align-items: center; + gap: 3px; + cursor: pointer; + font-weight: 600; + font-size: 10px; + transition: transform 0.2s; +} + +.control-btn:active { + transform: scale(0.95); +} + +.control-btn i { + font-size: 16px; +} + +.control-btn.hint { + background: linear-gradient(135deg, #ed8936, #dd6b20); +} + +/* Side Menu */ +.side-menu { + position: fixed; + top: 0; + left: -250px; + width: 250px; + height: 100vh; + background: white; + box-shadow: 2px 0 10px rgba(0,0,0,0.2); + transition: left 0.3s ease; + z-index: 1000; +} + +.side-menu.active { + left: 0; +} + +.menu-header { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + color: white; + padding: 15px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.close-menu { + background: none; + border: none; + color: white; + font-size: 20px; + cursor: pointer; +} + +.menu-items { + padding: 10px 0; +} + +.menu-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 15px; + border: none; + background: none; + width: 100%; + text-align: left; + cursor: pointer; + font-size: 14px; + transition: background 0.2s; +} + +.menu-item:active { + background: #f8f9fa; +} + +.menu-item i { + width: 18px; + color: #4facfe; +} + +/* Modals */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.6); + z-index: 2000; + justify-content: center; + align-items: center; + padding: 20px; +} + +.modal.active { + display: flex; +} + +.modal-content { + background: white; + border-radius: 16px; + padding: 24px; + text-align: center; + max-width: 320px; + width: 100%; + box-shadow: 0 10px 30px rgba(0,0,0,0.3); +} + +.success-icon { + font-size: 48px; + color: #48bb78; + margin-bottom: 12px; +} + +.modal-content h2 { + color: #2d3748; + margin-bottom: 8px; + font-size: 20px; +} + +.modal-content h3 { + color: #2d3748; + margin-bottom: 12px; + font-size: 18px; +} + +.modal-content p { + color: #4a5568; + margin-bottom: 16px; + font-size: 14px; +} + +.modal-btns { + display: flex; + gap: 8px; + margin-top: 16px; +} + +.btn-primary { + background: linear-gradient(135deg, #48bb78, #38a169); + color: white; + border: none; + border-radius: 8px; + padding: 12px 20px; + cursor: pointer; + font-weight: 600; + font-size: 14px; + flex: 1; +} + +.btn-secondary { + background: #e2e8f0; + color: #4a5568; + border: none; + border-radius: 8px; + padding: 12px 20px; + cursor: pointer; + font-weight: 600; + font-size: 14px; + flex: 1; +} + +.btn-primary:active, +.btn-secondary:active { + transform: scale(0.98); +} diff --git a/public/mobile.html b/public/mobile.html new file mode 100644 index 0000000..f15c7f7 --- /dev/null +++ b/public/mobile.html @@ -0,0 +1,108 @@ + + + + + + Kruiswoord Pro - Mobile + + + + + +
+ +
+ +
+ Niveau 1 +
+ 100 + 3 +
+
+
+ + +
+
+
+
+ 0% +
+ + +
+
+ +
+
+ + +
+ + + + +
+ + +
+ + +
+ + + + + + +
+ + + + + diff --git a/public/mobile.js b/public/mobile.js new file mode 100644 index 0000000..59c65af --- /dev/null +++ b/public/mobile.js @@ -0,0 +1,442 @@ +// Mobile-Optimized Crossword Game +class MobileCrosswordGame { + constructor() { + this.currentLevel = 1 + this.coins = 100 + this.hints = 3 + this.currentPuzzle = null + this.userAnswers = {} + this.selectedCell = null + this.gridElement = document.getElementById('crosswordGrid') + + this.init() + } + + init() { + this.loadPuzzle(this.currentLevel) + this.setupEventListeners() + this.updateUI() + } + + setupEventListeners() { + // Menu + document.getElementById('menuBtn').addEventListener('click', () => { + document.getElementById('sideMenu').classList.add('active') + }) + document.getElementById('closeMenu').addEventListener('click', () => { + document.getElementById('sideMenu').classList.remove('active') + }) + + // Controls + document.getElementById('checkBtn').addEventListener('click', () => this.checkAnswers()) + document.getElementById('clearBtn').addEventListener('click', () => this.clearGrid()) + document.getElementById('hintBtn').addEventListener('click', () => this.showHintModal()) + document.getElementById('skipBtn').addEventListener('click', () => this.skipPuzzle()) + + // Modals + document.getElementById('useHintBtn').addEventListener('click', () => this.useHint()) + document.getElementById('cancelHintBtn').addEventListener('click', () => this.hideHintModal()) + document.getElementById('nextBtn').addEventListener('click', () => this.nextPuzzle()) + + // Menu items + document.getElementById('dailyPuzzle').addEventListener('click', () => { + this.showFeedback('Dagelijkse puzzel komt binnenkort!') + document.getElementById('sideMenu').classList.remove('active') + }) + document.getElementById('levelSelect').addEventListener('click', () => { + this.showFeedback('Niveau selectie komt binnenkort!') + document.getElementById('sideMenu').classList.remove('active') + }) + } + + loadPuzzle(level) { + // Use puzzles from puzzleData.js + if (typeof DUTCH_PUZZLES !== 'undefined') { + this.currentPuzzle = DUTCH_PUZZLES[`level${level}`] || DUTCH_PUZZLES.level1 + } + + this.userAnswers = {} + this.renderGrid() + this.updateProgress() + this.updateUI() + } + + renderGrid() { + if (!this.currentPuzzle) return + + const grid = this.currentPuzzle.grid + const words = this.currentPuzzle.words + + this.gridElement.innerHTML = '' + + // Calculate grid dimensions (add 1 column for clues) + const rows = grid.length + const cols = Math.max(...grid.map(row => row.length)) + 1 + + // Calculate optimal cell size for mobile + const cellSize = this.calculateCellSize(rows, cols) + + // Set grid styles + this.gridElement.style.gridTemplateColumns = `repeat(${cols}, ${cellSize}px)` + this.gridElement.style.gridTemplateRows = `repeat(${rows}, ${cellSize}px)` + + // Create grid cells + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + if (col === 0) { + // First column: clue cells + const clueData = this.getClueForRow(row, words) + this.gridElement.appendChild(this.createClueCell(clueData, cellSize)) + } else { + // Data columns + const gridCol = col - 1 + const cellValue = grid[row]?.[gridCol] || '#' + this.gridElement.appendChild(this.createCell(cellValue, row, gridCol, words, cellSize)) + } + } + } + + this.attachCellListeners() + } + + calculateCellSize(rows, cols) { + // Mobile-specific calculation + const headerHeight = 50 + const progressHeight = 35 + const controlsHeight = 75 + const padding = 16 + + const availableHeight = window.innerHeight - headerHeight - progressHeight - controlsHeight - padding + const availableWidth = window.innerWidth - padding + + const maxFromHeight = Math.floor(availableHeight / rows) + const maxFromWidth = Math.floor(availableWidth / cols) + + let cellSize = Math.min(maxFromHeight, maxFromWidth) + + // Constrain to 30-42px for mobile readability + cellSize = Math.max(30, Math.min(42, cellSize)) + + return cellSize + } + + getClueForRow(row, words) { + return words.find(w => w.direction === 'horizontal' && w.startRow === row) + } + + createClueCell(clueData, cellSize) { + const cell = document.createElement('div') + cell.className = 'grid-cell clue-cell' + cell.style.width = `${cellSize}px` + cell.style.height = `${cellSize}px` + + if (clueData) { + const text = document.createElement('div') + text.className = 'clue-text' + text.textContent = clueData.clue + + const arrow = document.createElement('div') + arrow.className = 'arrow arrow-right' + arrow.textContent = '→' + + cell.appendChild(text) + cell.appendChild(arrow) + } else { + cell.classList.add('blocked-cell') + } + + return cell + } + + createCell(value, row, col, words, cellSize) { + const cell = document.createElement('div') + cell.className = 'grid-cell' + cell.style.width = `${cellSize}px` + cell.style.height = `${cellSize}px` + cell.dataset.row = row + cell.dataset.col = col + + if (value === '#') { + // Check if it's a clue cell for second word + const clueData = this.getClueAtPosition(row, col, words) + if (clueData) { + cell.classList.add('clue-cell') + + const text = document.createElement('div') + text.className = 'clue-text' + text.textContent = clueData.clue + + const arrow = document.createElement('div') + arrow.className = 'arrow arrow-right' + arrow.textContent = '→' + + cell.appendChild(text) + cell.appendChild(arrow) + } else { + cell.classList.add('blocked-cell') + } + } else { + // Input cell + cell.classList.add('input-cell') + const input = document.createElement('input') + input.type = 'text' + input.maxLength = 1 + input.setAttribute('inputmode', 'text') + input.dataset.row = row + input.dataset.col = col + + const fontSize = Math.max(14, Math.min(22, cellSize * 0.5)) + input.style.fontSize = `${fontSize}px` + + const key = `${row}-${col}` + if (this.userAnswers[key]) { + input.value = this.userAnswers[key] + } + + cell.appendChild(input) + } + + return cell + } + + getClueAtPosition(row, col, words) { + for (const word of words) { + if (word.direction === 'horizontal' && word.startRow === row && word.startCol - 1 === col) { + return word + } + } + return null + } + + attachCellListeners() { + const inputs = this.gridElement.querySelectorAll('input') + + inputs.forEach(input => { + input.addEventListener('input', (e) => this.handleInput(e)) + input.addEventListener('focus', (e) => this.selectCell(e.target)) + }) + } + + handleInput(e) { + const input = e.target + const value = input.value.toUpperCase() + input.value = value + + const row = parseInt(input.dataset.row) + const col = parseInt(input.dataset.col) + const key = `${row}-${col}` + + this.userAnswers[key] = value + + if (value && value.length === 1) { + this.moveToNextCell(input) + } + + this.updateProgress() + } + + selectCell(input) { + document.querySelectorAll('.grid-cell.input-cell').forEach(cell => { + cell.classList.remove('active') + }) + input.parentElement.classList.add('active') + this.selectedCell = input + } + + moveToNextCell(currentInput) { + const inputs = Array.from(this.gridElement.querySelectorAll('input')) + const currentIndex = inputs.indexOf(currentInput) + + if (currentIndex !== -1 && currentIndex < inputs.length - 1) { + const nextInput = inputs[currentIndex + 1] + nextInput.focus() + } + } + + checkAnswers() { + let correct = 0 + let total = 0 + + const inputs = this.gridElement.querySelectorAll('input') + inputs.forEach(input => { + const row = parseInt(input.dataset.row) + const col = parseInt(input.dataset.col) + const userAnswer = input.value.toUpperCase() + const correctAnswer = this.getCorrectAnswer(row, col) + + if (correctAnswer) { + total++ + const cell = input.parentElement + if (userAnswer === correctAnswer) { + correct++ + cell.classList.add('correct') + cell.classList.remove('incorrect') + } else if (userAnswer) { + cell.classList.add('incorrect') + cell.classList.remove('correct') + } + } + }) + + const percentage = total > 0 ? Math.round((correct / total) * 100) : 0 + + if (percentage === 100) { + this.showSuccess() + } else { + this.showFeedback(`${correct}/${total} correct (${percentage}%)`) + } + } + + getCorrectAnswer(row, col) { + for (const word of this.currentPuzzle.words) { + if (word.direction === 'horizontal') { + if (word.startRow === row && col >= word.startCol && col < word.startCol + word.answer.length) { + return word.answer[col - word.startCol] + } + } else if (word.direction === 'vertical') { + if (word.startCol === col && row >= word.startRow && row < word.startRow + word.answer.length) { + return word.answer[row - word.startRow] + } + } + } + return null + } + + clearGrid() { + if (confirm('Alles wissen?')) { + this.userAnswers = {} + const inputs = this.gridElement.querySelectorAll('input') + inputs.forEach(input => { + input.value = '' + input.parentElement.classList.remove('correct', 'incorrect') + }) + this.updateProgress() + this.showFeedback('Gewist') + } + } + + showHintModal() { + if (this.hints <= 0) { + this.showFeedback('Geen hints meer!') + return + } + document.getElementById('hintModal').classList.add('active') + } + + hideHintModal() { + document.getElementById('hintModal').classList.remove('active') + } + + useHint() { + if (this.hints <= 0) return + + const inputs = Array.from(this.gridElement.querySelectorAll('input')) + const emptyInputs = inputs.filter(input => !input.value) + + if (emptyInputs.length === 0) { + this.showFeedback('Alles al ingevuld!') + this.hideHintModal() + return + } + + const randomInput = emptyInputs[Math.floor(Math.random() * emptyInputs.length)] + const row = parseInt(randomInput.dataset.row) + const col = parseInt(randomInput.dataset.col) + const correctAnswer = this.getCorrectAnswer(row, col) + + if (correctAnswer) { + randomInput.value = correctAnswer + this.userAnswers[`${row}-${col}`] = correctAnswer + + this.hints-- + this.updateUI() + this.updateProgress() + this.showFeedback('Letter onthuld!') + this.hideHintModal() + } + } + + showSuccess() { + this.coins += 50 + this.hints += 1 + this.updateUI() + document.getElementById('successModal').classList.add('active') + } + + nextPuzzle() { + document.getElementById('successModal').classList.remove('active') + this.currentLevel++ + this.loadPuzzle(this.currentLevel) + this.showFeedback(`Niveau ${this.currentLevel}`) + } + + skipPuzzle() { + if (confirm('Overslaan?')) { + this.currentLevel++ + this.loadPuzzle(this.currentLevel) + this.showFeedback('Overgeslagen') + } + } + + updateProgress() { + const inputs = this.gridElement.querySelectorAll('input') + let filled = 0 + let total = 0 + + inputs.forEach(input => { + total++ + if (input.value) filled++ + }) + + const progress = total > 0 ? Math.round((filled / total) * 100) : 0 + document.getElementById('progressFill').style.width = `${progress}%` + document.getElementById('progressText').textContent = `${progress}%` + } + + updateUI() { + document.getElementById('coinsCount').textContent = this.coins + document.getElementById('hintsCount').textContent = this.hints + document.getElementById('levelNum').textContent = this.currentLevel + } + + showFeedback(message) { + // Simple alert for mobile + const feedback = document.createElement('div') + feedback.style.cssText = ` + position: fixed; + top: 60px; + left: 50%; + transform: translateX(-50%); + background: rgba(0,0,0,0.8); + color: white; + padding: 10px 20px; + border-radius: 20px; + font-size: 14px; + z-index: 9999; + animation: fadeInOut 2s ease; + ` + feedback.textContent = message + document.body.appendChild(feedback) + + setTimeout(() => { + feedback.remove() + }, 2000) + } +} + +// Add fade animation +const style = document.createElement('style') +style.textContent = ` + @keyframes fadeInOut { + 0% { opacity: 0; transform: translateX(-50%) translateY(-10px); } + 20% { opacity: 1; transform: translateX(-50%) translateY(0); } + 80% { opacity: 1; transform: translateX(-50%) translateY(0); } + 100% { opacity: 0; transform: translateX(-50%) translateY(-10px); } + } +` +document.head.appendChild(style) + +// Initialize game +document.addEventListener('DOMContentLoaded', () => { + new MobileCrosswordGame() +}) diff --git a/public/redirect.js b/public/redirect.js new file mode 100644 index 0000000..30529a3 --- /dev/null +++ b/public/redirect.js @@ -0,0 +1,16 @@ +// Device Detection and Redirect +(function() { + // Check if we're already on the correct page + const currentPage = window.location.pathname.split('/').pop() || 'index.html' + + // Detect mobile device + const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) + || window.innerWidth < 768 + + // Only redirect if needed and not in an infinite loop + if (isMobile && !currentPage.includes('mobile.html')) { + window.location.href = 'mobile.html' + } else if (!isMobile && currentPage.includes('mobile.html')) { + window.location.href = 'index.html' + } +})()