This commit is contained in:
mike
2025-12-15 22:33:12 +01:00
commit 0c2e39749f
26 changed files with 7573 additions and 0 deletions

11
.editorconfig Normal file
View File

@@ -0,0 +1,11 @@
# editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

194
.gitattributes vendored Normal file
View File

@@ -0,0 +1,194 @@
## GITATTRIBUTES FOR WEB PROJECTS
#
# These settings are for any web project.
#
# Details per file setting:
# text These files should be normalized (i.e. convert CRLF to LF).
# binary These files are binary and should be left untouched.
#
# Note that binary is a macro for -text -diff.
######################################################################
## AUTO-DETECT
## Handle line endings automatically for files detected as
## text and leave all files detected as binary untouched.
## This will handle all files NOT defined below.
* text=auto
## SOURCE CODE
*.bat text eol=crlf
*.coffee text
*.css text
*.htm text
*.html text
*.inc text
*.ini text
*.js text
*.json text
*.jsx text
*.less text
*.od text
*.onlydata text
*.php text
*.pl text
*.py text
*.rb text
*.sass text
*.scm text
*.scss text
*.sh text eol=lf
*.sql text
*.styl text
*.tag text
*.ts text
*.tsx text
*.xml text
*.xhtml text
## DOCKER
*.dockerignore text
Dockerfile text
## DOCUMENTATION
*.markdown text
*.md text
*.mdwn text
*.mdown text
*.mkd text
*.mkdn text
*.mdtxt text
*.mdtext text
*.txt text
AUTHORS text
CHANGELOG text
CHANGES text
CONTRIBUTING text
COPYING text
copyright text
*COPYRIGHT* text
INSTALL text
license text
LICENSE text
NEWS text
readme text
*README* text
TODO text
## TEMPLATES
*.dot text
*.ejs text
*.haml text
*.handlebars text
*.hbs text
*.hbt text
*.jade text
*.latte text
*.mustache text
*.njk text
*.phtml text
*.tmpl text
*.tpl text
*.twig text
## LINTERS
.babelrc text
.csslintrc text
.eslintrc text
.htmlhintrc text
.jscsrc text
.jshintrc text
.jshintignore text
.prettierrc text
.stylelintrc text
## CONFIGS
*.bowerrc text
*.cnf text
*.conf text
*.config text
.browserslistrc text
.editorconfig text
.gitattributes text
.gitconfig text
.gitignore text
.htaccess text
*.npmignore text
*.yaml text
*.yml text
browserslist text
Makefile text
makefile text
## HEROKU
Procfile text
.slugignore text
## GRAPHICS
*.ai binary
*.bmp binary
*.eps binary
*.gif binary
*.ico binary
*.jng binary
*.jp2 binary
*.jpg binary
*.jpeg binary
*.jpx binary
*.jxr binary
*.pdf binary
*.png binary
*.psb binary
*.psd binary
*.svg text
*.svgz binary
*.tif binary
*.tiff binary
*.wbmp binary
*.webp binary
## AUDIO
*.kar binary
*.m4a binary
*.mid binary
*.midi binary
*.mp3 binary
*.ogg binary
*.ra binary
## VIDEO
*.3gpp binary
*.3gp binary
*.as binary
*.asf binary
*.asx binary
*.fla binary
*.flv binary
*.m4v binary
*.mng binary
*.mov binary
*.mp4 binary
*.mpeg binary
*.mpg binary
*.ogv binary
*.swc binary
*.swf binary
*.webm binary
## ARCHIVES
*.7z binary
*.gz binary
*.jar binary
*.rar binary
*.tar binary
*.zip binary
## FONTS
*.ttf binary
*.eot binary
*.otf binary
*.woff binary
*.woff2 binary
## EXECUTABLES
*.exe binary
*.pyc binary

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
# Include your project-specific ignores in this file
# Read about how to use .gitignore: https://help.github.com/articles/ignoring-files
# Useful .gitignore templates: https://github.com/github/gitignore
node_modules
dist
.cache
.idea

62
404.html Normal file
View File

@@ -0,0 +1,62 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Page Not Found</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* {
line-height: 1.2;
margin: 0;
}
html {
color: #888;
display: table;
font-family: sans-serif;
height: 100%;
text-align: center;
width: 100%;
}
body {
display: table-cell;
vertical-align: middle;
margin: 2em auto;
}
h1 {
color: #555;
font-size: 2em;
font-weight: 400;
}
p {
margin: 0 auto;
width: 280px;
}
@media only screen and (max-width: 280px) {
body,
p {
width: 95%;
}
h1 {
font-size: 1.5em;
margin: 0 0 0.3em;
}
}
</style>
</head>
<body>
<h1>Page Not Found</h1>
<p>Sorry, but the page you were trying to view does not exist.</p>
</body>
</html>
<!-- IE needs 512+ bytes: https://docs.microsoft.com/archive/blogs/ieinternals/friendly-http-error-pages -->

19
LICENSE.txt Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) HTML5 Boilerplate
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

481
crossword.js Normal file
View File

@@ -0,0 +1,481 @@
// Crossword Game Controller
class CrosswordGame {
constructor() {
this.currentPuzzle = null;
this.userAnswers = {};
this.selectedCell = null;
this.gridElement = document.getElementById('crosswordGrid');
this.progress = 0;
this.initializeEventListeners();
this.loadNewPuzzle();
}
initializeEventListeners() {
// New puzzle button
document.getElementById('newPuzzleBtn').addEventListener('click', () => {
this.loadNewPuzzle();
});
// Settings button
document.getElementById('settingsBtn').addEventListener('click', () => {
document.getElementById('settingsModal').style.display = 'block';
});
// Close modal
document.querySelector('.close').addEventListener('click', () => {
document.getElementById('settingsModal').style.display = 'none';
});
// Game control buttons
document.getElementById('checkAnswersBtn').addEventListener('click', () => {
this.checkAnswers();
});
document.getElementById('revealLetterBtn').addEventListener('click', () => {
this.revealLetter();
});
document.getElementById('clearGridBtn').addEventListener('click', () => {
this.clearGrid();
});
// Settings changes
document.getElementById('difficultySelect').addEventListener('change', (e) => {
this.loadNewPuzzle(e.target.value);
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
this.handleKeyboardNavigation(e);
});
}
async loadNewPuzzle(difficulty = 'medium') {
try {
this.showLoading();
this.currentPuzzle = await PuzzleDatabase.getPuzzle(difficulty);
this.renderGrid();
this.userAnswers = {};
this.updateProgress();
} catch (error) {
console.error('Error loading puzzle:', error);
this.showError('Er is een fout opgetreden bij het laden van de puzzel.');
}
}
renderGrid() {
if (!this.currentPuzzle) return;
const grid = this.currentPuzzle.grid;
const words = this.currentPuzzle.words;
this.gridElement.innerHTML = '';
this.gridElement.style.gridTemplateColumns = `repeat(${grid[0].length}, 1fr)`;
// Create grid cells
for (let row = 0; row < grid.length; row++) {
for (let col = 0; col < grid[row].length; col++) {
const cell = grid[row][row];
const cellElement = this.createCellElement(cell, row, col, words);
this.gridElement.appendChild(cellElement);
}
}
// Add event listeners to input cells
this.attachInputListeners();
}
createCellElement(cellType, row, col, words) {
const cellDiv = document.createElement('div');
cellDiv.className = 'grid-cell';
cellDiv.dataset.row = row;
cellDiv.dataset.col = col;
switch (cellType) {
case '#':
cellDiv.classList.add('blocked-cell');
break;
case 'C':
case 'L':
case 'U':
case 'E':
// This is a clue cell (example from template)
const clueData = this.findClueForPosition(row, col, words);
if (clueData) {
cellDiv.classList.add('clue-cell', clueData.direction);
cellDiv.innerHTML = `
<div class="clue-text">${clueData.clue}</div>
<div class="arrow ${clueData.direction === 'horizontal' ? 'arrow-right' : 'arrow-down'}">
${clueData.direction === 'horizontal' ? '→' : '↓'}
</div>
`;
}
break;
default:
// This should be an input cell
cellDiv.classList.add('input-cell');
const input = document.createElement('input');
input.type = 'text';
input.maxLength = 1;
input.dataset.row = row;
input.dataset.col = col;
// Add existing answer if available
const key = `${row}-${col}`;
if (this.userAnswers[key]) {
input.value = this.userAnswers[key];
}
cellDiv.appendChild(input);
}
return cellDiv;
}
findClueForPosition(row, col, words) {
for (const word of words) {
if (word.startRow === row && word.startCol === col) {
return {
clue: word.clue,
direction: word.direction
};
}
}
return null;
}
attachInputListeners() {
const inputs = this.gridElement.querySelectorAll('input');
inputs.forEach(input => {
input.addEventListener('input', (e) => {
this.handleInput(e);
});
input.addEventListener('focus', (e) => {
this.selectCell(e.target);
});
input.addEventListener('click', (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;
// Move to next cell automatically
if (value && value.length === 1) {
this.moveToNextCell(input);
}
this.updateProgress();
}
selectCell(input) {
// Remove previous selection
document.querySelectorAll('.grid-cell').forEach(cell => {
cell.classList.remove('active');
});
// Add selection to current cell
input.parentElement.classList.add('active');
this.selectedCell = input;
// Highlight related word
this.highlightWord(input);
}
highlightWord(input) {
// Remove previous highlights
document.querySelectorAll('.grid-cell').forEach(cell => {
cell.classList.remove('highlighted');
});
// Find and highlight the entire word
const row = parseInt(input.dataset.row);
const col = parseInt(input.dataset.col);
// This is a simplified version - in a real app you'd track word boundaries
const word = this.findWordAtPosition(row, col);
if (word) {
// Highlight all cells in this word
// Implementation depends on your word tracking system
}
}
moveToNextCell(currentInput) {
const row = parseInt(currentInput.dataset.row);
const col = parseInt(currentInput.dataset.col);
// Try to find next input cell in row first
let nextInput = null;
const currentCell = currentInput.parentElement;
const nextCell = currentCell.nextElementSibling;
if (nextCell && nextCell.querySelector('input')) {
nextInput = nextCell.querySelector('input');
} else {
// Move to next row
const currentRow = currentCell.parentElement;
const nextRow = currentRow.nextElementSibling;
if (nextRow) {
const firstInput = nextRow.querySelector('input');
if (firstInput) {
nextInput = firstInput;
}
}
}
if (nextInput) {
nextInput.focus();
nextInput.select();
}
}
handleKeyboardNavigation(e) {
if (!this.selectedCell) return;
const row = parseInt(this.selectedCell.dataset.row);
const col = parseInt(this.selectedCell.dataset.col);
let targetRow = row;
let targetCol = col;
switch (e.key) {
case 'ArrowUp':
targetRow = Math.max(0, row - 1);
break;
case 'ArrowDown':
targetRow = Math.min(this.currentPuzzle.grid.length - 1, row + 1);
break;
case 'ArrowLeft':
targetCol = Math.max(0, col - 1);
break;
case 'ArrowRight':
targetCol = Math.min(this.currentPuzzle.grid[0].length - 1, col + 1);
break;
case 'Backspace':
this.selectedCell.value = '';
delete this.userAnswers[`${row}-${col}`];
this.updateProgress();
break;
default:
return;
}
e.preventDefault();
const targetInput = this.gridElement.querySelector(`input[data-row="${targetRow}"][data-col="${targetCol}"]`);
if (targetInput) {
targetInput.focus();
targetInput.select();
}
}
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 key = `${row}-${col}`;
const userAnswer = input.value.toUpperCase();
// Find the correct answer for this position
const correctAnswer = this.findCorrectAnswer(row, col);
if (correctAnswer) {
total++;
if (userAnswer === correctAnswer) {
correct++;
input.parentElement.classList.add('correct');
input.parentElement.classList.remove('incorrect');
} else if (userAnswer) {
input.parentElement.classList.add('incorrect');
input.parentElement.classList.remove('correct');
}
}
});
const percentage = total > 0 ? Math.round((correct / total) * 100) : 0;
// Show feedback
if (percentage === 100) {
this.showSuccess('Gefeliciteerd! Alle antwoorden zijn correct!');
} else {
this.showFeedback(`Je hebt ${correct} van de ${total} letters correct (${percentage}%).`);
}
}
findCorrectAnswer(row, col) {
// Find the correct letter for this position
for (const word of this.currentPuzzle.words) {
if (word.direction === 'horizontal') {
if (word.startRow === row && col >= word.startCol && col < word.startCol + word.answer.length) {
const letterIndex = col - word.startCol;
return word.answer[letterIndex];
}
} else if (word.direction === 'vertical') {
if (word.startCol === col && row >= word.startRow && row < word.startRow + word.answer.length) {
const letterIndex = row - word.startRow;
return word.answer[letterIndex];
}
}
}
return null;
}
revealLetter() {
if (!this.selectedCell) {
this.showError('Selecteer eerst een cel om een letter te onthullen.');
return;
}
const row = parseInt(this.selectedCell.dataset.row);
const col = parseInt(this.selectedCell.dataset.col);
const correctAnswer = this.findCorrectAnswer(row, col);
if (correctAnswer) {
this.selectedCell.value = correctAnswer;
const key = `${row}-${col}`;
this.userAnswers[key] = correctAnswer;
this.updateProgress();
this.showFeedback('Letter onthuld!');
}
}
clearGrid() {
if (confirm('Weet je zeker dat je het hele raster wilt wissen?')) {
const inputs = this.gridElement.querySelectorAll('input');
inputs.forEach(input => {
input.value = '';
input.parentElement.classList.remove('correct', 'incorrect');
});
this.userAnswers = {};
this.updateProgress();
this.showFeedback('Raster gewist.');
}
}
updateProgress() {
const inputs = this.gridElement.querySelectorAll('input');
let filled = 0;
let total = 0;
inputs.forEach(input => {
const row = parseInt(input.dataset.row);
const col = parseInt(input.dataset.col);
const correctAnswer = this.findCorrectAnswer(row, col);
if (correctAnswer) {
total++;
if (input.value) {
filled++;
}
}
});
this.progress = total > 0 ? Math.round((filled / total) * 100) : 0;
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
progressFill.style.width = `${this.progress}%`;
progressText.textContent = `${this.progress}% voltooid`;
if (this.progress === 100) {
this.checkAnswers();
}
}
showLoading() {
this.gridElement.innerHTML = '<div style="text-align: center; padding: 50px;"><div class="loading"></div><p>Laden...</p></div>';
}
showError(message) {
this.showFeedback(message, 'error');
}
showSuccess(message) {
this.showFeedback(message, 'success');
}
showFeedback(message, type = 'info') {
// Create feedback element
const feedback = document.createElement('div');
feedback.className = `feedback feedback-${type}`;
feedback.textContent = message;
// Style the feedback
feedback.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 8px;
color: white;
font-weight: 600;
z-index: 1000;
animation: slideIn 0.3s ease;
max-width: 300px;
word-wrap: break-word;
`;
// Set background color based on type
switch (type) {
case 'error':
feedback.style.background = 'linear-gradient(135deg, #f56565, #e53e3e)';
break;
case 'success':
feedback.style.background = 'linear-gradient(135deg, #48bb78, #38a169)';
break;
default:
feedback.style.background = 'linear-gradient(135deg, #4299e1, #3182ce)';
}
document.body.appendChild(feedback);
// Remove after 3 seconds
setTimeout(() => {
feedback.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
if (feedback.parentNode) {
feedback.parentNode.removeChild(feedback);
}
}, 300);
}, 3000);
}
}
// Add CSS animations for feedback
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
// Initialize the game when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
const game = new CrosswordGame();
});

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

820
gameLogic.js Normal file
View File

@@ -0,0 +1,820 @@
// Kruiswoord Pro Game Logic - Fixed Version with Tablet Support and Correct Hints
class KruiswoordProGame {
constructor() {
this.currentLevel = 1;
this.coins = 100;
this.hints = 3;
this.currentPuzzle = null;
this.userAnswers = {};
this.selectedCell = null;
this.gridElement = document.getElementById('crosswordGrid');
this.isTablet = window.innerWidth >= 768;
this.initializeGame();
this.setupEventListeners();
this.loadPuzzle(this.currentLevel);
// Handle window resize for tablet/desktop
window.addEventListener('resize', () => {
this.handleResize();
});
}
handleResize() {
const wasTablet = this.isTablet;
this.isTablet = window.innerWidth >= 768;
if (wasTablet !== this.isTablet && this.currentPuzzle) {
this.renderGrid(); // Re-render for different screen size
}
}
initializeGame() {
this.updateUI();
this.setupTabletStyles();
}
setupTabletStyles() {
if (this.isTablet) {
document.body.classList.add('tablet-mode');
} else {
document.body.classList.remove('tablet-mode');
}
}
setupEventListeners() {
// Menu controls
document.getElementById('menuBtn').addEventListener('click', () => {
document.getElementById('sideMenu').classList.add('active');
});
document.getElementById('closeMenu').addEventListener('click', () => {
document.getElementById('sideMenu').classList.remove('active');
});
// Game controls
document.getElementById('checkBtn').addEventListener('click', () => {
this.checkAnswers();
});
document.getElementById('clearBtn').addEventListener('click', () => {
this.clearGrid();
});
document.getElementById('revealLetterBtn').addEventListener('click', () => {
this.showHintModal();
});
document.getElementById('skipBtn').addEventListener('click', () => {
this.skipPuzzle();
});
// Hint modal
document.getElementById('useHintBtn').addEventListener('click', () => {
this.useHint();
});
document.getElementById('cancelHintBtn').addEventListener('click', () => {
this.hideHintModal();
});
// Success modal
document.getElementById('nextPuzzleBtn').addEventListener('click', () => {
this.nextPuzzle();
});
document.getElementById('closeModalBtn').addEventListener('click', () => {
this.hideSuccessModal();
});
// Menu items
document.getElementById('dailyPuzzle').addEventListener('click', () => {
this.loadDailyPuzzle();
});
document.getElementById('levelSelect').addEventListener('click', () => {
this.showLevelSelect();
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
this.handleKeyboardInput(e);
});
}
// FIXED: Better Dutch puzzles with correct hint placement
getDutchPuzzles() {
return {
1: {
grid: [
['H', 'A', 'A', 'S'],
['#', '#', '#', '#'],
['V', 'L', 'I', 'E', 'G'],
['#', '#', '#', '#', '#']
],
words: [
{
word: 'HAAS',
clue: 'Snel dier →',
startRow: 0,
startCol: 0,
direction: 'horizontal',
answer: 'HAAS'
},
{
word: 'VLIEG',
clue: 'Insect →',
startRow: 2,
startCol: 0,
direction: 'horizontal',
answer: 'VLIEG'
}
],
difficulty: 1
},
2: {
grid: [
['B', 'O', 'M', 'E', 'N'],
['#', '#', '#', '#', '#'],
['H', 'O', 'N', 'D', 'E'],
['#', '#', '#', '#', '#']
],
words: [
{
word: 'BOMEN',
clue: 'Planten →',
startRow: 0,
startCol: 0,
direction: 'horizontal',
answer: 'BOMEN'
},
{
word: 'HONDE',
clue: 'Huisdieren →',
startRow: 2,
startCol: 0,
direction: 'horizontal',
answer: 'HONDE'
}
],
difficulty: 2
},
3: {
grid: [
['R', 'E', 'G', 'E', 'N'],
['#', '#', '#', '#', '#'],
['S', 'N', 'E', 'E', 'U'],
['#', '#', '#', '#', '#']
],
words: [
{
word: 'REGEN',
clue: 'Valt uit de lucht →',
startRow: 0,
startCol: 0,
direction: 'horizontal',
answer: 'REGEN'
},
{
word: 'SNEEU',
clue: 'Witte vlokken →',
startRow: 2,
startCol: 0,
direction: 'horizontal',
answer: 'SNEEU'
}
],
difficulty: 3
},
// Tablet-optimized larger puzzles
4: {
grid: [
['H', 'A', 'A', 'S', '#', '#'],
['#', '#', '#', 'T', '#', '#'],
['V', 'L', 'I', 'E', 'G', '#'],
['#', '#', '#', 'S', '#', '#'],
['#', '#', '#', '#', '#', '#'],
['#', '#', '#', '#', '#', '#']
],
words: [
{
word: 'HAAS',
clue: 'Snel dier →',
startRow: 0,
startCol: 0,
direction: 'horizontal',
answer: 'HAAS'
},
{
word: 'VLIEG',
clue: 'Insect →',
startRow: 2,
startCol: 0,
direction: 'horizontal',
answer: 'VLIEG'
}
],
difficulty: 2,
tabletOptimized: true
}
};
}
loadPuzzle(level) {
const puzzles = this.getDutchPuzzles();
this.currentPuzzle = puzzles[level] || puzzles[1];
this.renderGrid();
this.userAnswers = {};
this.updateProgress();
}
renderGrid() {
if (!this.currentPuzzle) return;
const grid = this.currentPuzzle.grid;
const words = this.currentPuzzle.words;
this.gridElement.innerHTML = '';
// Set grid dimensions
const rows = grid.length;
const cols = Math.max(...grid.map(row => row.length));
// FIXED: Tablet-responsive sizing
const cellSize = this.isTablet ? (window.innerWidth > 1024 ? 60 : 50) : 45;
this.gridElement.style.gridTemplateColumns = `repeat(${cols}, ${cellSize}px)`;
this.gridElement.style.gridTemplateRows = `repeat(${rows}, ${cellSize}px)`;
this.gridElement.style.gap = '2px';
this.gridElement.style.justifyContent = 'center';
// Create grid cells
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const cellValue = grid[row] && grid[row][col] ? grid[row][col] : '#';
const cellElement = this.createCellElement(cellValue, row, col, words, cellSize);
this.gridElement.appendChild(cellElement);
}
}
this.attachCellListeners();
}
createCellElement(cellValue, row, col, words, cellSize) {
const cellDiv = document.createElement('div');
cellDiv.className = 'grid-cell';
cellDiv.dataset.row = row;
cellDiv.dataset.col = col;
// FIXED: Set consistent sizing
cellDiv.style.width = `${cellSize}px`;
cellDiv.style.height = `${cellSize}px`;
if (cellValue === '#') {
// Blocked cell
cellDiv.classList.add('blocked-cell');
} else if (this.isClueCell(row, col, words)) {
// Clue cell
const clueData = this.getClueForPosition(row, col, words);
cellDiv.classList.add('clue-cell', clueData.direction);
// FIXED: Better clue text sizing for tablets
const clueText = document.createElement('div');
clueText.className = 'clue-text';
clueText.textContent = clueData.clue;
clueText.style.fontSize = this.isTablet ? '10px' : '8px';
const arrow = document.createElement('div');
arrow.className = `arrow arrow-${clueData.direction === 'horizontal' ? 'right' : 'down'}`;
arrow.textContent = clueData.direction === 'horizontal' ? '→' : '↓';
arrow.style.fontSize = this.isTablet ? '16px' : '14px';
cellDiv.appendChild(clueText);
cellDiv.appendChild(arrow);
} else {
// FIXED: Input cell - only create if it's part of a word
if (this.isPartOfWord(row, col, words)) {
cellDiv.classList.add('input-cell');
const input = document.createElement('input');
input.type = 'text';
input.maxLength = 1;
input.dataset.row = row;
input.dataset.col = col;
// FIXED: Responsive font sizing
input.style.fontSize = this.isTablet ? '24px' : '20px';
// Add existing answer if available
const key = `${row}-${col}`;
if (this.userAnswers[key]) {
input.value = this.userAnswers[key];
}
cellDiv.appendChild(input);
} else {
// Empty cell that's not part of any word
cellDiv.classList.add('blocked-cell');
}
}
return cellDiv;
}
// FIXED: Check if cell is part of any word
isPartOfWord(row, col, words) {
for (const word of words) {
if (word.direction === 'horizontal') {
if (row === word.startRow && col >= word.startCol && col < word.startCol + word.answer.length) {
return true;
}
} else if (word.direction === 'vertical') {
if (col === word.startCol && row >= word.startRow && row < word.startRow + word.answer.length) {
return true;
}
}
}
return false;
}
isClueCell(row, col, words) {
return words.some(word => word.startRow === row && word.startCol === col);
}
getClueForPosition(row, col, words) {
for (const word of words) {
if (word.startRow === row && word.startCol === col) {
return {
clue: word.clue,
direction: word.direction
};
}
}
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);
});
input.addEventListener('click', (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;
// FIXED: Smart navigation to next cell in the same word
if (value && value.length === 1) {
this.moveToNextInWord(input, row, col);
}
this.updateProgress();
}
// FIXED: Navigate to next cell in the same word, not just any next input
moveToNextInWord(currentInput, row, col) {
const currentWord = this.findWordContainingCell(row, col);
if (!currentWord) {
this.moveToNextCell(currentInput);
return;
}
let nextRow = row;
let nextCol = col;
if (currentWord.direction === 'horizontal') {
nextCol++;
} else {
nextRow++;
}
// Check if next position is still in the same word
const nextInput = this.gridElement.querySelector(`input[data-row="${nextRow}"][data-col="${nextCol}"]`);
if (nextInput && this.isPartOfWord(nextRow, nextCol, this.currentPuzzle.words)) {
nextInput.focus();
nextInput.select();
} else {
// End of word, move to next available input
this.moveToNextCell(currentInput);
}
}
findWordContainingCell(row, col) {
for (const word of this.currentPuzzle.words) {
if (word.direction === 'horizontal') {
if (row === word.startRow && col >= word.startCol && col < word.startCol + word.answer.length) {
return word;
}
} else if (word.direction === 'vertical') {
if (col === word.startCol && row >= word.startRow && row < word.startRow + word.answer.length) {
return word;
}
}
}
return null;
}
selectCell(input) {
// Remove previous selection
document.querySelectorAll('.grid-cell').forEach(cell => {
cell.classList.remove('active');
});
// Add selection to current cell
input.parentElement.classList.add('active');
this.selectedCell = input;
}
// FIXED: Better navigation that respects word boundaries
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();
nextInput.select();
}
}
handleKeyboardInput(e) {
if (!this.selectedCell) return;
const inputs = Array.from(this.gridElement.querySelectorAll('input'));
const currentIndex = inputs.indexOf(this.selectedCell);
if (currentIndex === -1) return;
let targetIndex = currentIndex;
switch (e.key) {
case 'ArrowUp':
const currentRow = parseInt(this.selectedCell.dataset.row);
const currentCol = parseInt(this.selectedCell.dataset.col);
const targetInput = this.findInputAbove(currentRow, currentCol);
if (targetInput) {
targetInput.focus();
targetInput.select();
}
break;
case 'ArrowDown':
const targetInputDown = this.findInputBelow(currentRow, currentCol);
if (targetInputDown) {
targetInputDown.focus();
targetInputDown.select();
}
break;
case 'ArrowLeft':
if (currentIndex > 0) {
targetIndex--;
inputs[targetIndex].focus();
inputs[targetIndex].select();
}
break;
case 'ArrowRight':
if (currentIndex < inputs.length - 1) {
targetIndex++;
inputs[targetIndex].focus();
inputs[targetIndex].select();
}
break;
case 'Backspace':
this.selectedCell.value = '';
const row = parseInt(this.selectedCell.dataset.row);
const col = parseInt(this.selectedCell.dataset.col);
delete this.userAnswers[`${row}-${col}`];
this.updateProgress();
break;
}
}
findInputAbove(row, col) {
for (let r = row - 1; r >= 0; r--) {
const input = this.gridElement.querySelector(`input[data-row="${r}"][data-col="${col}"]`);
if (input) return input;
}
return null;
}
findInputBelow(row, col) {
const maxRow = Math.max(...Array.from(this.gridElement.querySelectorAll('input')).map(input => parseInt(input.dataset.row)));
for (let r = row + 1; r <= maxRow; r++) {
const input = this.gridElement.querySelector(`input[data-row="${r}"][data-col="${col}"]`);
if (input) return input;
}
return null;
}
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 key = `${row}-${col}`;
const userAnswer = input.value.toUpperCase();
const correctAnswer = this.findCorrectAnswer(row, col);
if (correctAnswer) {
total++;
if (userAnswer === correctAnswer) {
correct++;
input.parentElement.classList.add('correct');
input.parentElement.classList.remove('incorrect');
} else if (userAnswer) {
input.parentElement.classList.add('incorrect');
input.parentElement.classList.remove('correct');
}
}
});
const percentage = total > 0 ? Math.round((correct / total) * 100) : 0;
if (percentage === 100) {
this.showSuccess();
} else {
this.showFeedback(`Je hebt ${correct} van de ${total} letters correct (${percentage}%)`);
}
}
findCorrectAnswer(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) {
const letterIndex = col - word.startCol;
return word.answer[letterIndex];
}
} else if (word.direction === 'vertical') {
if (word.startCol === col && row >= word.startRow && row < word.startRow + word.answer.length) {
const letterIndex = row - word.startRow;
return word.answer[letterIndex];
}
}
}
return null;
}
clearGrid() {
if (confirm('Weet je zeker dat je alle ingevulde letters wilt wissen?')) {
const inputs = this.gridElement.querySelectorAll('input');
inputs.forEach(input => {
input.value = '';
input.parentElement.classList.remove('correct', 'incorrect');
});
this.userAnswers = {};
this.updateProgress();
this.showFeedback('Raster gewist');
}
}
showHintModal() {
if (this.hints <= 0) {
this.showFeedback('Je hebt geen hints meer. Verdien hints door puzzels op te lossen!', 'error');
return;
}
document.getElementById('hintModal').classList.add('active');
}
hideHintModal() {
document.getElementById('hintModal').classList.remove('active');
}
useHint() {
if (this.hints <= 0) return;
const inputs = this.gridElement.querySelectorAll('input');
const emptyInputs = Array.from(inputs).filter(input => !input.value);
if (emptyInputs.length === 0) {
this.showFeedback('Alle letters zijn al ingevuld!', 'info');
this.hideHintModal();
return;
}
// Randomly select an empty cell
const randomInput = emptyInputs[Math.floor(Math.random() * emptyInputs.length)];
const row = parseInt(randomInput.dataset.row);
const col = parseInt(randomInput.dataset.col);
const correctAnswer = this.findCorrectAnswer(row, col);
if (correctAnswer) {
randomInput.value = correctAnswer;
const key = `${row}-${col}`;
this.userAnswers[key] = correctAnswer;
this.hints--;
this.updateUI();
this.updateProgress();
this.showFeedback('Letter onthuld!');
this.hideHintModal();
// Check if puzzle is complete
this.checkIfComplete();
}
}
checkIfComplete() {
const inputs = this.gridElement.querySelectorAll('input');
let allFilled = true;
inputs.forEach(input => {
if (!input.value) {
allFilled = false;
}
});
if (allFilled) {
setTimeout(() => this.checkAnswers(), 500);
}
}
showSuccess() {
// Award rewards
this.coins += 50;
this.hints += 1;
this.updateUI();
document.getElementById('successModal').classList.add('active');
}
hideSuccessModal() {
document.getElementById('successModal').classList.remove('active');
}
nextPuzzle() {
this.hideSuccessModal();
this.currentLevel++;
this.loadPuzzle(this.currentLevel);
this.showFeedback(`Niveau ${this.currentLevel} geladen!`);
}
skipPuzzle() {
if (confirm('Weet je zeker dat je deze puzzel wilt overslaan?')) {
this.currentLevel++;
this.loadPuzzle(this.currentLevel);
this.showFeedback('Puzzel overgeslagen');
}
}
loadDailyPuzzle() {
this.showFeedback('Dagelijkse puzzel wordt geladen...');
// Implement daily puzzle logic
document.getElementById('sideMenu').classList.remove('active');
}
showLevelSelect() {
this.showFeedback('Niveau selectie komt binnenkort!');
document.getElementById('sideMenu').classList.remove('active');
}
updateProgress() {
const inputs = this.gridElement.querySelectorAll('input');
let filled = 0;
let total = 0;
inputs.forEach(input => {
const row = parseInt(input.dataset.row);
const col = parseInt(input.dataset.col);
const correctAnswer = this.findCorrectAnswer(row, col);
if (correctAnswer) {
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}% voltooid`;
}
updateUI() {
document.getElementById('coinsCount').textContent = this.coins;
document.querySelector('.hint-count').textContent = this.hints;
document.querySelector('.level-number').textContent = `Niveau ${this.currentLevel}`;
}
showFeedback(message, type = 'info') {
const feedback = document.createElement('div');
feedback.className = `feedback feedback-${type}`;
feedback.textContent = message;
feedback.style.cssText = `
position: fixed;
top: 100px;
right: 20px;
padding: 15px 20px;
border-radius: 8px;
color: white;
font-weight: 600;
z-index: 1000;
animation: slideIn 0.3s ease;
max-width: 300px;
word-wrap: break-word;
`;
switch (type) {
case 'error':
feedback.style.background = 'linear-gradient(135deg, #f56565, #e53e3e)';
break;
case 'success':
feedback.style.background = 'linear-gradient(135deg, #48bb78, #38a169)';
break;
default:
feedback.style.background = 'linear-gradient(135deg, #4299e1, #3182ce)';
}
document.body.appendChild(feedback);
setTimeout(() => {
feedback.style.animation = 'slideOut 0.3s ease';
setTimeout(() => {
if (feedback.parentNode) {
feedback.parentNode.removeChild(feedback);
}
}, 300);
}, 3000);
}
}
// Add CSS animations and tablet styles
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
/* Tablet Mode Styles */
.tablet-mode .grid-container {
max-width: 90vw;
margin: 0 auto;
}
.tablet-mode .puzzle-section {
padding: 30px;
}
@media (min-width: 768px) {
.app-container {
max-width: 100vw;
}
.game-main {
padding: 30px;
}
.bottom-controls {
max-width: 600px;
margin: 0 auto;
}
}
@media (min-width: 1024px) {
.grid-container {
transform: scale(1.1);
transform-origin: center;
}
.puzzle-section {
min-height: 70vh;
}
}
`;
document.head.appendChild(style);
// Initialize game when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
const game = new KruiswoordProGame();
});

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

1
icon.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 192 192"><path fill="#e08524" d="M75.3 73.4H18.4l45.3 34.3L48.3 163l46.1-32.3 48.2 34.6-16.9-58.3 44.9-33.6H115l-20.5-55-19.2 55z"/><path d="m96.7 18.8 18.2 8.2 16.5 44.3h-15.1L96.7 18.8zm-47 146 18.7 9.9 42.6-29.9-16.5-11.4-44.8 31.4zm79.1-56.8 17.4 9.4 18.6 60.1-19.7-11.3-16.3-58.2z"/><path d="m173.1 74.3 17.8 9.2-44.7 34-17.4-9.4 44.3-33.8z"/></svg>

After

Width:  |  Height:  |  Size: 429 B

0
img/.gitkeep Normal file
View File

169
index.html Normal file
View File

@@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kruiswoord Pro: Zweedse puzzel</title>
<link rel="stylesheet" href="styles.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="app-container">
<!-- Top Header Bar -->
<header class="top-header">
<div class="header-left">
<button class="header-btn menu-btn" id="menuBtn">
<i class="fas fa-bars"></i>
</button>
<div class="level-info">
<span class="level-number">Niveau 1</span>
<div class="difficulty-stars">
<i class="fas fa-star"></i>
<i class="fas fa-star"></i>
<i class="far fa-star"></i>
</div>
</div>
</div>
<div class="header-center">
<h1 class="app-title">Kruiswoord Pro</h1>
</div>
<div class="header-right">
<div class="coins-display">
<i class="fas fa-coins"></i>
<span id="coinsCount">100</span>
</div>
<button class="header-btn hints-btn" id="hintsBtn">
<i class="fas fa-lightbulb"></i>
<span class="hint-count">3</span>
</button>
</div>
</header>
<!-- Progress Bar -->
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<span class="progress-text" id="progressText">0% voltooid</span>
</div>
<!-- Main Game Area -->
<main class="game-main">
<!-- Crossword Grid Container -->
<div class="puzzle-section">
<div class="grid-container" id="crosswordGrid">
<!-- Grid will be generated by JavaScript -->
</div>
</div>
<!-- Bottom Controls -->
<div class="bottom-controls">
<button class="control-btn" id="checkBtn">
<i class="fas fa-check-circle"></i>
<span>Controleren</span>
</button>
<button class="control-btn" id="clearBtn">
<i class="fas fa-eraser"></i>
<span>Wissen</span>
</button>
<button class="control-btn hint-btn" id="revealLetterBtn">
<i class="fas fa-eye"></i>
<span>Letter</span>
</button>
<button class="control-btn" id="skipBtn">
<i class="fas fa-forward"></i>
<span>Overslaan</span>
</button>
</div>
</main>
<!-- Side Menu -->
<div class="side-menu" id="sideMenu">
<div class="menu-header">
<h3>Menu</h3>
<button class="close-menu" id="closeMenu">
<i class="fas fa-times"></i>
</button>
</div>
<div class="menu-items">
<button class="menu-item" id="dailyPuzzle">
<i class="fas fa-calendar-day"></i>
<span>Dagelijkse Puzzel</span>
<span class="bonus-indicator">+5</span>
</button>
<button class="menu-item" id="levelSelect">
<i class="fas fa-layer-group"></i>
<span>Kies Niveau</span>
</button>
<button class="menu-item" id="statistics">
<i class="fas fa-chart-bar"></i>
<span>Statistieken</span>
</button>
<button class="menu-item" id="settings">
<i class="fas fa-cog"></i>
<span>Instellingen</span>
</button>
<button class="menu-item" id="help">
<i class="fas fa-question-circle"></i>
<span>Hulp</span>
</button>
</div>
</div>
<!-- Success Modal -->
<div class="modal" id="successModal">
<div class="modal-content success-modal">
<div class="success-icon">
<i class="fas fa-trophy"></i>
</div>
<h2>Gefeliciteerd!</h2>
<p>Je hebt de puzzel opgelost!</p>
<div class="reward-info">
<div class="reward-item">
<i class="fas fa-star"></i>
<span>+3 Sterren</span>
</div>
<div class="reward-item">
<i class="fas fa-coins"></i>
<span>+50 Munten</span>
</div>
</div>
<div class="modal-buttons">
<button class="btn btn-primary" id="nextPuzzleBtn">Volgende Puzzel</button>
<button class="btn btn-secondary" id="closeModalBtn">Sluiten</button>
</div>
</div>
</div>
<!-- Hint Modal -->
<div class="modal" id="hintModal">
<div class="modal-content hint-modal">
<h3>Hint Gebruiken</h3>
<p>Wil je een hint gebruiken om een letter te onthullen?</p>
<div class="hint-cost">
<i class="fas fa-lightbulb"></i>
<span>Kosten: 1 hint</span>
</div>
<div class="modal-buttons">
<button class="btn btn-primary" id="useHintBtn">Gebruiken</button>
<button class="btn btn-secondary" id="cancelHintBtn">Annuleren</button>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script src="puzzleData.js"></script>
<script src="gameLogic.js"></script>
</body>
</html>

0
js/app.js Normal file
View File

0
js/vendor/.gitkeep vendored Normal file
View File

4203
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": " ",
"version": "0.0.1",
"description": "",
"private": true,
"keywords": [
""
],
"license": "",
"author": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack serve --open --config webpack.config.dev.js",
"build": "webpack --config webpack.config.prod.js"
},
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
"html-webpack-plugin": "^5.6.0",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"webpack-merge": "^5.10.0"
}
}

197
puzzleData.js Normal file
View File

@@ -0,0 +1,197 @@
// Dutch Swedish-style Crossword Puzzles Database
const DUTCH_PUZZLES = {
// Level 1 - Easy
level1: {
grid: [
['H', 'A', 'A', 'S'],
['#', '#', '#', 'T'],
['V', 'L', 'I', 'E', 'G'],
['#', '#', '#', 'S']
],
words: [
{
word: 'HAAS',
clue: 'Snel dier',
startRow: 0,
startCol: 0,
direction: 'horizontal',
answer: 'HAAS'
},
{
word: 'VLIEG',
clue: 'Insect',
startRow: 2,
startCol: 0,
direction: 'horizontal',
answer: 'VLIEG'
},
{
word: 'AT',
clue: 'Kleine woord',
startRow: 0,
startCol: 2,
direction: 'vertical',
answer: 'AT'
}
],
difficulty: 1,
rewards: { coins: 50, stars: 3, hints: 1 }
},
// Level 2 - Medium Easy
level2: {
grid: [
['B', 'O', 'M', 'E', 'N'],
['#', '#', '#', '#', 'D'],
['H', 'O', 'N', 'D', 'E'],
['#', '#', '#', '#', 'R']
],
words: [
{
word: 'BOMEN',
clue: 'Planten',
startRow: 0,
startCol: 0,
direction: 'horizontal',
answer: 'BOMEN'
},
{
word: 'HONDE',
clue: 'Huisdieren',
startRow: 2,
startCol: 0,
direction: 'horizontal',
answer: 'HONDE'
},
{
word: 'NE',
clue: 'Nee afkorting',
startRow: 0,
startCol: 4,
direction: 'vertical',
answer: 'NE'
}
],
difficulty: 2,
rewards: { coins: 75, stars: 3, hints: 1 }
},
// Level 3 - Weather Theme
level3: {
grid: [
['R', 'E', 'G', 'E', 'N'],
['#', '#', '#', '#', 'B'],
['S', 'N', 'E', 'E', 'U'],
['#', '#', '#', '#', 'W']
],
words: [
{
word: 'REGEN',
clue: 'Valt uit de lucht',
startRow: 0,
startCol: 0,
direction: 'horizontal',
answer: 'REGEN'
},
{
word: 'SNEEU',
clue: 'Witte vlokken',
startRow: 2,
startCol: 0,
direction: 'horizontal',
answer: 'SNEEU'
},
{
word: 'NB',
clue: 'Nota bene',
startRow: 0,
startCol: 4,
direction: 'vertical',
answer: 'NB'
}
],
difficulty: 3,
rewards: { coins: 100, stars: 3, hints: 1 }
},
// Daily Puzzle Template
daily: {
grid: [
['Z', 'O', 'N', 'N', 'E'],
['#', '#', '#', '#', 'D'],
['M', 'A', 'A', 'N', 'D'],
['#', '#', '#', '#', 'G']
],
words: [
{
word: 'ZONNE',
clue: 'Daglicht',
startRow: 0,
startCol: 0,
direction: 'horizontal',
answer: 'ZONNE'
},
{
word: 'MAAND',
clue: 'Tijdseenheid',
startRow: 2,
startCol: 0,
direction: 'horizontal',
answer: 'MAAND'
},
{
word: 'ND',
clue: 'Noord',
startRow: 0,
startCol: 4,
direction: 'vertical',
answer: 'ND'
}
],
difficulty: 2,
rewards: { coins: 150, stars: 5, hints: 2 },
dailyBonus: true
}
};
// Word Database for hints and validation
const DUTCH_WORD_DATABASE = {
animals: [
{ word: 'HAAS', clue: 'Snel dier' },
{ word: 'HOND', clue: 'Huisdier' },
{ word: 'KAT', clue: 'Katachtige' },
{ word: 'VLIEG', clue: 'Insect' },
{ word: 'VIS', clue: 'Zwemt in water' },
{ word: 'MUIS', clue: 'Klein dier' },
{ word: 'PAARD', clue: 'Rijdier' },
{ word: 'KIP', clue: 'Legt eieren' },
{ word: 'EEL', clue: 'Lang dier' }
],
nature: [
{ word: 'BOMEN', clue: 'Planten' },
{ word: 'REGEN', clue: 'Valt uit de lucht' },
{ word: 'SNEEU', clue: 'Witte vlokken' },
{ word: 'DONDER', clue: 'Weer verschijnsel' },
{ word: 'BLIKSEM', clue: 'Lichtflits' },
{ word: 'MAAND', clue: 'Satelliet' },
{ word: 'STER', clue: 'Hemellichaam' },
{ word: 'WOLK', clue: 'Watten in lucht' },
{ word: 'WIND', clue: 'Luchtbeweging' },
{ word: 'ZON', clue: 'Ster' }
],
common: [
{ word: 'AT', clue: 'Kleine woord' },
{ word: 'NE', clue: 'Nee afkorting' },
{ word: 'NB', clue: 'Nota bene' },
{ word: 'ND', clue: 'Noord' },
{ word: 'EN', clue: 'En' },
{ word: 'ER', clue: 'Er' },
{ word: 'IN', clue: 'In' },
{ word: 'OP', clue: 'Op' }
]
};
// Export for use in game logic
if (typeof module !== 'undefined' && module.exports) {
module.exports = { DUTCH_PUZZLES, DUTCH_WORD_DATABASE };
}

5
robots.txt Normal file
View File

@@ -0,0 +1,5 @@
# www.robotstxt.org/
# Allow crawling of all content
User-agent: *
Disallow:

12
site.webmanifest Normal file
View File

@@ -0,0 +1,12 @@
{
"short_name": "",
"name": "",
"icons": [{
"src": "icon.png",
"type": "image/png",
"sizes": "192x192"
}],
"start_url": "/?utm_source=homescreen",
"background_color": "#fafafa",
"theme_color": "#fafafa"
}

120
solve.html Normal file
View File

@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nederlandse Arroword Kruiswoordpuzzel</title>
<link rel="stylesheet" href="styles.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="app-container">
<!-- Header -->
<header class="app-header">
<h1><i class="fas fa-puzzle-piece"></i> Nederlandse Arroword</h1>
<div class="header-controls">
<button id="newPuzzleBtn" class="btn btn-primary">
<i class="fas fa-plus"></i> Nieuwe Puzzel
</button>
<button id="settingsBtn" class="btn btn-secondary">
<i class="fas fa-cog"></i> Instellingen
</button>
</div>
</header>
<!-- Main Content -->
<main class="main-content">
<!-- Puzzle Grid Container -->
<div class="puzzle-container">
<div id="crosswordGrid" class="crossword-grid"></div>
</div>
<!-- Sidebar -->
<aside class="sidebar">
<!-- Word Solver Tools -->
<div class="solver-tools">
<h3><i class="fas fa-lightbulb"></i> Hulp Gereedschap</h3>
<!-- Letter Pattern Solver -->
<div class="solver-section">
<h4>Letter Patroon</h4>
<input type="text" id="patternInput" placeholder="Bijv: H_L_O" maxlength="20">
<button id="patternSolveBtn" class="btn btn-help">
<i class="fas fa-search"></i> Zoek
</button>
<div id="patternResults" class="solver-results"></div>
</div>
<!-- Anagram Solver -->
<div class="solver-section">
<h4>Anagram Oplosser</h4>
<input type="text" id="anagramInput" placeholder="Bijv: OLHLO" maxlength="15">
<button id="anagramSolveBtn" class="btn btn-help">
<i class="fas fa-random"></i> Ontcijfer
</button>
<div id="anagramResults" class="solver-results"></div>
</div>
<!-- Progress Tracker -->
<div class="progress-section">
<h4>Voortgang</h4>
<div class="progress-bar">
<div id="progressFill" class="progress-fill"></div>
</div>
<span id="progressText">0% voltooid</span>
</div>
</div>
<!-- Game Controls -->
<div class="game-controls">
<button id="checkAnswersBtn" class="btn btn-success">
<i class="fas fa-check"></i> Controleer
</button>
<button id="revealLetterBtn" class="btn btn-warning">
<i class="fas fa-eye"></i> Onthul Letter
</button>
<button id="clearGridBtn" class="btn btn-danger">
<i class="fas fa-trash"></i> Wissen
</button>
</div>
</aside>
</main>
<!-- Settings Modal -->
<div id="settingsModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Instellingen</h2>
<div class="settings-group">
<label for="difficultySelect">Moeilijkheidsgraad:</label>
<select id="difficultySelect">
<option value="easy">Gemakkelijk</option>
<option value="medium" selected>Normaal</option>
<option value="hard">Moeilijk</option>
</select>
</div>
<div class="settings-group">
<label for="gridSizeSelect">Raster Grootte:</label>
<select id="gridSizeSelect">
<option value="11x16" selected>11x16 (Standaard)</option>
<option value="13x18">13x18</option>
<option value="15x20">15x20</option>
</select>
</div>
<div class="settings-group">
<label for="languageSelect">Taal:</label>
<select id="languageSelect">
<option value="nl" selected>Nederlands</option>
<option value="en">English</option>
</select>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script src="puzzleData.js"></script>
<script src="solvers.js"></script>
<script src="crossword.js"></script>
</body>
</html>

179
solvers.js Normal file
View File

@@ -0,0 +1,179 @@
// Pattern Solver
class PatternSolver {
static async solve(pattern) {
try {
const results = await PuzzleDatabase.getWordSuggestions(pattern);
return results.map(item => ({
word: item.word,
clue: item.clue,
confidence: this.calculateConfidence(pattern, item.word)
}));
} catch (error) {
console.error('Pattern solving error:', error);
return [];
}
}
static calculateConfidence(pattern, word) {
let matches = 0;
let total = pattern.length;
for (let i = 0; i < pattern.length; i++) {
if (pattern[i] !== '_' && pattern[i] === word[i]) {
matches++;
}
}
return Math.round((matches / total) * 100);
}
}
// Anagram Solver
class AnagramSolver {
static async solve(letters) {
try {
const results = await PuzzleDatabase.getAnagramSolutions(letters);
return results.map(item => ({
word: item.word,
clue: item.clue
}));
} catch (error) {
console.error('Anagram solving error:', error);
return [];
}
}
}
// Word Helper UI Controller
class WordHelperUI {
constructor() {
this.initializeEventListeners();
}
initializeEventListeners() {
// Pattern solver
const patternBtn = document.getElementById('patternSolveBtn');
const patternInput = document.getElementById('patternInput');
patternBtn.addEventListener('click', async () => {
await this.handlePatternSolve();
});
patternInput.addEventListener('keypress', async (e) => {
if (e.key === 'Enter') {
await this.handlePatternSolve();
}
});
// Anagram solver
const anagramBtn = document.getElementById('anagramSolveBtn');
const anagramInput = document.getElementById('anagramInput');
anagramBtn.addEventListener('click', async () => {
await this.handleAnagramSolve();
});
anagramInput.addEventListener('keypress', async (e) => {
if (e.key === 'Enter') {
await this.handleAnagramSolve();
}
});
}
async handlePatternSolve() {
const input = document.getElementById('patternInput').value.trim().toUpperCase();
const resultsDiv = document.getElementById('patternResults');
if (!input || !input.includes('_')) {
this.showError(resultsDiv, 'Voer een patroon in met onderstrepingstekens (_) voor onbekende letters.');
return;
}
this.showLoading(resultsDiv);
try {
const solutions = await PatternSolver.solve(input);
this.displayPatternResults(resultsDiv, solutions);
} catch (error) {
this.showError(resultsDiv, 'Er is een fout opgetreden bij het zoeken.');
}
}
async handleAnagramSolve() {
const input = document.getElementById('anagramInput').value.trim().toUpperCase();
const resultsDiv = document.getElementById('anagramResults');
if (!input || input.length < 3) {
this.showError(resultsDiv, 'Voer minstens 3 letters in voor anagram zoeken.');
return;
}
this.showLoading(resultsDiv);
try {
const solutions = await AnagramSolver.solve(input);
this.displayAnagramResults(resultsDiv, solutions);
} catch (error) {
this.showError(resultsDiv, 'Er is een fout opgetreden bij het ontcijferen.');
}
}
showLoading(container) {
container.innerHTML = '<div class="loading"></div><span>Zoeken...</span>';
}
showError(container, message) {
container.innerHTML = `<div style="color: #e53e3e; font-style: italic;">${message}</div>`;
}
displayPatternResults(container, results) {
if (results.length === 0) {
container.innerHTML = '<div style="color: #718096; font-style: italic;">Geen woorden gevonden.</div>';
return;
}
const html = results.map(result => `
<div class="solver-result" onclick="wordHelperUI.insertWord('${result.word}')">
<strong>${result.word}</strong> - ${result.clue}
<span style="float: right; color: #48bb78; font-size: 12px;">${result.confidence}%</span>
</div>
`).join('');
container.innerHTML = html;
}
displayAnagramResults(container, results) {
if (results.length === 0) {
container.innerHTML = '<div style="color: #718096; font-style: italic;">Geen anagrammen gevonden.</div>';
return;
}
const html = results.map(result => `
<div class="solver-result" onclick="wordHelperUI.insertWord('${result.word}')">
<strong>${result.word}</strong> - ${result.clue}
</div>
`).join('');
container.innerHTML = html;
}
insertWord(word) {
// This method can be enhanced to insert the word into the current active cell
const activeCell = document.querySelector('.grid-cell.active input');
if (activeCell) {
activeCell.value = word[0];
// Move to next cells automatically
let currentInput = activeCell;
for (let i = 1; i < word.length; i++) {
const nextCell = currentCell.parentElement.nextElementSibling?.querySelector('input');
if (nextCell) {
nextCell.value = word[i];
currentInput = nextCell;
}
}
}
}
}
// Initialize word helper
const wordHelperUI = new WordHelperUI();

472
style.css Normal file
View File

@@ -0,0 +1,472 @@
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
/* App Container */
.app-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* Header */
.app-header {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 20px 30px;
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.app-header h1 {
color: #4a5568;
font-size: 2rem;
font-weight: 700;
}
.header-controls {
display: flex;
gap: 15px;
}
/* Buttons */
.btn {
padding: 12px 20px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #e2e8f0;
color: #4a5568;
}
.btn-secondary:hover {
background: #cbd5e0;
}
.btn-success {
background: linear-gradient(135deg, #48bb78, #38a169);
color: white;
}
.btn-warning {
background: linear-gradient(135deg, #ed8936, #dd6b20);
color: white;
}
.btn-danger {
background: linear-gradient(135deg, #f56565, #e53e3e);
color: white;
}
.btn-help {
background: linear-gradient(135deg, #4299e1, #3182ce);
color: white;
padding: 8px 15px;
font-size: 0.9rem;
}
/* Main Content */
.main-content {
display: grid;
grid-template-columns: 1fr 350px;
gap: 30px;
align-items: start;
}
/* Puzzle Container */
.puzzle-container {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
/* Crossword Grid */
.crossword-grid {
display: grid;
gap: 2px;
background: #2d3748;
padding: 10px;
border-radius: 10px;
width: fit-content;
margin: 0 auto;
}
.grid-cell {
width: 40px;
height: 40px;
border: 2px solid #e2e8f0;
background: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 18px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.grid-cell:hover {
background: #f7fafc;
border-color: #4299e1;
}
.grid-cell.active {
background: #ebf8ff;
border-color: #3182ce;
box-shadow: 0 0 10px rgba(66, 153, 225, 0.3);
}
.grid-cell.correct {
background: #c6f6d5;
border-color: #48bb78;
}
.grid-cell.incorrect {
background: #fed7d7;
border-color: #f56565;
}
/* Clue Cell Styles */
.clue-cell {
background: linear-gradient(135deg, #4a5568, #2d3748);
color: white;
font-size: 8px;
font-weight: 600;
text-align: center;
padding: 2px;
line-height: 1.2;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
}
.clue-cell.horizontal {
border-right: 3px solid #4299e1;
}
.clue-cell.vertical {
border-bottom: 3px solid #4299e1;
}
.clue-text {
font-size: 7px;
line-height: 1.1;
word-wrap: break-word;
text-align: left;
}
.arrow {
position: absolute;
font-size: 12px;
color: #4299e1;
font-weight: bold;
}
.arrow-right {
right: 2px;
top: 50%;
transform: translateY(-50%);
}
.arrow-down {
bottom: 2px;
left: 50%;
transform: translateX(-50%);
}
/* Input Cell Styles */
.input-cell {
background: white;
border: 2px solid #cbd5e0;
}
.input-cell input {
width: 100%;
height: 100%;
border: none;
background: transparent;
text-align: center;
font-weight: bold;
font-size: 18px;
color: #2d3748;
text-transform: uppercase;
}
.input-cell input:focus {
outline: none;
background: #ebf8ff;
}
/* Blocked Cell */
.blocked-cell {
background: #2d3748;
border: 2px solid #2d3748;
}
/* Sidebar */
.sidebar {
display: flex;
flex-direction: column;
gap: 20px;
}
/* Solver Tools */
.solver-tools, .game-controls {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 25px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.solver-tools h3 {
color: #4a5568;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.solver-section {
margin-bottom: 20px;
}
.solver-section h4 {
color: #2d3748;
margin-bottom: 10px;
font-size: 1rem;
}
.solver-section input {
width: 100%;
padding: 10px;
border: 2px solid #e2e8f0;
border-radius: 6px;
margin-bottom: 10px;
font-size: 14px;
}
.solver-section input:focus {
outline: none;
border-color: #4299e1;
}
.solver-results {
max-height: 100px;
overflow-y: auto;
background: #f7fafc;
border-radius: 6px;
padding: 10px;
margin-top: 10px;
}
.solver-result {
padding: 5px 0;
cursor: pointer;
border-bottom: 1px solid #e2e8f0;
}
.solver-result:hover {
background: #ebf8ff;
}
.solver-result:last-child {
border-bottom: none;
}
/* Progress Section */
.progress-section h4 {
color: #2d3748;
margin-bottom: 10px;
}
.progress-bar {
width: 100%;
height: 10px;
background: #e2e8f0;
border-radius: 5px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #48bb78, #38a169);
width: 0%;
transition: width 0.3s ease;
}
#progressText {
font-size: 14px;
color: #4a5568;
font-weight: 600;
}
/* Game Controls */
.game-controls {
display: flex;
flex-direction: column;
gap: 12px;
}
.game-controls .btn {
width: 100%;
justify-content: center;
}
/* Modal */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
background-color: white;
margin: 15% auto;
padding: 30px;
border-radius: 15px;
width: 80%;
max-width: 500px;
position: relative;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover {
color: #000;
}
.settings-group {
margin-bottom: 20px;
}
.settings-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
.settings-group select {
width: 100%;
padding: 10px;
border: 2px solid #e2e8f0;
border-radius: 6px;
}
/* Responsive Design */
@media (max-width: 1024px) {
.main-content {
grid-template-columns: 1fr;
}
.sidebar {
order: -1;
}
}
@media (max-width: 768px) {
.app-header {
flex-direction: column;
gap: 15px;
text-align: center;
}
.header-controls {
justify-content: center;
}
.grid-cell {
width: 35px;
height: 35px;
font-size: 16px;
}
.clue-text {
font-size: 6px;
}
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.crossword-grid {
animation: fadeIn 0.5s ease-out;
}
/* Success Animation */
@keyframes success {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.correct {
animation: success 0.3s ease;
}
/* Loading Animation */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #4299e1;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

546
styles.css Normal file
View File

@@ -0,0 +1,546 @@
/* Reset and Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
overflow-x: hidden;
}
/* App Container */
.app-container {
max-width: 100vw;
margin: 0 auto;
background: white;
min-height: 100vh;
position: relative;
}
/* Top Header */
.top-header {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 100;
}
.header-left {
display: flex;
align-items: center;
gap: 15px;
}
.header-right {
display: flex;
align-items: center;
gap: 15px;
}
.header-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
font-size: 18px;
}
.header-btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.level-info {
display: flex;
flex-direction: column;
}
.level-number {
font-weight: 600;
font-size: 16px;
}
.difficulty-stars {
display: flex;
gap: 2px;
font-size: 12px;
}
.app-title {
font-size: 20px;
font-weight: 700;
text-align: center;
flex: 1;
}
.coins-display {
display: flex;
align-items: center;
gap: 5px;
background: rgba(255, 255, 255, 0.2);
padding: 8px 12px;
border-radius: 20px;
font-weight: 600;
}
.hints-btn {
position: relative;
}
.hint-count {
position: absolute;
top: -5px;
right: -5px;
background: #ff6b6b;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
}
/* Progress Bar */
.progress-container {
background: #f8f9fa;
padding: 15px 20px;
display: flex;
align-items: center;
gap: 15px;
border-bottom: 1px solid #e9ecef;
}
.progress-bar {
flex: 1;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #48bb78, #38a169);
width: 0%;
transition: width 0.3s ease;
}
.progress-text {
font-size: 14px;
font-weight: 600;
color: #4a5568;
min-width: 80px;
}
/* Main Game Area */
.game-main {
flex: 1;
display: flex;
flex-direction: column;
padding: 20px;
gap: 20px;
}
.puzzle-section {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
background: white;
border-radius: 15px;
padding: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
/* Crossword Grid */
.grid-container {
display: grid;
gap: 1px;
background: #cbd5e0;
padding: 10px;
border-radius: 10px;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.grid-cell {
width: 45px;
height: 45px;
background: white;
border: 2px solid #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
position: relative;
cursor: pointer;
transition: all 0.2s ease;
}
.grid-cell:hover {
background: #f7fafc;
border-color: #4299e1;
}
.grid-cell.active {
background: #ebf8ff;
border-color: #3182ce;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.3);
}
.grid-cell.correct {
background: #c6f6d5;
border-color: #48bb78;
}
.grid-cell.incorrect {
background: #fed7d7;
border-color: #f56565;
}
/* Clue Cell Styles */
.clue-cell {
background: linear-gradient(135deg, #4a5568, #2d3748);
color: white;
font-size: 9px;
font-weight: 600;
text-align: center;
padding: 2px;
line-height: 1.2;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
position: relative;
}
.clue-cell.horizontal {
border-right: 3px solid #4299e1;
}
.clue-cell.vertical {
border-bottom: 3px solid #4299e1;
}
.clue-text {
font-size: 8px;
line-height: 1.1;
word-wrap: break-word;
text-align: left;
width: 100%;
}
.arrow {
position: absolute;
font-size: 14px;
color: #4299e1;
font-weight: bold;
}
.arrow-right {
right: 1px;
top: 50%;
transform: translateY(-50%);
}
.arrow-down {
bottom: 1px;
left: 50%;
transform: translateX(-50%);
}
/* Input Cell Styles */
.input-cell {
background: white;
border: 2px solid #cbd5e0;
}
.input-cell input {
width: 100%;
height: 100%;
border: none;
background: transparent;
text-align: center;
font-weight: bold;
font-size: 20px;
color: #2d3748;
text-transform: uppercase;
}
.input-cell input:focus {
outline: none;
background: #ebf8ff;
}
/* Blocked Cell */
.blocked-cell {
background: #2d3748;
border: 2px solid #2d3748;
}
/* Bottom Controls */
.bottom-controls {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
padding: 0;
}
.control-btn {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 12px;
padding: 15px 10px;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
font-size: 12px;
}
.control-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.control-btn:active {
transform: translateY(0);
}
.control-btn i {
font-size: 20px;
}
.hint-btn {
background: linear-gradient(135deg, #ed8936, #dd6b20);
}
/* Side Menu */
.side-menu {
position: fixed;
top: 0;
left: -300px;
width: 300px;
height: 100vh;
background: white;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
transition: left 0.3s ease;
z-index: 200;
}
.side-menu.active {
left: 0;
}
.menu-header {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.close-menu {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
}
.menu-items {
padding: 20px 0;
}
.menu-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px 20px;
border: none;
background: none;
width: 100%;
text-align: left;
cursor: pointer;
transition: background 0.2s ease;
font-size: 16px;
}
.menu-item:hover {
background: #f8f9fa;
}
.menu-item i {
width: 20px;
color: #4facfe;
}
.bonus-indicator {
background: #48bb78;
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 600;
margin-left: auto;
}
/* Modals */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 300;
justify-content: center;
align-items: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
border-radius: 20px;
padding: 30px;
text-align: center;
max-width: 400px;
width: 90%;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.success-icon {
font-size: 60px;
color: #48bb78;
margin-bottom: 20px;
}
.success-modal h2 {
color: #2d3748;
margin-bottom: 10px;
}
.reward-info {
display: flex;
justify-content: space-around;
margin: 20px 0;
}
.reward-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
color: #4a5568;
}
.modal-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary {
background: linear-gradient(135deg, #48bb78, #38a169);
color: white;
flex: 1;
}
.btn-secondary {
background: #e2e8f0;
color: #4a5568;
}
.hint-cost {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin: 15px 0;
color: #4a5568;
}
/* Responsive Design */
@media (max-width: 768px) {
.app-title {
font-size: 18px;
}
.grid-cell {
width: 35px;
height: 35px;
}
.input-cell input {
font-size: 16px;
}
.clue-text {
font-size: 7px;
}
.control-btn {
padding: 12px 8px;
font-size: 11px;
}
.control-btn i {
font-size: 18px;
}
}
/* Animations */
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-10px); }
60% { transform: translateY(-5px); }
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.success-icon {
animation: bounce 1s ease;
}
.control-btn:hover {
animation: pulse 0.3s ease;
}

12
webpack.common.js Normal file
View File

@@ -0,0 +1,12 @@
const path = require('path');
module.exports = {
entry: {
app: './js/app.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
clean: true,
filename: './js/app.js',
},
};

13
webpack.config.dev.js Normal file
View File

@@ -0,0 +1,13 @@
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'inline-source-map',
devServer: {
liveReload: true,
hot: true,
open: true,
static: ['./'],
},
});

26
webpack.config.prod.js Normal file
View File

@@ -0,0 +1,26 @@
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
plugins: [
new HtmlWebpackPlugin({
template: './index.html',
}),
new CopyPlugin({
patterns: [
{ from: 'img', to: 'img' },
{ from: 'css', to: 'css' },
{ from: 'js/vendor', to: 'js/vendor' },
{ from: 'icon.svg', to: 'icon.svg' },
{ from: 'favicon.ico', to: 'favicon.ico' },
{ from: 'robots.txt', to: 'robots.txt' },
{ from: 'icon.png', to: 'icon.png' },
{ from: '404.html', to: '404.html' },
{ from: 'site.webmanifest', to: 'site.webmanifest' },
],
}),
],
});