vimalk78 commited on
Commit
d9a16d6
·
1 Parent(s): 39d60ba

display pre-generated crossword

Browse files

- able to build frontend and backend
- able to display the generated grid
- able to show the words with "Reveal Solution" button

Signed-off-by: Vimal Kumar <[email protected]>

Files changed (35) hide show
  1. .gitignore +42 -0
  2. README.md +85 -0
  3. crossword-app/README.md +209 -0
  4. crossword-app/backend/data/word-lists/animals.json +27 -0
  5. crossword-app/backend/data/word-lists/geography.json +32 -0
  6. crossword-app/backend/data/word-lists/science.json +32 -0
  7. crossword-app/backend/data/word-lists/technology.json +32 -0
  8. crossword-app/backend/package-lock.json +0 -0
  9. crossword-app/backend/package.json +49 -0
  10. crossword-app/backend/src/app.js +94 -0
  11. crossword-app/backend/src/controllers/puzzleController.js +65 -0
  12. crossword-app/backend/src/models/Topic.js +144 -0
  13. crossword-app/backend/src/models/Word.js +112 -0
  14. crossword-app/backend/src/routes/api.js +22 -0
  15. crossword-app/backend/src/services/crosswordGenerator.js +313 -0
  16. crossword-app/backend/src/services/wordService.js +123 -0
  17. crossword-app/database/migrations/001_initial_schema.sql +91 -0
  18. crossword-app/database/seeds/seed_data.sql +214 -0
  19. crossword-app/frontend/index.html +14 -0
  20. crossword-app/frontend/package-lock.json +0 -0
  21. crossword-app/frontend/package.json +42 -0
  22. crossword-app/frontend/src/App.jsx +127 -0
  23. crossword-app/frontend/src/components/ClueList.jsx +29 -0
  24. crossword-app/frontend/src/components/LoadingSpinner.jsx +12 -0
  25. crossword-app/frontend/src/components/PuzzleGrid.jsx +77 -0
  26. crossword-app/frontend/src/components/TopicSelector.jsx +36 -0
  27. crossword-app/frontend/src/hooks/useCrossword.js +93 -0
  28. crossword-app/frontend/src/main.jsx +9 -0
  29. crossword-app/frontend/src/styles/puzzle.css +332 -0
  30. crossword-app/frontend/src/utils/gridHelpers.js +58 -0
  31. crossword-app/frontend/vite.config.js +31 -0
  32. docs/TODO.md +111 -0
  33. docs/api-specification.md +244 -0
  34. docs/crossword-app-plan.md +217 -0
  35. docs/database-schema.sql +72 -0
.gitignore ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+ */node_modules/
4
+
5
+ # Environment variables
6
+ .env
7
+ .env.local
8
+ .env.development.local
9
+ .env.test.local
10
+ .env.production.local
11
+
12
+ # Build outputs
13
+ build/
14
+ dist/
15
+ */build/
16
+ */dist/
17
+
18
+ # Logs
19
+ npm-debug.log*
20
+ yarn-debug.log*
21
+ yarn-error.log*
22
+
23
+ # Runtime data
24
+ pids
25
+ *.pid
26
+ *.seed
27
+ *.pid.lock
28
+
29
+ # IDE files
30
+ .vscode/
31
+ .idea/
32
+ *.swp
33
+ *.swo
34
+
35
+ # OS generated files
36
+ .DS_Store
37
+ .DS_Store?
38
+ ._*
39
+ .Spotlight-V100
40
+ .Trashes
41
+ ehthumbs.db
42
+ Thumbs.db
README.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Crossword Puzzle Generator
2
+
3
+ A full-stack web application for generating and solving crossword puzzles with various topics.
4
+
5
+ ## Features
6
+
7
+ - Topic-based crossword generation
8
+ - Interactive puzzle grid with proper visual formatting
9
+ - Clues display (Across and Down)
10
+ - Solution reveal functionality
11
+ - Responsive design
12
+
13
+ ## Prerequisites
14
+
15
+ - Node.js (v14 or higher)
16
+ - npm
17
+
18
+ ## Installation & Setup
19
+
20
+ 1. **Clone the repository**
21
+ ```bash
22
+ git clone <your-repo-url>
23
+ cd cross-words
24
+ ```
25
+
26
+ 2. **Install backend dependencies**
27
+ ```bash
28
+ cd crossword-app/backend
29
+ npm install
30
+ ```
31
+
32
+ 3. **Install frontend dependencies**
33
+ ```bash
34
+ cd ../frontend
35
+ npm install
36
+ ```
37
+
38
+ ## Running the Application
39
+
40
+ 1. **Start the backend server**
41
+ ```bash
42
+ cd crossword-app/backend
43
+ npm run dev
44
+ ```
45
+ The backend will run on `http://localhost:3001`
46
+
47
+ 2. **Start the frontend development server** (in a new terminal)
48
+ ```bash
49
+ cd crossword-app/frontend
50
+ npm run dev
51
+ ```
52
+ The frontend will run on `http://localhost:5173`
53
+
54
+ 3. **Open your browser** and navigate to `http://localhost:5173`
55
+
56
+ ## How to Use
57
+
58
+ 1. Select one or more topics from the available options
59
+ 2. Click "Generate Puzzle" to create a new crossword
60
+ 3. Fill in the grid using the provided clues
61
+ 4. Click "Reveal Solution" to see the answers
62
+ 5. Click "Reset" to clear the grid and start over
63
+
64
+ ## Project Structure
65
+
66
+ ```
67
+ cross-words/
68
+ ├── crossword-app/
69
+ │ ├── backend/ # Node.js/Express API
70
+ │ │ ├── src/
71
+ │ │ ├── package.json
72
+ │ │ └── ...
73
+ │ └── frontend/ # React application
74
+ │ ├── src/
75
+ │ ├── package.json
76
+ │ └── ...
77
+ ├── crossword.py # Original Python implementation
78
+ └── README.md
79
+ ```
80
+
81
+ ## Technology Stack
82
+
83
+ - **Frontend**: React, Vite, CSS
84
+ - **Backend**: Node.js, Express
85
+ - **Algorithm**: Backtracking-based crossword generation
crossword-app/README.md ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Crossword Puzzle Generator
2
+
3
+ A full-stack web application that generates custom crossword puzzles based on selected topics using React frontend and Node.js backend.
4
+
5
+ ## Features
6
+
7
+ - **Topic Selection**: Choose from various categories (Animals, Science, Geography, Technology)
8
+ - **Dynamic Grid Generation**: Automatically creates crossword grids with word intersections
9
+ - **Interactive Interface**: Click-to-fill crossword grid with real-time validation
10
+ - **Difficulty Levels**: Easy, Medium, and Hard puzzle generation
11
+ - **Responsive Design**: Works on desktop and mobile devices
12
+
13
+ ## Tech Stack
14
+
15
+ ### Frontend
16
+ - **React 18** with hooks and functional components
17
+ - **Vite** for fast development and building
18
+ - **CSS Grid** for puzzle layout
19
+ - **Vanilla CSS** for styling (no external UI libraries)
20
+
21
+ ### Backend
22
+ - **Node.js** with Express.js framework
23
+ - **File-based word storage** (JSON files by topic)
24
+ - **CORS** enabled for cross-origin requests
25
+ - **Rate limiting** for API protection
26
+ - **Helmet** for security headers
27
+
28
+ ### Database (Optional)
29
+ - **PostgreSQL** schema included for advanced features
30
+ - **Migration and seed scripts** provided
31
+ - Currently uses JSON files for simplicity
32
+
33
+ ## Project Structure
34
+
35
+ ```
36
+ crossword-app/
37
+ ├── frontend/ # React frontend application
38
+ │ ├── src/
39
+ │ │ ├── components/ # React components
40
+ │ │ │ ├── TopicSelector.jsx
41
+ │ │ │ ├── PuzzleGrid.jsx
42
+ │ │ │ ├── ClueList.jsx
43
+ │ │ │ └── LoadingSpinner.jsx
44
+ │ │ ├── hooks/ # Custom React hooks
45
+ │ │ │ └── useCrossword.js
46
+ │ │ ├── utils/ # Utility functions
47
+ │ │ │ └── gridHelpers.js
48
+ │ │ ├── styles/ # CSS styling
49
+ │ │ │ └── puzzle.css
50
+ │ │ ├── App.jsx # Main app component
51
+ │ │ └── main.jsx # React entry point
52
+ │ ├── public/ # Static assets
53
+ │ ├── package.json
54
+ │ └── vite.config.js
55
+ ├── backend/ # Node.js backend API
56
+ │ ├── src/
57
+ │ │ ├── controllers/ # Request handlers
58
+ │ │ │ └── puzzleController.js
59
+ │ │ ├── services/ # Business logic
60
+ │ │ │ ├── crosswordGenerator.js
61
+ │ │ │ └── wordService.js
62
+ │ │ ├── models/ # Data models
63
+ │ │ │ ├── Word.js
64
+ │ │ │ └── Topic.js
65
+ │ │ ├── routes/ # API routes
66
+ │ │ │ └── api.js
67
+ │ │ └── app.js # Express application
68
+ │ ├── data/
69
+ │ │ └── word-lists/ # JSON word files by topic
70
+ │ ├── package.json
71
+ │ └── .env # Environment variables
72
+ └── database/ # Database schema (optional)
73
+ ├── migrations/
74
+ └── seeds/
75
+ ```
76
+
77
+ ## Getting Started
78
+
79
+ ### Prerequisites
80
+ - Node.js 18+ and npm 9+
81
+ - PostgreSQL (optional, for advanced features)
82
+
83
+ ### Installation
84
+
85
+ 1. **Clone the repository**
86
+ ```bash
87
+ git clone <repository-url>
88
+ cd crossword-app
89
+ ```
90
+
91
+ 2. **Install backend dependencies**
92
+ ```bash
93
+ cd backend
94
+ npm install
95
+ ```
96
+
97
+ 3. **Install frontend dependencies**
98
+ ```bash
99
+ cd ../frontend
100
+ npm install
101
+ ```
102
+
103
+ 4. **Configure environment variables**
104
+ ```bash
105
+ cd ../backend
106
+ cp .env.example .env
107
+ # Edit .env with your configuration
108
+ ```
109
+
110
+ ### Development
111
+
112
+ 1. **Start the backend server**
113
+ ```bash
114
+ cd backend
115
+ npm run dev
116
+ ```
117
+ Backend runs on http://localhost:3000
118
+
119
+ 2. **Start the frontend development server**
120
+ ```bash
121
+ cd frontend
122
+ npm run dev
123
+ ```
124
+ Frontend runs on http://localhost:5173
125
+
126
+ 3. **Access the application**
127
+ Open http://localhost:5173 in your browser
128
+
129
+ ### API Endpoints
130
+
131
+ - `GET /api/topics` - Get available topics
132
+ - `POST /api/generate` - Generate a crossword puzzle
133
+ - `POST /api/validate` - Validate user answers
134
+ - `GET /api/words/:topic` - Get words for a topic (admin)
135
+ - `GET /api/health` - Health check
136
+
137
+ ### Example API Usage
138
+
139
+ **Generate a puzzle:**
140
+ ```bash
141
+ curl -X POST http://localhost:3000/api/generate \
142
+ -H "Content-Type: application/json" \
143
+ -d '{
144
+ "topics": ["animals", "science"],
145
+ "difficulty": "medium"
146
+ }'
147
+ ```
148
+
149
+ ## Algorithm Details
150
+
151
+ ### Crossword Generation Process
152
+
153
+ 1. **Word Selection**: Picks 6-12 words from selected topics based on difficulty
154
+ 2. **Grid Sizing**: Automatically calculates optimal grid size
155
+ 3. **Placement Algorithm**: Uses backtracking to place words with intersections
156
+ 4. **Validation**: Ensures valid crossword structure with proper letter matching
157
+
158
+ ### Core Algorithm Features
159
+
160
+ - **Backtracking**: Tries all possible word placements and backtracks on conflicts
161
+ - **Intersection Logic**: Words can only cross where letters match
162
+ - **Grid Optimization**: Minimizes grid size while maximizing word density
163
+ - **Difficulty Scaling**: Adjusts word length and complexity based on difficulty
164
+
165
+ ## Deployment
166
+
167
+ ### Frontend (Vercel/Netlify)
168
+ ```bash
169
+ cd frontend
170
+ npm run build
171
+ # Deploy dist/ folder to your hosting service
172
+ ```
173
+
174
+ ### Backend (Railway/Heroku)
175
+ ```bash
176
+ cd backend
177
+ # Set environment variables in your hosting service
178
+ # Deploy with git or Docker
179
+ ```
180
+
181
+ ### Environment Variables for Production
182
+ ```bash
183
+ NODE_ENV=production
184
+ DATABASE_URL=postgresql://user:pass@host:port/db
185
+ CORS_ORIGIN=https://your-frontend-domain.com
186
+ JWT_SECRET=your-secure-secret
187
+ ```
188
+
189
+ ## Contributing
190
+
191
+ 1. Fork the repository
192
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
193
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
194
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
195
+ 5. Open a Pull Request
196
+
197
+ ## Future Enhancements
198
+
199
+ - [ ] User accounts and saved puzzles
200
+ - [ ] Puzzle sharing and social features
201
+ - [ ] Advanced word filtering and custom topics
202
+ - [ ] Print-friendly puzzle format
203
+ - [ ] Mobile app versions
204
+ - [ ] Multiplayer crossword solving
205
+ - [ ] AI-generated clues
206
+
207
+ ## License
208
+
209
+ This project is licensed under the MIT License - see the LICENSE file for details.
crossword-app/backend/data/word-lists/animals.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "word": "DOG", "clue": "Man's best friend" },
3
+ { "word": "CAT", "clue": "Feline pet that purrs" },
4
+ { "word": "ELEPHANT", "clue": "Large mammal with a trunk" },
5
+ { "word": "TIGER", "clue": "Striped big cat" },
6
+ { "word": "WHALE", "clue": "Largest marine mammal" },
7
+ { "word": "BUTTERFLY", "clue": "Colorful flying insect" },
8
+ { "word": "BIRD", "clue": "Flying creature with feathers" },
9
+ { "word": "FISH", "clue": "Aquatic animal with gills" },
10
+ { "word": "LION", "clue": "King of the jungle" },
11
+ { "word": "BEAR", "clue": "Large mammal that hibernates" },
12
+ { "word": "RABBIT", "clue": "Hopping mammal with long ears" },
13
+ { "word": "HORSE", "clue": "Riding animal with hooves" },
14
+ { "word": "SHEEP", "clue": "Woolly farm animal" },
15
+ { "word": "GOAT", "clue": "Horned farm animal" },
16
+ { "word": "DUCK", "clue": "Water bird that quacks" },
17
+ { "word": "CHICKEN", "clue": "Farm bird that lays eggs" },
18
+ { "word": "SNAKE", "clue": "Slithering reptile" },
19
+ { "word": "TURTLE", "clue": "Shelled reptile" },
20
+ { "word": "FROG", "clue": "Amphibian that croaks" },
21
+ { "word": "SHARK", "clue": "Predatory ocean fish" },
22
+ { "word": "DOLPHIN", "clue": "Intelligent marine mammal" },
23
+ { "word": "PENGUIN", "clue": "Flightless Antarctic bird" },
24
+ { "word": "MONKEY", "clue": "Primate that swings in trees" },
25
+ { "word": "ZEBRA", "clue": "Striped African animal" },
26
+ { "word": "GIRAFFE", "clue": "Tallest land animal" }
27
+ ]
crossword-app/backend/data/word-lists/geography.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "word": "MOUNTAIN", "clue": "High elevation landform" },
3
+ { "word": "OCEAN", "clue": "Large body of salt water" },
4
+ { "word": "DESERT", "clue": "Dry, arid region" },
5
+ { "word": "CONTINENT", "clue": "Large landmass" },
6
+ { "word": "RIVER", "clue": "Flowing body of water" },
7
+ { "word": "ISLAND", "clue": "Land surrounded by water" },
8
+ { "word": "FOREST", "clue": "Dense area of trees" },
9
+ { "word": "VALLEY", "clue": "Low area between hills" },
10
+ { "word": "LAKE", "clue": "Body of freshwater" },
11
+ { "word": "BEACH", "clue": "Sandy shore by water" },
12
+ { "word": "CLIFF", "clue": "Steep rock face" },
13
+ { "word": "PLATEAU", "clue": "Elevated flat area" },
14
+ { "word": "CANYON", "clue": "Deep gorge with steep sides" },
15
+ { "word": "GLACIER", "clue": "Moving mass of ice" },
16
+ { "word": "VOLCANO", "clue": "Mountain that erupts" },
17
+ { "word": "PENINSULA", "clue": "Land surrounded by water on three sides" },
18
+ { "word": "ARCHIPELAGO", "clue": "Group of islands" },
19
+ { "word": "PRAIRIE", "clue": "Grassland plain" },
20
+ { "word": "TUNDRA", "clue": "Cold, treeless region" },
21
+ { "word": "SAVANNA", "clue": "Tropical grassland" },
22
+ { "word": "EQUATOR", "clue": "Earth's middle line" },
23
+ { "word": "LATITUDE", "clue": "Distance from equator" },
24
+ { "word": "LONGITUDE", "clue": "Distance from prime meridian" },
25
+ { "word": "CLIMATE", "clue": "Long-term weather pattern" },
26
+ { "word": "MONSOON", "clue": "Seasonal wind pattern" },
27
+ { "word": "CAPITAL", "clue": "Main city of country" },
28
+ { "word": "BORDER", "clue": "Boundary between countries" },
29
+ { "word": "COAST", "clue": "Land meeting the sea" },
30
+ { "word": "STRAIT", "clue": "Narrow water passage" },
31
+ { "word": "DELTA", "clue": "River mouth formation" }
32
+ ]
crossword-app/backend/data/word-lists/science.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "word": "ATOM", "clue": "Smallest unit of matter" },
3
+ { "word": "GRAVITY", "clue": "Force that pulls objects down" },
4
+ { "word": "MOLECULE", "clue": "Group of atoms bonded together" },
5
+ { "word": "PHOTON", "clue": "Particle of light" },
6
+ { "word": "CHEMISTRY", "clue": "Study of matter and reactions" },
7
+ { "word": "PHYSICS", "clue": "Study of matter and energy" },
8
+ { "word": "BIOLOGY", "clue": "Study of living organisms" },
9
+ { "word": "ELEMENT", "clue": "Pure chemical substance" },
10
+ { "word": "OXYGEN", "clue": "Gas essential for breathing" },
11
+ { "word": "CARBON", "clue": "Element found in all life" },
12
+ { "word": "HYDROGEN", "clue": "Lightest chemical element" },
13
+ { "word": "ENERGY", "clue": "Capacity to do work" },
14
+ { "word": "FORCE", "clue": "Push or pull on an object" },
15
+ { "word": "VELOCITY", "clue": "Speed with direction" },
16
+ { "word": "MASS", "clue": "Amount of matter in object" },
17
+ { "word": "VOLUME", "clue": "Amount of space occupied" },
18
+ { "word": "DENSITY", "clue": "Mass per unit volume" },
19
+ { "word": "PRESSURE", "clue": "Force per unit area" },
20
+ { "word": "TEMPERATURE", "clue": "Measure of heat" },
21
+ { "word": "ELECTRON", "clue": "Negatively charged particle" },
22
+ { "word": "PROTON", "clue": "Positively charged particle" },
23
+ { "word": "NEUTRON", "clue": "Neutral atomic particle" },
24
+ { "word": "NUCLEUS", "clue": "Center of an atom" },
25
+ { "word": "CELL", "clue": "Basic unit of life" },
26
+ { "word": "DNA", "clue": "Genetic blueprint molecule" },
27
+ { "word": "PROTEIN", "clue": "Complex biological molecule" },
28
+ { "word": "ENZYME", "clue": "Biological catalyst" },
29
+ { "word": "VIRUS", "clue": "Infectious agent" },
30
+ { "word": "BACTERIA", "clue": "Single-celled organisms" },
31
+ { "word": "EVOLUTION", "clue": "Change in species over time" }
32
+ ]
crossword-app/backend/data/word-lists/technology.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ { "word": "COMPUTER", "clue": "Electronic processing device" },
3
+ { "word": "INTERNET", "clue": "Global computer network" },
4
+ { "word": "ALGORITHM", "clue": "Set of rules for solving problems" },
5
+ { "word": "DATABASE", "clue": "Organized collection of data" },
6
+ { "word": "SOFTWARE", "clue": "Computer programs" },
7
+ { "word": "HARDWARE", "clue": "Physical computer components" },
8
+ { "word": "NETWORK", "clue": "Connected system of computers" },
9
+ { "word": "CODE", "clue": "Programming instructions" },
10
+ { "word": "ROBOT", "clue": "Automated machine" },
11
+ { "word": "ARTIFICIAL", "clue": "Made by humans, not natural" },
12
+ { "word": "DIGITAL", "clue": "Using binary data" },
13
+ { "word": "BINARY", "clue": "Base-2 number system" },
14
+ { "word": "PROCESSOR", "clue": "Computer's brain" },
15
+ { "word": "MEMORY", "clue": "Data storage component" },
16
+ { "word": "KEYBOARD", "clue": "Input device with keys" },
17
+ { "word": "MONITOR", "clue": "Computer display screen" },
18
+ { "word": "MOUSE", "clue": "Pointing input device" },
19
+ { "word": "PRINTER", "clue": "Device that prints documents" },
20
+ { "word": "SCANNER", "clue": "Device that digitizes images" },
21
+ { "word": "CAMERA", "clue": "Device that captures images" },
22
+ { "word": "SMARTPHONE", "clue": "Portable computing device" },
23
+ { "word": "TABLET", "clue": "Touchscreen computing device" },
24
+ { "word": "LAPTOP", "clue": "Portable computer" },
25
+ { "word": "SERVER", "clue": "Computer that serves data" },
26
+ { "word": "CLOUD", "clue": "Internet-based computing" },
27
+ { "word": "WEBSITE", "clue": "Collection of web pages" },
28
+ { "word": "EMAIL", "clue": "Electronic mail" },
29
+ { "word": "BROWSER", "clue": "Web navigation software" },
30
+ { "word": "SEARCH", "clue": "Look for information" },
31
+ { "word": "DOWNLOAD", "clue": "Transfer data to device" }
32
+ ]
crossword-app/backend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
crossword-app/backend/package.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "crossword-backend",
3
+ "version": "1.0.0",
4
+ "description": "Node.js backend for crossword puzzle generator",
5
+ "main": "src/app.js",
6
+ "type": "commonjs",
7
+ "scripts": {
8
+ "start": "node src/app.js",
9
+ "dev": "nodemon src/app.js",
10
+ "test": "jest",
11
+ "test:watch": "jest --watch",
12
+ "lint": "eslint src/",
13
+ "lint:fix": "eslint src/ --fix",
14
+ "format": "prettier --write \"src/**/*.js\"",
15
+ "db:migrate": "node scripts/migrate.js",
16
+ "db:seed": "node scripts/seed.js",
17
+ "db:reset": "npm run db:migrate && npm run db:seed"
18
+ },
19
+ "dependencies": {
20
+ "express": "^4.18.2",
21
+ "cors": "^2.8.5",
22
+ "helmet": "^7.1.0",
23
+ "express-rate-limit": "^7.1.5",
24
+ "dotenv": "^16.3.1",
25
+ "pg": "^8.11.3",
26
+ "compression": "^1.7.4"
27
+ },
28
+ "devDependencies": {
29
+ "nodemon": "^3.0.2",
30
+ "jest": "^29.7.0",
31
+ "supertest": "^6.3.3",
32
+ "eslint": "^8.55.0",
33
+ "prettier": "^3.1.1"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0",
37
+ "npm": ">=9.0.0"
38
+ },
39
+ "keywords": [
40
+ "crossword",
41
+ "puzzle",
42
+ "word-game",
43
+ "api",
44
+ "nodejs",
45
+ "express"
46
+ ],
47
+ "author": "Crossword App Team",
48
+ "license": "MIT"
49
+ }
crossword-app/backend/src/app.js ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ const helmet = require('helmet');
4
+ const rateLimit = require('express-rate-limit');
5
+ const apiRoutes = require('./routes/api');
6
+
7
+ const app = express();
8
+ const PORT = process.env.PORT || 3000;
9
+
10
+ app.use(helmet());
11
+
12
+ const corsOptions = {
13
+ origin: process.env.CORS_ORIGIN || ['http://localhost:5173', 'http://localhost:3000'],
14
+ credentials: true,
15
+ optionsSuccessStatus: 200
16
+ };
17
+ app.use(cors(corsOptions));
18
+
19
+ const limiter = rateLimit({
20
+ windowMs: 15 * 60 * 1000,
21
+ max: 100,
22
+ message: 'Too many requests from this IP, please try again later.',
23
+ standardHeaders: true,
24
+ legacyHeaders: false,
25
+ });
26
+ app.use(limiter);
27
+
28
+ const generateLimiter = rateLimit({
29
+ windowMs: 5 * 60 * 1000,
30
+ max: 10,
31
+ message: 'Too many puzzle generation requests, please wait before trying again.',
32
+ });
33
+
34
+ app.use(express.json({ limit: '10mb' }));
35
+ app.use(express.urlencoded({ extended: true, limit: '10mb' }));
36
+
37
+ app.use((req, res, next) => {
38
+ console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
39
+ next();
40
+ });
41
+
42
+ app.use('/api/generate', generateLimiter);
43
+ app.use('/api', apiRoutes);
44
+
45
+ app.get('/', (req, res) => {
46
+ res.json({
47
+ message: 'Crossword Puzzle API',
48
+ version: '1.0.0',
49
+ endpoints: {
50
+ topics: 'GET /api/topics',
51
+ generate: 'POST /api/generate',
52
+ validate: 'POST /api/validate',
53
+ words: 'GET /api/words/:topic',
54
+ health: 'GET /api/health'
55
+ }
56
+ });
57
+ });
58
+
59
+ app.use((req, res) => {
60
+ res.status(404).json({
61
+ error: 'Not Found',
62
+ message: `Route ${req.method} ${req.path} not found`,
63
+ timestamp: new Date().toISOString()
64
+ });
65
+ });
66
+
67
+ app.use((error, req, res, next) => {
68
+ console.error('Error:', error);
69
+
70
+ if (error.type === 'entity.parse.failed') {
71
+ return res.status(400).json({
72
+ error: 'Invalid JSON',
73
+ message: 'Request body contains invalid JSON'
74
+ });
75
+ }
76
+
77
+ res.status(500).json({
78
+ error: 'Internal Server Error',
79
+ message: process.env.NODE_ENV === 'production'
80
+ ? 'Something went wrong'
81
+ : error.message,
82
+ timestamp: new Date().toISOString()
83
+ });
84
+ });
85
+
86
+ if (require.main === module) {
87
+ app.listen(PORT, () => {
88
+ console.log(`Server running on port ${PORT}`);
89
+ console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
90
+ console.log(`CORS enabled for: ${JSON.stringify(corsOptions.origin)}`);
91
+ });
92
+ }
93
+
94
+ module.exports = app;
crossword-app/backend/src/controllers/puzzleController.js ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const CrosswordGenerator = require('../services/crosswordGenerator');
2
+ const WordService = require('../services/wordService');
3
+
4
+ class PuzzleController {
5
+ static async getTopics(req, res) {
6
+ try {
7
+ const topics = await WordService.getAllTopics();
8
+ res.json(topics);
9
+ } catch (error) {
10
+ console.error('Error fetching topics:', error);
11
+ res.status(500).json({ error: 'Failed to fetch topics' });
12
+ }
13
+ }
14
+
15
+ static async generatePuzzle(req, res) {
16
+ try {
17
+ const { topics, difficulty = 'medium' } = req.body;
18
+
19
+ if (!topics || !Array.isArray(topics) || topics.length === 0) {
20
+ return res.status(400).json({ error: 'Topics array is required' });
21
+ }
22
+
23
+ const generator = new CrosswordGenerator();
24
+ const puzzle = await generator.generatePuzzle(topics, difficulty);
25
+
26
+ if (!puzzle) {
27
+ return res.status(400).json({ error: 'Could not generate puzzle with selected topics' });
28
+ }
29
+
30
+ res.json(puzzle);
31
+ } catch (error) {
32
+ console.error('Error generating puzzle:', error);
33
+ res.status(500).json({ error: 'Failed to generate puzzle' });
34
+ }
35
+ }
36
+
37
+ static async validateAnswers(req, res) {
38
+ try {
39
+ const { puzzle, answers } = req.body;
40
+
41
+ if (!puzzle || !answers) {
42
+ return res.status(400).json({ error: 'Puzzle and answers are required' });
43
+ }
44
+
45
+ const validation = CrosswordGenerator.validateAnswers(puzzle, answers);
46
+ res.json(validation);
47
+ } catch (error) {
48
+ console.error('Error validating answers:', error);
49
+ res.status(500).json({ error: 'Failed to validate answers' });
50
+ }
51
+ }
52
+
53
+ static async getWords(req, res) {
54
+ try {
55
+ const { topic } = req.params;
56
+ const words = await WordService.getWordsByTopic(topic);
57
+ res.json(words);
58
+ } catch (error) {
59
+ console.error('Error fetching words:', error);
60
+ res.status(500).json({ error: 'Failed to fetch words' });
61
+ }
62
+ }
63
+ }
64
+
65
+ module.exports = PuzzleController;
crossword-app/backend/src/models/Topic.js ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Topic {
2
+ constructor({ id, name, description, wordCount = 0, difficulty = 'medium' }) {
3
+ this.id = id;
4
+ this.name = name;
5
+ this.description = description;
6
+ this.wordCount = wordCount;
7
+ this.difficulty = difficulty;
8
+ this.createdAt = new Date();
9
+ this.updatedAt = new Date();
10
+ }
11
+
12
+ static validateTopic(topicData) {
13
+ const errors = [];
14
+
15
+ if (!topicData.name || typeof topicData.name !== 'string') {
16
+ errors.push('Topic name is required and must be a string');
17
+ } else if (topicData.name.length < 2 || topicData.name.length > 50) {
18
+ errors.push('Topic name must be between 2 and 50 characters');
19
+ }
20
+
21
+ if (topicData.description && typeof topicData.description !== 'string') {
22
+ errors.push('Description must be a string');
23
+ }
24
+
25
+ if (topicData.difficulty && !['easy', 'medium', 'hard'].includes(topicData.difficulty)) {
26
+ errors.push('Difficulty must be easy, medium, or hard');
27
+ }
28
+
29
+ return {
30
+ isValid: errors.length === 0,
31
+ errors
32
+ };
33
+ }
34
+
35
+ static fromJSON(data) {
36
+ return new Topic(data);
37
+ }
38
+
39
+ toJSON() {
40
+ return {
41
+ id: this.id,
42
+ name: this.name,
43
+ description: this.description,
44
+ wordCount: this.wordCount,
45
+ difficulty: this.difficulty,
46
+ createdAt: this.createdAt,
47
+ updatedAt: this.updatedAt
48
+ };
49
+ }
50
+
51
+ updateWordCount(count) {
52
+ this.wordCount = count;
53
+ this.updatedAt = new Date();
54
+ }
55
+
56
+ getDisplayName() {
57
+ return this.name
58
+ .split(/[-_\s]+/)
59
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
60
+ .join(' ');
61
+ }
62
+
63
+ getSlug() {
64
+ return this.name
65
+ .toLowerCase()
66
+ .replace(/[^a-z0-9]+/g, '-')
67
+ .replace(/^-+|-+$/g, '');
68
+ }
69
+
70
+ isPopular() {
71
+ return this.wordCount >= 20;
72
+ }
73
+
74
+ getDifficultyLevel() {
75
+ const levels = {
76
+ easy: 1,
77
+ medium: 2,
78
+ hard: 3
79
+ };
80
+
81
+ return levels[this.difficulty] || 2;
82
+ }
83
+
84
+ static getDefaultTopics() {
85
+ return [
86
+ new Topic({
87
+ id: 'animals',
88
+ name: 'Animals',
89
+ description: 'Various animals from around the world',
90
+ wordCount: 25,
91
+ difficulty: 'easy'
92
+ }),
93
+ new Topic({
94
+ id: 'science',
95
+ name: 'Science',
96
+ description: 'Scientific terms and concepts',
97
+ wordCount: 30,
98
+ difficulty: 'medium'
99
+ }),
100
+ new Topic({
101
+ id: 'geography',
102
+ name: 'Geography',
103
+ description: 'Countries, cities, and geographical features',
104
+ wordCount: 28,
105
+ difficulty: 'medium'
106
+ }),
107
+ new Topic({
108
+ id: 'technology',
109
+ name: 'Technology',
110
+ description: 'Computer and technology terms',
111
+ wordCount: 35,
112
+ difficulty: 'hard'
113
+ }),
114
+ new Topic({
115
+ id: 'history',
116
+ name: 'History',
117
+ description: 'Historical events and figures',
118
+ wordCount: 22,
119
+ difficulty: 'hard'
120
+ }),
121
+ new Topic({
122
+ id: 'sports',
123
+ name: 'Sports',
124
+ description: 'Sports and athletic activities',
125
+ wordCount: 20,
126
+ difficulty: 'easy'
127
+ })
128
+ ];
129
+ }
130
+
131
+ static sortByDifficulty(topics) {
132
+ return topics.sort((a, b) => a.getDifficultyLevel() - b.getDifficultyLevel());
133
+ }
134
+
135
+ static sortByPopularity(topics) {
136
+ return topics.sort((a, b) => b.wordCount - a.wordCount);
137
+ }
138
+
139
+ static filterByDifficulty(topics, difficulty) {
140
+ return topics.filter(topic => topic.difficulty === difficulty);
141
+ }
142
+ }
143
+
144
+ module.exports = Topic;
crossword-app/backend/src/models/Word.js ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Word {
2
+ constructor({ id, word, clue, topic, difficulty = 'medium', length }) {
3
+ this.id = id;
4
+ this.word = word.toUpperCase();
5
+ this.clue = clue;
6
+ this.topic = topic;
7
+ this.difficulty = difficulty;
8
+ this.length = length || word.length;
9
+ this.createdAt = new Date();
10
+ }
11
+
12
+ static validateWord(wordData) {
13
+ const errors = [];
14
+
15
+ if (!wordData.word || typeof wordData.word !== 'string') {
16
+ errors.push('Word is required and must be a string');
17
+ } else if (wordData.word.length < 2 || wordData.word.length > 15) {
18
+ errors.push('Word must be between 2 and 15 characters');
19
+ } else if (!/^[A-Za-z]+$/.test(wordData.word)) {
20
+ errors.push('Word must contain only letters');
21
+ }
22
+
23
+ if (!wordData.clue || typeof wordData.clue !== 'string') {
24
+ errors.push('Clue is required and must be a string');
25
+ }
26
+
27
+ if (!wordData.topic || typeof wordData.topic !== 'string') {
28
+ errors.push('Topic is required and must be a string');
29
+ }
30
+
31
+ if (wordData.difficulty && !['easy', 'medium', 'hard'].includes(wordData.difficulty)) {
32
+ errors.push('Difficulty must be easy, medium, or hard');
33
+ }
34
+
35
+ return {
36
+ isValid: errors.length === 0,
37
+ errors
38
+ };
39
+ }
40
+
41
+ static fromJSON(data) {
42
+ return new Word(data);
43
+ }
44
+
45
+ toJSON() {
46
+ return {
47
+ id: this.id,
48
+ word: this.word,
49
+ clue: this.clue,
50
+ topic: this.topic,
51
+ difficulty: this.difficulty,
52
+ length: this.length,
53
+ createdAt: this.createdAt
54
+ };
55
+ }
56
+
57
+ getDifficultyScore() {
58
+ const baseScore = this.length;
59
+ const difficultyMultiplier = {
60
+ easy: 1,
61
+ medium: 1.2,
62
+ hard: 1.5
63
+ };
64
+
65
+ return Math.round(baseScore * (difficultyMultiplier[this.difficulty] || 1));
66
+ }
67
+
68
+ isCompatibleWith(otherWord) {
69
+ if (!otherWord || !(otherWord instanceof Word)) {
70
+ return false;
71
+ }
72
+
73
+ const thisWord = this.word;
74
+ const otherWordStr = otherWord.word;
75
+
76
+ for (let i = 0; i < thisWord.length; i++) {
77
+ for (let j = 0; j < otherWordStr.length; j++) {
78
+ if (thisWord[i] === otherWordStr[j]) {
79
+ return true;
80
+ }
81
+ }
82
+ }
83
+
84
+ return false;
85
+ }
86
+
87
+ getIntersectionPoints(otherWord) {
88
+ if (!this.isCompatibleWith(otherWord)) {
89
+ return [];
90
+ }
91
+
92
+ const intersections = [];
93
+ const thisWord = this.word;
94
+ const otherWordStr = otherWord.word;
95
+
96
+ for (let i = 0; i < thisWord.length; i++) {
97
+ for (let j = 0; j < otherWordStr.length; j++) {
98
+ if (thisWord[i] === otherWordStr[j]) {
99
+ intersections.push({
100
+ thisIndex: i,
101
+ otherIndex: j,
102
+ letter: thisWord[i]
103
+ });
104
+ }
105
+ }
106
+ }
107
+
108
+ return intersections;
109
+ }
110
+ }
111
+
112
+ module.exports = Word;
crossword-app/backend/src/routes/api.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const PuzzleController = require('../controllers/puzzleController');
3
+
4
+ const router = express.Router();
5
+
6
+ router.get('/topics', PuzzleController.getTopics);
7
+
8
+ router.post('/generate', PuzzleController.generatePuzzle);
9
+
10
+ router.post('/validate', PuzzleController.validateAnswers);
11
+
12
+ router.get('/words/:topic', PuzzleController.getWords);
13
+
14
+ router.get('/health', (req, res) => {
15
+ res.json({
16
+ status: 'OK',
17
+ timestamp: new Date().toISOString(),
18
+ service: 'Crossword API'
19
+ });
20
+ });
21
+
22
+ module.exports = router;
crossword-app/backend/src/services/crosswordGenerator.js ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const WordService = require('./wordService');
2
+
3
+ class CrosswordGenerator {
4
+ constructor() {
5
+ this.maxAttempts = 100;
6
+ this.minWords = 6;
7
+ this.maxWords = 12;
8
+ }
9
+
10
+ async generatePuzzle(topics, difficulty = 'medium') {
11
+ try {
12
+ const words = await this.selectWords(topics, difficulty);
13
+ if (words.length < this.minWords) {
14
+ throw new Error('Not enough words available for selected topics');
15
+ }
16
+
17
+ const gridResult = this.createGrid(words);
18
+ if (!gridResult) {
19
+ throw new Error('Could not place words in grid');
20
+ }
21
+
22
+ const clues = this.generateClues(words, gridResult.placedWords);
23
+
24
+ return {
25
+ grid: gridResult.grid,
26
+ clues: clues,
27
+ metadata: {
28
+ topics,
29
+ difficulty,
30
+ wordCount: words.length,
31
+ size: gridResult.size
32
+ }
33
+ };
34
+ } catch (error) {
35
+ console.error('Error generating puzzle:', error);
36
+ return null;
37
+ }
38
+ }
39
+
40
+ async selectWords(topics, difficulty) {
41
+ const allWords = [];
42
+
43
+ for (const topic of topics) {
44
+ const topicWords = await WordService.getWordsByTopic(topic);
45
+ allWords.push(...topicWords);
46
+ }
47
+
48
+ const filteredWords = this.filterWordsByDifficulty(allWords, difficulty);
49
+ const shuffled = this.shuffleArray(filteredWords);
50
+
51
+ return shuffled.slice(0, this.maxWords);
52
+ }
53
+
54
+ filterWordsByDifficulty(words, difficulty) {
55
+ const difficultyMap = {
56
+ easy: { minLen: 3, maxLen: 7 },
57
+ medium: { minLen: 4, maxLen: 10 },
58
+ hard: { minLen: 5, maxLen: 15 }
59
+ };
60
+
61
+ const { minLen, maxLen } = difficultyMap[difficulty] || difficultyMap.medium;
62
+
63
+ return words.filter(word =>
64
+ word.word.length >= minLen && word.word.length <= maxLen
65
+ );
66
+ }
67
+
68
+ createGrid(words) {
69
+ if (!words || words.length === 0) return null;
70
+
71
+ const wordList = words.map(w => w.word.toUpperCase()).sort((a, b) => b.length - a.length);
72
+ const size = this.calculateGridSize(wordList);
73
+
74
+ for (let attempt = 0; attempt < 3; attempt++) {
75
+ const currentSize = size + attempt;
76
+ const result = this.placeWordsInGrid(wordList, currentSize);
77
+ if (result) {
78
+ return { grid: result.grid, size: currentSize, placedWords: result.placedWords };
79
+ }
80
+ }
81
+
82
+ return null;
83
+ }
84
+
85
+ calculateGridSize(words) {
86
+ const totalChars = words.reduce((sum, word) => sum + word.length, 0);
87
+ const longestWord = Math.max(...words.map(word => word.length));
88
+ return Math.max(
89
+ Math.ceil(Math.sqrt(totalChars * 1.5)),
90
+ longestWord,
91
+ 8
92
+ );
93
+ }
94
+
95
+ trimGrid(grid, placedWords) {
96
+ if (!placedWords || placedWords.length === 0) return grid;
97
+
98
+ // Find the bounds of the actual puzzle
99
+ let minRow = grid.length, maxRow = -1;
100
+ let minCol = grid[0].length, maxCol = -1;
101
+
102
+ placedWords.forEach(word => {
103
+ const { row, col, direction } = word;
104
+ const wordLength = word.word.length;
105
+
106
+ minRow = Math.min(minRow, row);
107
+ minCol = Math.min(minCol, col);
108
+
109
+ if (direction === 'horizontal') {
110
+ maxRow = Math.max(maxRow, row);
111
+ maxCol = Math.max(maxCol, col + wordLength - 1);
112
+ } else {
113
+ maxRow = Math.max(maxRow, row + wordLength - 1);
114
+ maxCol = Math.max(maxCol, col);
115
+ }
116
+ });
117
+
118
+ // Add small padding
119
+ minRow = Math.max(0, minRow - 1);
120
+ minCol = Math.max(0, minCol - 1);
121
+ maxRow = Math.min(grid.length - 1, maxRow + 1);
122
+ maxCol = Math.min(grid[0].length - 1, maxCol + 1);
123
+
124
+ // Create trimmed grid
125
+ const trimmedGrid = [];
126
+ for (let r = minRow; r <= maxRow; r++) {
127
+ const row = [];
128
+ for (let c = minCol; c <= maxCol; c++) {
129
+ row.push(grid[r][c]);
130
+ }
131
+ trimmedGrid.push(row);
132
+ }
133
+
134
+ // Update placed words positions to match trimmed grid
135
+ const updatedPlacedWords = placedWords.map(word => ({
136
+ ...word,
137
+ row: word.row - minRow,
138
+ col: word.col - minCol
139
+ }));
140
+
141
+ return { grid: trimmedGrid, placedWords: updatedPlacedWords };
142
+ }
143
+
144
+ placeWordsInGrid(words, size) {
145
+ const grid = Array(size).fill().map(() => Array(size).fill('.'));
146
+ const placedWords = [];
147
+
148
+ if (this.backtrackPlacement(grid, words, 0, placedWords)) {
149
+ const trimmed = this.trimGrid(grid, placedWords);
150
+ return { grid: trimmed.grid, placedWords: trimmed.placedWords };
151
+ }
152
+
153
+ return null;
154
+ }
155
+
156
+ backtrackPlacement(grid, words, wordIndex, placedWords) {
157
+ if (wordIndex >= words.length) {
158
+ return true;
159
+ }
160
+
161
+ const word = words[wordIndex];
162
+ const size = grid.length;
163
+
164
+ for (let row = 0; row < size; row++) {
165
+ for (let col = 0; col < size; col++) {
166
+ for (const direction of ['horizontal', 'vertical']) {
167
+ if (this.canPlaceWord(grid, word, row, col, direction)) {
168
+ const originalState = this.placeWord(grid, word, row, col, direction);
169
+ placedWords.push({ word, row, col, direction, number: wordIndex + 1 });
170
+
171
+ if (this.backtrackPlacement(grid, words, wordIndex + 1, placedWords)) {
172
+ return true;
173
+ }
174
+
175
+ this.removeWord(grid, originalState);
176
+ placedWords.pop();
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ return false;
183
+ }
184
+
185
+ canPlaceWord(grid, word, row, col, direction) {
186
+ const size = grid.length;
187
+
188
+ if (direction === 'horizontal') {
189
+ if (col + word.length > size) return false;
190
+
191
+ for (let i = 0; i < word.length; i++) {
192
+ const currentCell = grid[row][col + i];
193
+ if (currentCell !== '.' && currentCell !== word[i]) {
194
+ return false;
195
+ }
196
+ }
197
+ } else {
198
+ if (row + word.length > size) return false;
199
+
200
+ for (let i = 0; i < word.length; i++) {
201
+ const currentCell = grid[row + i][col];
202
+ if (currentCell !== '.' && currentCell !== word[i]) {
203
+ return false;
204
+ }
205
+ }
206
+ }
207
+
208
+ return this.validateWordPlacement(grid, word, row, col, direction);
209
+ }
210
+
211
+ validateWordPlacement(grid, word, row, col, direction) {
212
+ const size = grid.length;
213
+
214
+ if (direction === 'horizontal') {
215
+ if (col > 0 && grid[row][col - 1] !== '.') return false;
216
+ if (col + word.length < size && grid[row][col + word.length] !== '.') return false;
217
+ } else {
218
+ if (row > 0 && grid[row - 1][col] !== '.') return false;
219
+ if (row + word.length < size && grid[row + word.length][col] !== '.') return false;
220
+ }
221
+
222
+ return true;
223
+ }
224
+
225
+ placeWord(grid, word, row, col, direction) {
226
+ const originalState = [];
227
+
228
+ if (direction === 'horizontal') {
229
+ for (let i = 0; i < word.length; i++) {
230
+ originalState.push({
231
+ row: row,
232
+ col: col + i,
233
+ value: grid[row][col + i]
234
+ });
235
+ grid[row][col + i] = word[i];
236
+ }
237
+ } else {
238
+ for (let i = 0; i < word.length; i++) {
239
+ originalState.push({
240
+ row: row + i,
241
+ col: col,
242
+ value: grid[row + i][col]
243
+ });
244
+ grid[row + i][col] = word[i];
245
+ }
246
+ }
247
+
248
+ return originalState;
249
+ }
250
+
251
+ removeWord(grid, originalState) {
252
+ originalState.forEach(state => {
253
+ grid[state.row][state.col] = state.value;
254
+ });
255
+ }
256
+
257
+ generateClues(words, placedWords) {
258
+ return placedWords.map((placedWord, index) => {
259
+ // Find the word object that matches this placed word
260
+ const wordObj = words.find(w => w.word.toUpperCase() === placedWord.word);
261
+
262
+ return {
263
+ number: index + 1,
264
+ word: placedWord.word,
265
+ text: wordObj ? (wordObj.clue || `Clue for ${placedWord.word}`) : `Clue for ${placedWord.word}`,
266
+ direction: placedWord.direction === 'horizontal' ? 'across' : 'down',
267
+ position: { row: placedWord.row, col: placedWord.col }
268
+ };
269
+ });
270
+ }
271
+
272
+ static validateAnswers(puzzle, userAnswers) {
273
+ const correct = {};
274
+ const incorrect = {};
275
+ let totalCorrect = 0;
276
+ let totalCells = 0;
277
+
278
+ puzzle.grid.forEach((row, rowIndex) => {
279
+ row.forEach((cell, colIndex) => {
280
+ if (cell !== '.') {
281
+ totalCells++;
282
+ const key = `${rowIndex}-${colIndex}`;
283
+ const userAnswer = userAnswers[key];
284
+
285
+ if (userAnswer === cell) {
286
+ correct[key] = true;
287
+ totalCorrect++;
288
+ } else if (userAnswer) {
289
+ incorrect[key] = true;
290
+ }
291
+ }
292
+ });
293
+ });
294
+
295
+ return {
296
+ correct,
297
+ incorrect,
298
+ isComplete: totalCorrect === totalCells,
299
+ progress: totalCells > 0 ? (totalCorrect / totalCells) * 100 : 0
300
+ };
301
+ }
302
+
303
+ shuffleArray(array) {
304
+ const shuffled = [...array];
305
+ for (let i = shuffled.length - 1; i > 0; i--) {
306
+ const j = Math.floor(Math.random() * (i + 1));
307
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
308
+ }
309
+ return shuffled;
310
+ }
311
+ }
312
+
313
+ module.exports = CrosswordGenerator;
crossword-app/backend/src/services/wordService.js ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+
4
+ class WordService {
5
+ constructor() {
6
+ this.wordsCache = new Map();
7
+ this.topicsCache = null;
8
+ }
9
+
10
+ async getAllTopics() {
11
+ if (this.topicsCache) {
12
+ return this.topicsCache;
13
+ }
14
+
15
+ try {
16
+ const dataDir = path.join(__dirname, '../../data/word-lists');
17
+ const files = await fs.readdir(dataDir);
18
+
19
+ const topics = files
20
+ .filter(file => file.endsWith('.json'))
21
+ .map(file => ({
22
+ id: file.replace('.json', ''),
23
+ name: this.formatTopicName(file.replace('.json', ''))
24
+ }));
25
+
26
+ this.topicsCache = topics;
27
+ return topics;
28
+ } catch (error) {
29
+ console.error('Error loading topics:', error);
30
+ return this.getDefaultTopics();
31
+ }
32
+ }
33
+
34
+ async getWordsByTopic(topicName) {
35
+ const cacheKey = topicName.toLowerCase();
36
+
37
+ if (this.wordsCache.has(cacheKey)) {
38
+ return this.wordsCache.get(cacheKey);
39
+ }
40
+
41
+ try {
42
+ const filePath = path.join(__dirname, '../../data/word-lists', `${topicName.toLowerCase()}.json`);
43
+ const fileContent = await fs.readFile(filePath, 'utf8');
44
+ const words = JSON.parse(fileContent);
45
+
46
+ this.wordsCache.set(cacheKey, words);
47
+ return words;
48
+ } catch (error) {
49
+ console.error(`Error loading words for topic ${topicName}:`, error);
50
+ return this.getDefaultWords(topicName);
51
+ }
52
+ }
53
+
54
+ formatTopicName(fileName) {
55
+ return fileName
56
+ .split('-')
57
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
58
+ .join(' ');
59
+ }
60
+
61
+ getDefaultTopics() {
62
+ return [
63
+ { id: 'animals', name: 'Animals' },
64
+ { id: 'science', name: 'Science' },
65
+ { id: 'geography', name: 'Geography' },
66
+ { id: 'technology', name: 'Technology' }
67
+ ];
68
+ }
69
+
70
+ getDefaultWords(topic) {
71
+ const defaultWordSets = {
72
+ animals: [
73
+ { word: 'DOG', clue: 'Man\'s best friend' },
74
+ { word: 'CAT', clue: 'Feline pet' },
75
+ { word: 'ELEPHANT', clue: 'Large mammal with trunk' },
76
+ { word: 'TIGER', clue: 'Striped big cat' },
77
+ { word: 'WHALE', clue: 'Largest marine mammal' },
78
+ { word: 'BUTTERFLY', clue: 'Colorful flying insect' },
79
+ { word: 'BIRD', clue: 'Flying creature with feathers' },
80
+ { word: 'FISH', clue: 'Aquatic animal with gills' }
81
+ ],
82
+ science: [
83
+ { word: 'ATOM', clue: 'Smallest unit of matter' },
84
+ { word: 'GRAVITY', clue: 'Force that pulls objects down' },
85
+ { word: 'MOLECULE', clue: 'Group of atoms bonded together' },
86
+ { word: 'PHOTON', clue: 'Particle of light' },
87
+ { word: 'CHEMISTRY', clue: 'Study of matter and reactions' },
88
+ { word: 'PHYSICS', clue: 'Study of matter and energy' },
89
+ { word: 'BIOLOGY', clue: 'Study of living organisms' },
90
+ { word: 'ELEMENT', clue: 'Pure chemical substance' }
91
+ ],
92
+ geography: [
93
+ { word: 'MOUNTAIN', clue: 'High elevation landform' },
94
+ { word: 'OCEAN', clue: 'Large body of salt water' },
95
+ { word: 'DESERT', clue: 'Dry, arid region' },
96
+ { word: 'CONTINENT', clue: 'Large landmass' },
97
+ { word: 'RIVER', clue: 'Flowing body of water' },
98
+ { word: 'ISLAND', clue: 'Land surrounded by water' },
99
+ { word: 'FOREST', clue: 'Dense area of trees' },
100
+ { word: 'VALLEY', clue: 'Low area between hills' }
101
+ ],
102
+ technology: [
103
+ { word: 'COMPUTER', clue: 'Electronic processing device' },
104
+ { word: 'INTERNET', clue: 'Global computer network' },
105
+ { word: 'ALGORITHM', clue: 'Set of rules for solving problems' },
106
+ { word: 'DATABASE', clue: 'Organized collection of data' },
107
+ { word: 'SOFTWARE', clue: 'Computer programs' },
108
+ { word: 'ROBOT', clue: 'Automated machine' },
109
+ { word: 'NETWORK', clue: 'Connected system of computers' },
110
+ { word: 'CODE', clue: 'Programming instructions' }
111
+ ]
112
+ };
113
+
114
+ return defaultWordSets[topic.toLowerCase()] || [];
115
+ }
116
+
117
+ clearCache() {
118
+ this.wordsCache.clear();
119
+ this.topicsCache = null;
120
+ }
121
+ }
122
+
123
+ module.exports = new WordService();
crossword-app/database/migrations/001_initial_schema.sql ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Initial database schema for crossword puzzle application
2
+ -- This migration creates the basic tables for topics, words, and clues
3
+
4
+ CREATE TABLE IF NOT EXISTS topics (
5
+ id SERIAL PRIMARY KEY,
6
+ name VARCHAR(100) NOT NULL UNIQUE,
7
+ description TEXT,
8
+ difficulty VARCHAR(20) DEFAULT 'medium' CHECK (difficulty IN ('easy', 'medium', 'hard')),
9
+ word_count INTEGER DEFAULT 0,
10
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
11
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
12
+ );
13
+
14
+ CREATE TABLE IF NOT EXISTS words (
15
+ id SERIAL PRIMARY KEY,
16
+ word VARCHAR(50) NOT NULL,
17
+ length INTEGER NOT NULL,
18
+ topic_id INTEGER REFERENCES topics(id) ON DELETE CASCADE,
19
+ difficulty VARCHAR(20) DEFAULT 'medium' CHECK (difficulty IN ('easy', 'medium', 'hard')),
20
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
21
+ CONSTRAINT unique_word_topic UNIQUE (word, topic_id)
22
+ );
23
+
24
+ CREATE TABLE IF NOT EXISTS clues (
25
+ id SERIAL PRIMARY KEY,
26
+ word_id INTEGER REFERENCES words(id) ON DELETE CASCADE,
27
+ clue_text TEXT NOT NULL,
28
+ difficulty VARCHAR(20) DEFAULT 'medium' CHECK (difficulty IN ('easy', 'medium', 'hard')),
29
+ is_primary BOOLEAN DEFAULT true,
30
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
31
+ );
32
+
33
+ CREATE TABLE IF NOT EXISTS generated_puzzles (
34
+ id SERIAL PRIMARY KEY,
35
+ grid_data JSONB NOT NULL,
36
+ clues_data JSONB NOT NULL,
37
+ metadata JSONB,
38
+ topics TEXT[] NOT NULL,
39
+ difficulty VARCHAR(20) DEFAULT 'medium',
40
+ word_count INTEGER,
41
+ grid_size INTEGER,
42
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
43
+ expires_at TIMESTAMP WITH TIME ZONE DEFAULT (CURRENT_TIMESTAMP + INTERVAL '24 hours')
44
+ );
45
+
46
+ -- Indexes for better query performance
47
+ CREATE INDEX IF NOT EXISTS idx_words_topic_id ON words(topic_id);
48
+ CREATE INDEX IF NOT EXISTS idx_words_length ON words(length);
49
+ CREATE INDEX IF NOT EXISTS idx_words_difficulty ON words(difficulty);
50
+ CREATE INDEX IF NOT EXISTS idx_clues_word_id ON clues(word_id);
51
+ CREATE INDEX IF NOT EXISTS idx_clues_primary ON clues(is_primary);
52
+ CREATE INDEX IF NOT EXISTS idx_puzzles_topics ON generated_puzzles USING GIN(topics);
53
+ CREATE INDEX IF NOT EXISTS idx_puzzles_difficulty ON generated_puzzles(difficulty);
54
+ CREATE INDEX IF NOT EXISTS idx_puzzles_expires ON generated_puzzles(expires_at);
55
+
56
+ -- Function to update word count in topics table
57
+ CREATE OR REPLACE FUNCTION update_topic_word_count()
58
+ RETURNS TRIGGER AS $$
59
+ BEGIN
60
+ IF TG_OP = 'INSERT' THEN
61
+ UPDATE topics
62
+ SET word_count = word_count + 1, updated_at = CURRENT_TIMESTAMP
63
+ WHERE id = NEW.topic_id;
64
+ RETURN NEW;
65
+ ELSIF TG_OP = 'DELETE' THEN
66
+ UPDATE topics
67
+ SET word_count = word_count - 1, updated_at = CURRENT_TIMESTAMP
68
+ WHERE id = OLD.topic_id;
69
+ RETURN OLD;
70
+ END IF;
71
+ RETURN NULL;
72
+ END;
73
+ $$ LANGUAGE plpgsql;
74
+
75
+ -- Trigger to automatically update word count
76
+ CREATE TRIGGER trigger_update_word_count
77
+ AFTER INSERT OR DELETE ON words
78
+ FOR EACH ROW
79
+ EXECUTE FUNCTION update_topic_word_count();
80
+
81
+ -- Function to clean up expired puzzles
82
+ CREATE OR REPLACE FUNCTION cleanup_expired_puzzles()
83
+ RETURNS INTEGER AS $$
84
+ DECLARE
85
+ deleted_count INTEGER;
86
+ BEGIN
87
+ DELETE FROM generated_puzzles WHERE expires_at < CURRENT_TIMESTAMP;
88
+ GET DIAGNOSTICS deleted_count = ROW_COUNT;
89
+ RETURN deleted_count;
90
+ END;
91
+ $$ LANGUAGE plpgsql;
crossword-app/database/seeds/seed_data.sql ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Seed data for crossword puzzle application
2
+ -- This file populates the database with initial topics and words
3
+
4
+ -- Insert topics
5
+ INSERT INTO topics (name, description, difficulty) VALUES
6
+ ('Animals', 'Various animals from around the world', 'easy'),
7
+ ('Science', 'Scientific terms and concepts', 'medium'),
8
+ ('Geography', 'Countries, cities, and geographical features', 'medium'),
9
+ ('Technology', 'Computer and technology terms', 'hard'),
10
+ ('History', 'Historical events and figures', 'hard'),
11
+ ('Sports', 'Sports and athletic activities', 'easy')
12
+ ON CONFLICT (name) DO NOTHING;
13
+
14
+ -- Insert words and clues for Animals topic
15
+ WITH animal_topic AS (SELECT id FROM topics WHERE name = 'Animals')
16
+ INSERT INTO words (word, length, topic_id, difficulty)
17
+ SELECT word_data.word, LENGTH(word_data.word), animal_topic.id, 'easy'
18
+ FROM animal_topic,
19
+ (VALUES
20
+ ('DOG'), ('CAT'), ('ELEPHANT'), ('TIGER'), ('WHALE'),
21
+ ('BUTTERFLY'), ('BIRD'), ('FISH'), ('LION'), ('BEAR'),
22
+ ('RABBIT'), ('HORSE'), ('SHEEP'), ('GOAT'), ('DUCK'),
23
+ ('CHICKEN'), ('SNAKE'), ('TURTLE'), ('FROG'), ('SHARK'),
24
+ ('DOLPHIN'), ('PENGUIN'), ('MONKEY'), ('ZEBRA'), ('GIRAFFE')
25
+ ) AS word_data(word)
26
+ ON CONFLICT (word, topic_id) DO NOTHING;
27
+
28
+ -- Insert clues for animals
29
+ WITH animal_words AS (
30
+ SELECT w.id, w.word
31
+ FROM words w
32
+ JOIN topics t ON w.topic_id = t.id
33
+ WHERE t.name = 'Animals'
34
+ )
35
+ INSERT INTO clues (word_id, clue_text, difficulty)
36
+ SELECT aw.id, clue_data.clue, 'easy'
37
+ FROM animal_words aw
38
+ JOIN (VALUES
39
+ ('DOG', 'Man''s best friend'),
40
+ ('CAT', 'Feline pet that purrs'),
41
+ ('ELEPHANT', 'Large mammal with a trunk'),
42
+ ('TIGER', 'Striped big cat'),
43
+ ('WHALE', 'Largest marine mammal'),
44
+ ('BUTTERFLY', 'Colorful flying insect'),
45
+ ('BIRD', 'Flying creature with feathers'),
46
+ ('FISH', 'Aquatic animal with gills'),
47
+ ('LION', 'King of the jungle'),
48
+ ('BEAR', 'Large mammal that hibernates'),
49
+ ('RABBIT', 'Hopping mammal with long ears'),
50
+ ('HORSE', 'Riding animal with hooves'),
51
+ ('SHEEP', 'Woolly farm animal'),
52
+ ('GOAT', 'Horned farm animal'),
53
+ ('DUCK', 'Water bird that quacks'),
54
+ ('CHICKEN', 'Farm bird that lays eggs'),
55
+ ('SNAKE', 'Slithering reptile'),
56
+ ('TURTLE', 'Shelled reptile'),
57
+ ('FROG', 'Amphibian that croaks'),
58
+ ('SHARK', 'Predatory ocean fish'),
59
+ ('DOLPHIN', 'Intelligent marine mammal'),
60
+ ('PENGUIN', 'Flightless Antarctic bird'),
61
+ ('MONKEY', 'Primate that swings in trees'),
62
+ ('ZEBRA', 'Striped African animal'),
63
+ ('GIRAFFE', 'Tallest land animal')
64
+ ) AS clue_data(word, clue) ON aw.word = clue_data.word;
65
+
66
+ -- Insert words and clues for Science topic
67
+ WITH science_topic AS (SELECT id FROM topics WHERE name = 'Science')
68
+ INSERT INTO words (word, length, topic_id, difficulty)
69
+ SELECT word_data.word, LENGTH(word_data.word), science_topic.id, 'medium'
70
+ FROM science_topic,
71
+ (VALUES
72
+ ('ATOM'), ('GRAVITY'), ('MOLECULE'), ('PHOTON'), ('CHEMISTRY'),
73
+ ('PHYSICS'), ('BIOLOGY'), ('ELEMENT'), ('OXYGEN'), ('CARBON'),
74
+ ('HYDROGEN'), ('ENERGY'), ('FORCE'), ('VELOCITY'), ('MASS'),
75
+ ('VOLUME'), ('DENSITY'), ('PRESSURE'), ('ELECTRON'), ('PROTON'),
76
+ ('NEUTRON'), ('NUCLEUS'), ('CELL'), ('DNA'), ('PROTEIN')
77
+ ) AS word_data(word)
78
+ ON CONFLICT (word, topic_id) DO NOTHING;
79
+
80
+ -- Insert clues for science
81
+ WITH science_words AS (
82
+ SELECT w.id, w.word
83
+ FROM words w
84
+ JOIN topics t ON w.topic_id = t.id
85
+ WHERE t.name = 'Science'
86
+ )
87
+ INSERT INTO clues (word_id, clue_text, difficulty)
88
+ SELECT sw.id, clue_data.clue, 'medium'
89
+ FROM science_words sw
90
+ JOIN (VALUES
91
+ ('ATOM', 'Smallest unit of matter'),
92
+ ('GRAVITY', 'Force that pulls objects down'),
93
+ ('MOLECULE', 'Group of atoms bonded together'),
94
+ ('PHOTON', 'Particle of light'),
95
+ ('CHEMISTRY', 'Study of matter and reactions'),
96
+ ('PHYSICS', 'Study of matter and energy'),
97
+ ('BIOLOGY', 'Study of living organisms'),
98
+ ('ELEMENT', 'Pure chemical substance'),
99
+ ('OXYGEN', 'Gas essential for breathing'),
100
+ ('CARBON', 'Element found in all life'),
101
+ ('HYDROGEN', 'Lightest chemical element'),
102
+ ('ENERGY', 'Capacity to do work'),
103
+ ('FORCE', 'Push or pull on an object'),
104
+ ('VELOCITY', 'Speed with direction'),
105
+ ('MASS', 'Amount of matter in object'),
106
+ ('VOLUME', 'Amount of space occupied'),
107
+ ('DENSITY', 'Mass per unit volume'),
108
+ ('PRESSURE', 'Force per unit area'),
109
+ ('ELECTRON', 'Negatively charged particle'),
110
+ ('PROTON', 'Positively charged particle'),
111
+ ('NEUTRON', 'Neutral atomic particle'),
112
+ ('NUCLEUS', 'Center of an atom'),
113
+ ('CELL', 'Basic unit of life'),
114
+ ('DNA', 'Genetic blueprint molecule'),
115
+ ('PROTEIN', 'Complex biological molecule')
116
+ ) AS clue_data(word, clue) ON sw.word = clue_data.word;
117
+
118
+ -- Insert words and clues for Geography topic
119
+ WITH geography_topic AS (SELECT id FROM topics WHERE name = 'Geography')
120
+ INSERT INTO words (word, length, topic_id, difficulty)
121
+ SELECT word_data.word, LENGTH(word_data.word), geography_topic.id, 'medium'
122
+ FROM geography_topic,
123
+ (VALUES
124
+ ('MOUNTAIN'), ('OCEAN'), ('DESERT'), ('CONTINENT'), ('RIVER'),
125
+ ('ISLAND'), ('FOREST'), ('VALLEY'), ('LAKE'), ('BEACH'),
126
+ ('PLATEAU'), ('CANYON'), ('GLACIER'), ('VOLCANO'), ('EQUATOR'),
127
+ ('LATITUDE'), ('CLIMATE'), ('CAPITAL'), ('BORDER'), ('COAST')
128
+ ) AS word_data(word)
129
+ ON CONFLICT (word, topic_id) DO NOTHING;
130
+
131
+ -- Insert clues for geography
132
+ WITH geography_words AS (
133
+ SELECT w.id, w.word
134
+ FROM words w
135
+ JOIN topics t ON w.topic_id = t.id
136
+ WHERE t.name = 'Geography'
137
+ )
138
+ INSERT INTO clues (word_id, clue_text, difficulty)
139
+ SELECT gw.id, clue_data.clue, 'medium'
140
+ FROM geography_words gw
141
+ JOIN (VALUES
142
+ ('MOUNTAIN', 'High elevation landform'),
143
+ ('OCEAN', 'Large body of salt water'),
144
+ ('DESERT', 'Dry, arid region'),
145
+ ('CONTINENT', 'Large landmass'),
146
+ ('RIVER', 'Flowing body of water'),
147
+ ('ISLAND', 'Land surrounded by water'),
148
+ ('FOREST', 'Dense area of trees'),
149
+ ('VALLEY', 'Low area between hills'),
150
+ ('LAKE', 'Body of freshwater'),
151
+ ('BEACH', 'Sandy shore by water'),
152
+ ('PLATEAU', 'Elevated flat area'),
153
+ ('CANYON', 'Deep gorge with steep sides'),
154
+ ('GLACIER', 'Moving mass of ice'),
155
+ ('VOLCANO', 'Mountain that erupts'),
156
+ ('EQUATOR', 'Earth''s middle line'),
157
+ ('LATITUDE', 'Distance from equator'),
158
+ ('CLIMATE', 'Long-term weather pattern'),
159
+ ('CAPITAL', 'Main city of country'),
160
+ ('BORDER', 'Boundary between countries'),
161
+ ('COAST', 'Land meeting the sea')
162
+ ) AS clue_data(word, clue) ON gw.word = clue_data.word;
163
+
164
+ -- Insert words and clues for Technology topic
165
+ WITH technology_topic AS (SELECT id FROM topics WHERE name = 'Technology')
166
+ INSERT INTO words (word, length, topic_id, difficulty)
167
+ SELECT word_data.word, LENGTH(word_data.word), technology_topic.id, 'hard'
168
+ FROM technology_topic,
169
+ (VALUES
170
+ ('COMPUTER'), ('INTERNET'), ('ALGORITHM'), ('DATABASE'), ('SOFTWARE'),
171
+ ('HARDWARE'), ('NETWORK'), ('CODE'), ('ROBOT'), ('DIGITAL'),
172
+ ('PROCESSOR'), ('MEMORY'), ('KEYBOARD'), ('MONITOR'), ('MOUSE'),
173
+ ('SMARTPHONE'), ('TABLET'), ('LAPTOP'), ('SERVER'), ('CLOUD'),
174
+ ('WEBSITE'), ('EMAIL'), ('BROWSER'), ('SEARCH'), ('DOWNLOAD')
175
+ ) AS word_data(word)
176
+ ON CONFLICT (word, topic_id) DO NOTHING;
177
+
178
+ -- Insert clues for technology
179
+ WITH technology_words AS (
180
+ SELECT w.id, w.word
181
+ FROM words w
182
+ JOIN topics t ON w.topic_id = t.id
183
+ WHERE t.name = 'Technology'
184
+ )
185
+ INSERT INTO clues (word_id, clue_text, difficulty)
186
+ SELECT tw.id, clue_data.clue, 'hard'
187
+ FROM technology_words tw
188
+ JOIN (VALUES
189
+ ('COMPUTER', 'Electronic processing device'),
190
+ ('INTERNET', 'Global computer network'),
191
+ ('ALGORITHM', 'Set of rules for solving problems'),
192
+ ('DATABASE', 'Organized collection of data'),
193
+ ('SOFTWARE', 'Computer programs'),
194
+ ('HARDWARE', 'Physical computer components'),
195
+ ('NETWORK', 'Connected system of computers'),
196
+ ('CODE', 'Programming instructions'),
197
+ ('ROBOT', 'Automated machine'),
198
+ ('DIGITAL', 'Using binary data'),
199
+ ('PROCESSOR', 'Computer''s brain'),
200
+ ('MEMORY', 'Data storage component'),
201
+ ('KEYBOARD', 'Input device with keys'),
202
+ ('MONITOR', 'Computer display screen'),
203
+ ('MOUSE', 'Pointing input device'),
204
+ ('SMARTPHONE', 'Portable computing device'),
205
+ ('TABLET', 'Touchscreen computing device'),
206
+ ('LAPTOP', 'Portable computer'),
207
+ ('SERVER', 'Computer that serves data'),
208
+ ('CLOUD', 'Internet-based computing'),
209
+ ('WEBSITE', 'Collection of web pages'),
210
+ ('EMAIL', 'Electronic mail'),
211
+ ('BROWSER', 'Web navigation software'),
212
+ ('SEARCH', 'Look for information'),
213
+ ('DOWNLOAD', 'Transfer data to device')
214
+ ) AS clue_data(word, clue) ON tw.word = clue_data.word;
crossword-app/frontend/index.html ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="Generate custom crossword puzzles by selecting topics" />
7
+ <meta name="keywords" content="crossword, puzzle, word game, brain teaser" />
8
+ <title>Crossword Puzzle Generator</title>
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/src/main.jsx"></script>
13
+ </body>
14
+ </html>
crossword-app/frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
crossword-app/frontend/package.json ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "crossword-frontend",
3
+ "version": "1.0.0",
4
+ "description": "React frontend for crossword puzzle generator",
5
+ "private": true,
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "vite",
9
+ "build": "vite build",
10
+ "preview": "vite preview",
11
+ "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
12
+ "lint:fix": "eslint . --ext js,jsx --fix",
13
+ "format": "prettier --write \"src/**/*.{js,jsx,css,md}\""
14
+ },
15
+ "dependencies": {
16
+ "react": "^18.2.0",
17
+ "react-dom": "^18.2.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/react": "^18.2.43",
21
+ "@types/react-dom": "^18.2.17",
22
+ "@vitejs/plugin-react": "^4.2.1",
23
+ "eslint": "^8.55.0",
24
+ "eslint-plugin-react": "^7.33.2",
25
+ "eslint-plugin-react-hooks": "^4.6.0",
26
+ "eslint-plugin-react-refresh": "^0.4.5",
27
+ "prettier": "^3.1.1",
28
+ "vite": "^5.0.8"
29
+ },
30
+ "browserslist": {
31
+ "production": [
32
+ ">0.2%",
33
+ "not dead",
34
+ "not op_mini all"
35
+ ],
36
+ "development": [
37
+ "last 1 chrome version",
38
+ "last 1 firefox version",
39
+ "last 1 safari version"
40
+ ]
41
+ }
42
+ }
crossword-app/frontend/src/App.jsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import TopicSelector from './components/TopicSelector';
3
+ import PuzzleGrid from './components/PuzzleGrid';
4
+ import ClueList from './components/ClueList';
5
+ import LoadingSpinner from './components/LoadingSpinner';
6
+ import useCrossword from './hooks/useCrossword';
7
+ import './styles/puzzle.css';
8
+
9
+ function App() {
10
+ const [selectedTopics, setSelectedTopics] = useState([]);
11
+ const [difficulty, setDifficulty] = useState('medium');
12
+ const [showSolution, setShowSolution] = useState(false);
13
+
14
+ const {
15
+ puzzle,
16
+ loading,
17
+ error,
18
+ topics,
19
+ fetchTopics,
20
+ generatePuzzle,
21
+ resetPuzzle
22
+ } = useCrossword();
23
+
24
+ useEffect(() => {
25
+ fetchTopics();
26
+ }, [fetchTopics]);
27
+
28
+ const handleGeneratePuzzle = async () => {
29
+ if (selectedTopics.length === 0) {
30
+ alert('Please select at least one topic');
31
+ return;
32
+ }
33
+
34
+ await generatePuzzle(selectedTopics, difficulty);
35
+ };
36
+
37
+ const handleTopicsChange = (topics) => {
38
+ setSelectedTopics(topics);
39
+ };
40
+
41
+ const handleReset = () => {
42
+ resetPuzzle();
43
+ setSelectedTopics([]);
44
+ setShowSolution(false);
45
+ };
46
+
47
+ const handleRevealSolution = () => {
48
+ setShowSolution(true);
49
+ };
50
+
51
+ return (
52
+ <div className="crossword-app">
53
+ <header className="app-header">
54
+ <h1 className="app-title">Crossword Puzzle Generator</h1>
55
+ <p>Select topics and generate your custom crossword puzzle!</p>
56
+ </header>
57
+
58
+ <TopicSelector
59
+ onTopicsChange={handleTopicsChange}
60
+ availableTopics={topics}
61
+ />
62
+
63
+ <div className="puzzle-controls">
64
+ <select
65
+ value={difficulty}
66
+ onChange={(e) => setDifficulty(e.target.value)}
67
+ className="control-btn"
68
+ >
69
+ <option value="easy">Easy</option>
70
+ <option value="medium">Medium</option>
71
+ <option value="hard">Hard</option>
72
+ </select>
73
+
74
+ <button
75
+ onClick={handleGeneratePuzzle}
76
+ disabled={loading || selectedTopics.length === 0}
77
+ className="control-btn generate-btn"
78
+ >
79
+ {loading ? 'Generating...' : 'Generate Puzzle'}
80
+ </button>
81
+
82
+ <button
83
+ onClick={handleReset}
84
+ className="control-btn reset-btn"
85
+ >
86
+ Reset
87
+ </button>
88
+
89
+ {puzzle && !showSolution && (
90
+ <button
91
+ onClick={handleRevealSolution}
92
+ className="control-btn reveal-btn"
93
+ >
94
+ Reveal Solution
95
+ </button>
96
+ )}
97
+ </div>
98
+
99
+ {error && (
100
+ <div className="error-message">
101
+ Error: {error}
102
+ </div>
103
+ )}
104
+
105
+ {loading && <LoadingSpinner />}
106
+
107
+ {puzzle && !loading && (
108
+ <div className="puzzle-layout">
109
+ <PuzzleGrid
110
+ grid={puzzle.grid}
111
+ clues={puzzle.clues}
112
+ showSolution={showSolution}
113
+ />
114
+ <ClueList clues={puzzle.clues} />
115
+ </div>
116
+ )}
117
+
118
+ {!puzzle && !loading && !error && (
119
+ <div style={{ textAlign: 'center', padding: '40px', color: '#7f8c8d' }}>
120
+ Select topics and click "Generate Puzzle" to start!
121
+ </div>
122
+ )}
123
+ </div>
124
+ );
125
+ }
126
+
127
+ export default App;
crossword-app/frontend/src/components/ClueList.jsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ const ClueList = ({ clues = [] }) => {
4
+ const acrossClues = clues.filter(clue => clue.direction === 'across');
5
+ const downClues = clues.filter(clue => clue.direction === 'down');
6
+
7
+ const ClueSection = ({ title, clueList }) => (
8
+ <div className="clue-section">
9
+ <h4>{title}</h4>
10
+ <ol>
11
+ {clueList.map(clue => (
12
+ <li key={`${clue.number}-${clue.direction}`} className="clue-item">
13
+ <span className="clue-number">{clue.number}</span>
14
+ <span className="clue-text">{clue.text}</span>
15
+ </li>
16
+ ))}
17
+ </ol>
18
+ </div>
19
+ );
20
+
21
+ return (
22
+ <div className="clue-list">
23
+ <ClueSection title="Across" clueList={acrossClues} />
24
+ <ClueSection title="Down" clueList={downClues} />
25
+ </div>
26
+ );
27
+ };
28
+
29
+ export default ClueList;
crossword-app/frontend/src/components/LoadingSpinner.jsx ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ const LoadingSpinner = ({ message = "Generating puzzle..." }) => {
4
+ return (
5
+ <div className="loading-spinner">
6
+ <div className="spinner"></div>
7
+ <p className="loading-message">{message}</p>
8
+ </div>
9
+ );
10
+ };
11
+
12
+ export default LoadingSpinner;
crossword-app/frontend/src/components/PuzzleGrid.jsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+
3
+ const PuzzleGrid = ({ grid, clues, showSolution, onCellChange }) => {
4
+ const [userAnswers, setUserAnswers] = useState({});
5
+
6
+ const handleCellInput = (row, col, value) => {
7
+ const key = `${row}-${col}`;
8
+ const newAnswers = { ...userAnswers, [key]: value.toUpperCase() };
9
+ setUserAnswers(newAnswers);
10
+ onCellChange && onCellChange(row, col, value);
11
+ };
12
+
13
+ const getCellValue = (row, col) => {
14
+ if (showSolution && !isBlackCell(row, col)) {
15
+ return grid[row][col];
16
+ }
17
+ const key = `${row}-${col}`;
18
+ return userAnswers[key] || '';
19
+ };
20
+
21
+ const isBlackCell = (row, col) => {
22
+ return grid[row][col] === '.';
23
+ };
24
+
25
+ const getCellNumber = (row, col) => {
26
+ if (!clues) return null;
27
+ const clue = clues.find(c => c.position.row === row && c.position.col === col);
28
+ return clue ? clue.number : null;
29
+ };
30
+
31
+ if (!grid || grid.length === 0) {
32
+ return <div className="puzzle-grid">No puzzle loaded</div>;
33
+ }
34
+
35
+ const gridRows = grid.length;
36
+ const gridCols = grid[0] ? grid[0].length : 0;
37
+
38
+ return (
39
+ <div className="puzzle-container">
40
+ <div
41
+ className="puzzle-grid"
42
+ style={{
43
+ gridTemplateColumns: `repeat(${gridCols}, 35px)`,
44
+ gridTemplateRows: `repeat(${gridRows}, 35px)`
45
+ }}
46
+ >
47
+ {grid.map((row, rowIndex) =>
48
+ row.map((cell, colIndex) => {
49
+ const cellNumber = getCellNumber(rowIndex, colIndex);
50
+ return (
51
+ <div
52
+ key={`${rowIndex}-${colIndex}`}
53
+ className={`grid-cell ${isBlackCell(rowIndex, colIndex) ? 'black-cell' : 'white-cell'}`}
54
+ >
55
+ {!isBlackCell(rowIndex, colIndex) && (
56
+ <>
57
+ {cellNumber && <span className="cell-number">{cellNumber}</span>}
58
+ <input
59
+ type="text"
60
+ maxLength="1"
61
+ value={getCellValue(rowIndex, colIndex)}
62
+ onChange={(e) => handleCellInput(rowIndex, colIndex, e.target.value)}
63
+ className={`cell-input ${showSolution ? 'solution-text' : ''}`}
64
+ disabled={showSolution}
65
+ />
66
+ </>
67
+ )}
68
+ </div>
69
+ );
70
+ })
71
+ )}
72
+ </div>
73
+ </div>
74
+ );
75
+ };
76
+
77
+ export default PuzzleGrid;
crossword-app/frontend/src/components/TopicSelector.jsx ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+
3
+ const TopicSelector = ({ onTopicsChange, availableTopics = [] }) => {
4
+ const [selectedTopics, setSelectedTopics] = useState([]);
5
+
6
+ const handleTopicToggle = (topic) => {
7
+ const newSelectedTopics = selectedTopics.includes(topic)
8
+ ? selectedTopics.filter(t => t !== topic)
9
+ : [...selectedTopics, topic];
10
+
11
+ setSelectedTopics(newSelectedTopics);
12
+ onTopicsChange(newSelectedTopics);
13
+ };
14
+
15
+ return (
16
+ <div className="topic-selector">
17
+ <h3>Select Topics</h3>
18
+ <div className="topic-buttons">
19
+ {availableTopics.map(topic => (
20
+ <button
21
+ key={topic.id}
22
+ className={`topic-btn ${selectedTopics.includes(topic.name) ? 'selected' : ''}`}
23
+ onClick={() => handleTopicToggle(topic.name)}
24
+ >
25
+ {topic.name}
26
+ </button>
27
+ ))}
28
+ </div>
29
+ <p className="selected-count">
30
+ {selectedTopics.length} topic{selectedTopics.length !== 1 ? 's' : ''} selected
31
+ </p>
32
+ </div>
33
+ );
34
+ };
35
+
36
+ export default TopicSelector;
crossword-app/frontend/src/hooks/useCrossword.js ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback } from 'react';
2
+
3
+ const useCrossword = () => {
4
+ const [puzzle, setPuzzle] = useState(null);
5
+ const [loading, setLoading] = useState(false);
6
+ const [error, setError] = useState(null);
7
+ const [topics, setTopics] = useState([]);
8
+
9
+ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000';
10
+
11
+ const fetchTopics = useCallback(async () => {
12
+ try {
13
+ setLoading(true);
14
+ const response = await fetch(`${API_BASE_URL}/api/topics`);
15
+ if (!response.ok) throw new Error('Failed to fetch topics');
16
+ const data = await response.json();
17
+ setTopics(data);
18
+ } catch (err) {
19
+ setError(err.message);
20
+ } finally {
21
+ setLoading(false);
22
+ }
23
+ }, [API_BASE_URL]);
24
+
25
+ const generatePuzzle = useCallback(async (selectedTopics, difficulty = 'medium') => {
26
+ try {
27
+ setLoading(true);
28
+ setError(null);
29
+
30
+ const response = await fetch(`${API_BASE_URL}/api/generate`, {
31
+ method: 'POST',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ },
35
+ body: JSON.stringify({
36
+ topics: selectedTopics,
37
+ difficulty
38
+ })
39
+ });
40
+
41
+ if (!response.ok) throw new Error('Failed to generate puzzle');
42
+
43
+ const puzzleData = await response.json();
44
+ setPuzzle(puzzleData);
45
+ return puzzleData;
46
+ } catch (err) {
47
+ setError(err.message);
48
+ return null;
49
+ } finally {
50
+ setLoading(false);
51
+ }
52
+ }, [API_BASE_URL]);
53
+
54
+ const validateAnswers = useCallback(async (userAnswers) => {
55
+ try {
56
+ const response = await fetch(`${API_BASE_URL}/api/validate`, {
57
+ method: 'POST',
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ body: JSON.stringify({
62
+ puzzle: puzzle,
63
+ answers: userAnswers
64
+ })
65
+ });
66
+
67
+ if (!response.ok) throw new Error('Failed to validate answers');
68
+
69
+ return await response.json();
70
+ } catch (err) {
71
+ setError(err.message);
72
+ return null;
73
+ }
74
+ }, [API_BASE_URL, puzzle]);
75
+
76
+ const resetPuzzle = useCallback(() => {
77
+ setPuzzle(null);
78
+ setError(null);
79
+ }, []);
80
+
81
+ return {
82
+ puzzle,
83
+ loading,
84
+ error,
85
+ topics,
86
+ fetchTopics,
87
+ generatePuzzle,
88
+ validateAnswers,
89
+ resetPuzzle
90
+ };
91
+ };
92
+
93
+ export default useCrossword;
crossword-app/frontend/src/main.jsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.jsx'
4
+
5
+ ReactDOM.createRoot(document.getElementById('root')).render(
6
+ <React.StrictMode>
7
+ <App />
8
+ </React.StrictMode>,
9
+ )
crossword-app/frontend/src/styles/puzzle.css ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Crossword Puzzle Styles */
2
+
3
+ .crossword-app {
4
+ max-width: 1200px;
5
+ margin: 0 auto;
6
+ padding: 20px;
7
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
8
+ }
9
+
10
+ .app-header {
11
+ text-align: center;
12
+ margin-bottom: 30px;
13
+ }
14
+
15
+ .app-title {
16
+ color: #2c3e50;
17
+ font-size: 2.5rem;
18
+ margin-bottom: 10px;
19
+ }
20
+
21
+ /* Topic Selector Styles */
22
+ .topic-selector {
23
+ background: #f8f9fa;
24
+ padding: 20px;
25
+ border-radius: 8px;
26
+ margin-bottom: 20px;
27
+ }
28
+
29
+ .topic-selector h3 {
30
+ margin-top: 0;
31
+ color: #2c3e50;
32
+ }
33
+
34
+ .topic-buttons {
35
+ display: flex;
36
+ flex-wrap: wrap;
37
+ gap: 10px;
38
+ margin-bottom: 15px;
39
+ }
40
+
41
+ .topic-btn {
42
+ padding: 8px 16px;
43
+ border: 2px solid #3498db;
44
+ background: white;
45
+ color: #3498db;
46
+ border-radius: 20px;
47
+ cursor: pointer;
48
+ transition: all 0.3s ease;
49
+ font-weight: 500;
50
+ }
51
+
52
+ .topic-btn:hover {
53
+ background: #3498db;
54
+ color: white;
55
+ }
56
+
57
+ .topic-btn.selected {
58
+ background: #3498db;
59
+ color: white;
60
+ }
61
+
62
+ .selected-count {
63
+ color: #7f8c8d;
64
+ font-size: 0.9rem;
65
+ margin: 0;
66
+ }
67
+
68
+ /* Puzzle Controls */
69
+ .puzzle-controls {
70
+ display: flex;
71
+ gap: 15px;
72
+ margin-bottom: 20px;
73
+ justify-content: center;
74
+ }
75
+
76
+ .control-btn {
77
+ padding: 10px 20px;
78
+ border: none;
79
+ border-radius: 5px;
80
+ cursor: pointer;
81
+ font-weight: 600;
82
+ transition: background-color 0.3s ease;
83
+ }
84
+
85
+ .generate-btn {
86
+ background: #27ae60;
87
+ color: white;
88
+ }
89
+
90
+ .generate-btn:hover {
91
+ background: #229954;
92
+ }
93
+
94
+ .generate-btn:disabled {
95
+ background: #bdc3c7;
96
+ cursor: not-allowed;
97
+ }
98
+
99
+ .reset-btn {
100
+ background: #e74c3c;
101
+ color: white;
102
+ }
103
+
104
+ .reset-btn:hover {
105
+ background: #c0392b;
106
+ }
107
+
108
+ .reveal-btn {
109
+ background: #f39c12;
110
+ color: white;
111
+ }
112
+
113
+ .reveal-btn:hover {
114
+ background: #e67e22;
115
+ }
116
+
117
+ /* Loading Spinner */
118
+ .loading-spinner {
119
+ display: flex;
120
+ flex-direction: column;
121
+ align-items: center;
122
+ padding: 40px;
123
+ }
124
+
125
+ .spinner {
126
+ width: 40px;
127
+ height: 40px;
128
+ border: 4px solid #f3f3f3;
129
+ border-top: 4px solid #3498db;
130
+ border-radius: 50%;
131
+ animation: spin 1s linear infinite;
132
+ margin-bottom: 15px;
133
+ }
134
+
135
+ @keyframes spin {
136
+ 0% { transform: rotate(0deg); }
137
+ 100% { transform: rotate(360deg); }
138
+ }
139
+
140
+ .loading-message {
141
+ color: #7f8c8d;
142
+ font-size: 1.1rem;
143
+ }
144
+
145
+ /* Puzzle Layout */
146
+ .puzzle-layout {
147
+ display: grid;
148
+ grid-template-columns: 1fr 300px;
149
+ gap: 30px;
150
+ margin-top: 20px;
151
+ }
152
+
153
+ @media (max-width: 768px) {
154
+ .puzzle-layout {
155
+ grid-template-columns: 1fr;
156
+ gap: 20px;
157
+ }
158
+ }
159
+
160
+ /* Puzzle Grid */
161
+ .puzzle-container {
162
+ display: flex;
163
+ justify-content: center;
164
+ }
165
+
166
+ .puzzle-grid {
167
+ display: grid;
168
+ gap: 0;
169
+ margin: 0 auto;
170
+ width: fit-content;
171
+ height: fit-content;
172
+ border: 2px solid #2c3e50;
173
+ border-radius: 4px;
174
+ }
175
+
176
+ .grid-cell {
177
+ width: 35px;
178
+ height: 35px;
179
+ position: relative;
180
+ display: flex;
181
+ align-items: center;
182
+ justify-content: center;
183
+ box-sizing: border-box;
184
+ background: white;
185
+ }
186
+
187
+ .grid-cell::before {
188
+ content: '';
189
+ position: absolute;
190
+ top: 0;
191
+ left: 0;
192
+ right: 0;
193
+ bottom: 0;
194
+ border: 1px solid #2c3e50;
195
+ pointer-events: none;
196
+ z-index: 10;
197
+ }
198
+
199
+ .black-cell {
200
+ background: #2c3e50;
201
+ }
202
+
203
+ .black-cell::before {
204
+ background: #2c3e50;
205
+ border: 1px solid #2c3e50;
206
+ }
207
+
208
+ .white-cell {
209
+ background: white;
210
+ }
211
+
212
+ .cell-input {
213
+ width: 100%;
214
+ height: 100%;
215
+ border: none !important;
216
+ text-align: center;
217
+ font-size: 16px;
218
+ font-weight: bold;
219
+ background: transparent;
220
+ outline: none;
221
+ text-transform: uppercase;
222
+ position: relative;
223
+ z-index: 5;
224
+ }
225
+
226
+ .cell-input:focus {
227
+ background: #e8f4fd;
228
+ box-shadow: inset 0 0 0 2px #3498db;
229
+ }
230
+
231
+ .cell-number {
232
+ position: absolute;
233
+ top: 1px;
234
+ left: 2px;
235
+ font-size: 10px;
236
+ font-weight: bold;
237
+ color: #2c3e50;
238
+ line-height: 1;
239
+ z-index: 15;
240
+ pointer-events: none;
241
+ }
242
+
243
+ .solution-text {
244
+ color: #e74c3c !important;
245
+ font-weight: bold !important;
246
+ background: #fef9f9 !important;
247
+ }
248
+
249
+ .solution-text:disabled {
250
+ opacity: 1 !important;
251
+ cursor: default;
252
+ }
253
+
254
+
255
+ /* Specifically for solution state */
256
+ .grid-cell .solution-text {
257
+ border: none !important;
258
+ background: #fef9f9 !important;
259
+ }
260
+
261
+ /* Clue List */
262
+ .clue-list {
263
+ background: #f8f9fa;
264
+ padding: 20px;
265
+ border-radius: 8px;
266
+ max-height: 600px;
267
+ overflow-y: auto;
268
+ }
269
+
270
+ .clue-section {
271
+ margin-bottom: 25px;
272
+ }
273
+
274
+ .clue-section h4 {
275
+ color: #2c3e50;
276
+ margin-bottom: 15px;
277
+ font-size: 1.2rem;
278
+ border-bottom: 2px solid #3498db;
279
+ padding-bottom: 5px;
280
+ }
281
+
282
+ .clue-section ol {
283
+ padding-left: 0;
284
+ list-style: none;
285
+ }
286
+
287
+ .clue-item {
288
+ display: flex;
289
+ margin-bottom: 8px;
290
+ padding: 8px;
291
+ border-radius: 4px;
292
+ cursor: pointer;
293
+ transition: background-color 0.2s ease;
294
+ }
295
+
296
+ .clue-item:hover {
297
+ background: #e9ecef;
298
+ }
299
+
300
+ .clue-number {
301
+ font-weight: bold;
302
+ color: #3498db;
303
+ margin-right: 10px;
304
+ min-width: 25px;
305
+ }
306
+
307
+ .clue-text {
308
+ flex: 1;
309
+ color: #2c3e50;
310
+ }
311
+
312
+ /* Error Messages */
313
+ .error-message {
314
+ background: #f8d7da;
315
+ color: #721c24;
316
+ padding: 15px;
317
+ border-radius: 5px;
318
+ margin: 20px 0;
319
+ border: 1px solid #f5c6cb;
320
+ }
321
+
322
+ /* Success Messages */
323
+ .success-message {
324
+ background: #d4edda;
325
+ color: #155724;
326
+ padding: 15px;
327
+ border-radius: 5px;
328
+ margin: 20px 0;
329
+ border: 1px solid #c3e6cb;
330
+ text-align: center;
331
+ font-weight: 600;
332
+ }
crossword-app/frontend/src/utils/gridHelpers.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const formatGridForDisplay = (grid) => {
2
+ if (!grid || !Array.isArray(grid)) return [];
3
+
4
+ return grid.map(row =>
5
+ row.map(cell => cell === '.' ? '.' : cell.toUpperCase())
6
+ );
7
+ };
8
+
9
+ export const validateUserInput = (input) => {
10
+ return /^[A-Za-z]?$/.test(input);
11
+ };
12
+
13
+ export const checkPuzzleCompletion = (grid, userAnswers) => {
14
+ if (!grid || !userAnswers) return false;
15
+
16
+ for (let row = 0; row < grid.length; row++) {
17
+ for (let col = 0; col < grid[row].length; col++) {
18
+ if (grid[row][col] !== '.') {
19
+ const key = `${row}-${col}`;
20
+ if (!userAnswers[key] || userAnswers[key] !== grid[row][col]) {
21
+ return false;
22
+ }
23
+ }
24
+ }
25
+ }
26
+ return true;
27
+ };
28
+
29
+ export const getWordPositions = (grid, clues) => {
30
+ const positions = {};
31
+
32
+ clues.forEach(clue => {
33
+ positions[`${clue.number}-${clue.direction}`] = {
34
+ start: clue.position,
35
+ length: clue.word.length,
36
+ direction: clue.direction
37
+ };
38
+ });
39
+
40
+ return positions;
41
+ };
42
+
43
+ export const highlightWord = (wordPositions, selectedClue) => {
44
+ if (!selectedClue || !wordPositions[selectedClue]) return [];
45
+
46
+ const { start, length, direction } = wordPositions[selectedClue];
47
+ const cells = [];
48
+
49
+ for (let i = 0; i < length; i++) {
50
+ if (direction === 'across') {
51
+ cells.push(`${start.row}-${start.col + i}`);
52
+ } else {
53
+ cells.push(`${start.row + i}-${start.col}`);
54
+ }
55
+ }
56
+
57
+ return cells;
58
+ };
crossword-app/frontend/vite.config.js ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ host: true,
9
+ proxy: {
10
+ '/api': {
11
+ target: 'http://localhost:3000',
12
+ changeOrigin: true,
13
+ secure: false,
14
+ }
15
+ }
16
+ },
17
+ build: {
18
+ outDir: 'dist',
19
+ sourcemap: true,
20
+ rollupOptions: {
21
+ output: {
22
+ manualChunks: {
23
+ vendor: ['react', 'react-dom']
24
+ }
25
+ }
26
+ }
27
+ },
28
+ define: {
29
+ 'process.env': process.env
30
+ }
31
+ })
docs/TODO.md ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Crossword Puzzle Webapp - TODO List
2
+
3
+ ## Implementation Tasks
4
+
5
+ ### Phase 1: Project Setup
6
+ - [ ] Set up project structure and initialize repositories
7
+ - Create frontend/ and backend/ directories
8
+ - Initialize package.json files
9
+ - Set up Git repository
10
+ - Configure ESLint and Prettier
11
+
12
+ ### Phase 2: Database & Data Management
13
+ - [ ] Create database schema and seed data
14
+ - Design PostgreSQL tables (Topics, Words, Clues)
15
+ - Create migration scripts
16
+ - Seed database with initial word lists by topic
17
+ - Set up Prisma ORM configuration
18
+
19
+ ### Phase 3: Core Algorithm Development
20
+ - [ ] Implement crossword generation algorithm
21
+ - Word selection logic based on topics
22
+ - Grid placement algorithm with intersections
23
+ - Backtracking for conflict resolution
24
+ - Grid optimization (minimize size, maximize density)
25
+
26
+ ### Phase 4: Backend API
27
+ - [ ] Build backend API endpoints
28
+ - GET /topics - List available topics
29
+ - POST /generate - Generate crossword puzzle
30
+ - POST /validate - Validate user answers
31
+ - Error handling and validation middleware
32
+ - CORS configuration
33
+
34
+ ### Phase 5: Frontend Components
35
+ - [ ] Create frontend components (TopicSelector, PuzzleGrid, ClueList)
36
+ - TopicSelector with multi-select functionality
37
+ - PuzzleGrid with CSS Grid layout
38
+ - ClueList component (Across/Down)
39
+ - LoadingSpinner for generation feedback
40
+ - PuzzleControls (Reset/New/Difficulty)
41
+
42
+ ### Phase 6: Interactive Features
43
+ - [ ] Implement interactive crossword grid functionality
44
+ - Click-to-focus input cells
45
+ - Keyboard navigation (arrow keys, tab)
46
+ - Real-time input validation
47
+ - Highlight current word being filled
48
+ - Auto-advance to next cell
49
+
50
+ ### Phase 7: User Experience
51
+ - [ ] Add puzzle validation and user input handling
52
+ - Check answers against solution
53
+ - Provide visual feedback (correct/incorrect)
54
+ - Show completion status
55
+ - Reset and new puzzle functionality
56
+
57
+ ### Phase 8: Styling & Polish
58
+ - [ ] Style the application and improve UX
59
+ - Responsive design for mobile/desktop
60
+ - Professional color scheme
61
+ - Smooth animations and transitions
62
+ - Loading states and error messages
63
+ - Print-friendly styles
64
+
65
+ ### Phase 9: Environment Setup
66
+ - [ ] Set up development and production environments
67
+ - Configure environment variables
68
+ - Set up local development database
69
+ - Configure build scripts
70
+ - Set up CI/CD pipeline with GitHub Actions
71
+
72
+ ### Phase 10: Deployment
73
+ - [ ] Deploy application and test end-to-end
74
+ - Deploy backend to Railway/Heroku
75
+ - Deploy frontend to Vercel/Netlify
76
+ - Configure production database
77
+ - End-to-end testing
78
+ - Performance optimization
79
+
80
+ ## Detailed Sub-tasks
81
+
82
+ ### Technical Specifications
83
+ - **Database Schema**: See `database-schema.sql`
84
+ - **API Endpoints**: See `api-specification.md`
85
+
86
+ ### Component Props
87
+ ```javascript
88
+ // TopicSelector.jsx
89
+ const TopicSelector = ({ topics, selectedTopics, onTopicChange, loading }) => {}
90
+
91
+ // PuzzleGrid.jsx
92
+ const PuzzleGrid = ({ grid, onCellChange, currentCell, answers }) => {}
93
+
94
+ // ClueList.jsx
95
+ const ClueList = ({ clues, direction, onClueClick, completedClues }) => {}
96
+ ```
97
+
98
+ ### Key Files to Create
99
+ - `backend/src/services/crosswordGenerator.js` - Core algorithm
100
+ - `backend/src/controllers/puzzleController.js` - API handlers
101
+ - `frontend/src/hooks/useCrossword.js` - State management
102
+ - `frontend/src/utils/gridHelpers.js` - Grid utilities
103
+ - `frontend/src/styles/puzzle.css` - Crossword styling
104
+
105
+ ## Priority Order
106
+ 1. Setup project structure and basic backend
107
+ 2. Implement word placement algorithm
108
+ 3. Create basic frontend with grid display
109
+ 4. Add topic selection and API integration
110
+ 5. Implement interactive features
111
+ 6. Polish UI/UX and deploy
docs/api-specification.md ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Crossword Puzzle API Specification
2
+
3
+ ## Base URL
4
+ - Development: `http://localhost:3000/api`
5
+ - Production: `https://your-backend-domain.com/api`
6
+
7
+ ## Authentication
8
+ No authentication required for this MVP version.
9
+
10
+ ## Endpoints
11
+
12
+ ### GET /topics
13
+ Retrieve all available topics for crossword generation.
14
+
15
+ **Response:**
16
+ ```json
17
+ {
18
+ "topics": [
19
+ {
20
+ "id": 1,
21
+ "name": "Animals",
22
+ "description": "Creatures from the animal kingdom"
23
+ },
24
+ {
25
+ "id": 2,
26
+ "name": "Science",
27
+ "description": "Scientific terms and concepts"
28
+ }
29
+ ]
30
+ }
31
+ ```
32
+
33
+ **Status Codes:**
34
+ - `200 OK` - Success
35
+ - `500 Internal Server Error` - Database error
36
+
37
+ ---
38
+
39
+ ### POST /generate
40
+ Generate a new crossword puzzle based on selected topics and difficulty.
41
+
42
+ **Request Body:**
43
+ ```json
44
+ {
45
+ "topics": ["Animals", "Science"],
46
+ "difficulty": "medium",
47
+ "gridSize": 15
48
+ }
49
+ ```
50
+
51
+ **Parameters:**
52
+ - `topics` (string[]): Array of topic names to include
53
+ - `difficulty` (string): "easy" | "medium" | "hard"
54
+ - `gridSize` (number, optional): Grid size (default: 15)
55
+
56
+ **Response:**
57
+ ```json
58
+ {
59
+ "puzzle": {
60
+ "id": "puzzle_123",
61
+ "grid": [
62
+ [
63
+ { "letter": "D", "number": 1, "isBlocked": false },
64
+ { "letter": "", "number": null, "isBlocked": true }
65
+ ]
66
+ ],
67
+ "clues": {
68
+ "across": [
69
+ {
70
+ "number": 1,
71
+ "clue": "Man's best friend",
72
+ "answer": "DOG",
73
+ "startRow": 0,
74
+ "startCol": 0,
75
+ "length": 3
76
+ }
77
+ ],
78
+ "down": [
79
+ {
80
+ "number": 2,
81
+ "clue": "Feline pet",
82
+ "answer": "CAT",
83
+ "startRow": 0,
84
+ "startCol": 2,
85
+ "length": 3
86
+ }
87
+ ]
88
+ },
89
+ "metadata": {
90
+ "difficulty": "medium",
91
+ "topics": ["Animals"],
92
+ "wordCount": 8,
93
+ "generatedAt": "2024-01-15T10:30:00Z"
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ **Status Codes:**
100
+ - `200 OK` - Puzzle generated successfully
101
+ - `400 Bad Request` - Invalid request parameters
102
+ - `422 Unprocessable Entity` - Unable to generate puzzle with given constraints
103
+ - `500 Internal Server Error` - Generation failed
104
+
105
+ ---
106
+
107
+ ### POST /validate
108
+ Validate user answers against the puzzle solution.
109
+
110
+ **Request Body:**
111
+ ```json
112
+ {
113
+ "puzzleId": "puzzle_123",
114
+ "answers": {
115
+ "1_across": "DOG",
116
+ "2_down": "CAT"
117
+ }
118
+ }
119
+ ```
120
+
121
+ **Response:**
122
+ ```json
123
+ {
124
+ "validation": {
125
+ "isComplete": false,
126
+ "correctCount": 1,
127
+ "totalCount": 2,
128
+ "results": {
129
+ "1_across": {
130
+ "correct": true,
131
+ "expected": "DOG",
132
+ "provided": "DOG"
133
+ },
134
+ "2_down": {
135
+ "correct": false,
136
+ "expected": "CAT",
137
+ "provided": "CAR"
138
+ }
139
+ }
140
+ }
141
+ }
142
+ ```
143
+
144
+ **Status Codes:**
145
+ - `200 OK` - Validation completed
146
+ - `400 Bad Request` - Invalid puzzle ID or answers format
147
+ - `404 Not Found` - Puzzle not found
148
+ - `500 Internal Server Error` - Validation failed
149
+
150
+ ---
151
+
152
+ ### GET /words/:topic (Admin/Development)
153
+ Retrieve all words for a specific topic. Useful for development and debugging.
154
+
155
+ **Parameters:**
156
+ - `topic` (string): Topic name
157
+
158
+ **Response:**
159
+ ```json
160
+ {
161
+ "topic": "Animals",
162
+ "words": [
163
+ {
164
+ "id": 1,
165
+ "word": "DOG",
166
+ "length": 3,
167
+ "difficulty": 1,
168
+ "clues": [
169
+ {
170
+ "id": 1,
171
+ "text": "Man's best friend",
172
+ "difficulty": 1
173
+ }
174
+ ]
175
+ }
176
+ ]
177
+ }
178
+ ```
179
+
180
+ **Status Codes:**
181
+ - `200 OK` - Words retrieved
182
+ - `404 Not Found` - Topic not found
183
+ - `500 Internal Server Error` - Database error
184
+
185
+ ## Error Response Format
186
+
187
+ All error responses follow this format:
188
+
189
+ ```json
190
+ {
191
+ "error": {
192
+ "code": "VALIDATION_ERROR",
193
+ "message": "Invalid difficulty level",
194
+ "details": {
195
+ "field": "difficulty",
196
+ "allowedValues": ["easy", "medium", "hard"]
197
+ }
198
+ }
199
+ }
200
+ ```
201
+
202
+ ## Rate Limiting
203
+
204
+ - Generate endpoint: 10 requests per minute per IP
205
+ - Other endpoints: 100 requests per minute per IP
206
+
207
+ ## CORS Configuration
208
+
209
+ - Allowed origins: Frontend domain(s)
210
+ - Allowed methods: GET, POST, OPTIONS
211
+ - Allowed headers: Content-Type, Authorization
212
+
213
+ ## Data Types
214
+
215
+ ### Grid Cell
216
+ ```typescript
217
+ interface GridCell {
218
+ letter: string; // Empty string for user input cells
219
+ number: number | null; // Clue number if cell starts a word
220
+ isBlocked: boolean; // True for black squares
221
+ }
222
+ ```
223
+
224
+ ### Clue
225
+ ```typescript
226
+ interface Clue {
227
+ number: number; // Clue number
228
+ clue: string; // Clue text
229
+ answer: string; // Expected answer
230
+ startRow: number; // Starting row in grid
231
+ startCol: number; // Starting column in grid
232
+ length: number; // Word length
233
+ }
234
+ ```
235
+
236
+ ### Puzzle Metadata
237
+ ```typescript
238
+ interface PuzzleMetadata {
239
+ difficulty: 'easy' | 'medium' | 'hard';
240
+ topics: string[];
241
+ wordCount: number;
242
+ generatedAt: string; // ISO timestamp
243
+ }
244
+ ```
docs/crossword-app-plan.md ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Crossword Puzzle Webapp - Complete Implementation Plan
2
+
3
+ ## Architecture Overview
4
+
5
+ **Frontend (React/Vue/Vanilla JS)**
6
+ - Topic selection dropdown/buttons
7
+ - Generate puzzle button
8
+ - Interactive crossword grid display
9
+ - Clue lists (across/down)
10
+
11
+ **Backend (Node.js/Python/Go)**
12
+ - REST API endpoints for puzzle generation
13
+ - Crossword algorithm implementation
14
+ - Word/clue database management
15
+
16
+ **Database**
17
+ - Word collections organized by topics
18
+ - Clue-answer pairs
19
+ - Pre-generated grids (optional caching)
20
+
21
+ ## Core Components
22
+
23
+ 1. **Topic Management**: Categories like "Animals", "Science", "History"
24
+ 2. **Word Selection**: Algorithm to pick words from chosen topic
25
+ 3. **Grid Generation**: Place words intersecting on a grid
26
+ 4. **Clue Generation**: Match words with appropriate clues
27
+ 5. **UI Rendering**: Display interactive puzzle with input fields
28
+
29
+ ## Key Algorithms Needed
30
+
31
+ - **Grid placement**: Find valid intersections between words
32
+ - **Backtracking**: Handle conflicts when placing words
33
+ - **Difficulty scaling**: Adjust grid size and word complexity
34
+
35
+ ## Tech Stack Suggestions
36
+
37
+ - **Frontend**: React + CSS Grid for puzzle layout
38
+ - **Backend**: Node.js + Express or Python + Flask
39
+ - **Database**: PostgreSQL or MongoDB for word storage
40
+ - **Deployment**: Vercel/Netlify + Railway/Heroku
41
+
42
+ ## Frontend Components & UI
43
+
44
+ **Main Page Layout**
45
+ ```
46
+ Header: "Crossword Puzzle Generator"
47
+ Topic Selector: Dropdown with categories
48
+ Generate Button: "Create Puzzle"
49
+ Loading State: Spinner during generation
50
+ Puzzle Display: Grid + Clues
51
+ Actions: Reset, New Puzzle, Print
52
+ ```
53
+
54
+ **Components:**
55
+ - `TopicSelector`: Multi-select topics
56
+ - `PuzzleGrid`: Interactive crossword grid
57
+ - `ClueList`: Numbered clues (Across/Down)
58
+ - `LoadingSpinner`: Generation feedback
59
+ - `PuzzleControls`: Reset/New/Difficulty buttons
60
+
61
+ **UI Flow:**
62
+ 1. User selects topic(s)
63
+ 2. Clicks generate → Loading state
64
+ 3. Puzzle renders with empty grid
65
+ 4. User fills in answers
66
+ 5. Real-time validation feedback
67
+
68
+ ## Backend API & Crossword Generation
69
+
70
+ **API Endpoints:**
71
+ ```
72
+ GET /topics - List available topics
73
+ POST /generate - Generate puzzle
74
+ Body: { topics: string[], difficulty: 'easy'|'medium'|'hard' }
75
+ Response: { grid: Cell[][], clues: Clue[], metadata: {} }
76
+
77
+ GET /words/:topic - Get words for topic (admin)
78
+ POST /validate - Validate user answers
79
+ ```
80
+
81
+ **Core Algorithm:**
82
+ 1. **Word Selection**: Pick 8-15 words from chosen topics
83
+ 2. **Grid Placement**:
84
+ - Start with longest word horizontally
85
+ - Find intersections for perpendicular words
86
+ - Use backtracking for conflicts
87
+ 3. **Grid Optimization**: Minimize grid size, maximize word density
88
+ 4. **Clue Matching**: Pair each word with appropriate clue
89
+
90
+ **Generation Logic:**
91
+ ```javascript
92
+ class CrosswordGenerator {
93
+ generatePuzzle(topics, difficulty) {
94
+ const words = selectWords(topics, difficulty)
95
+ const grid = createEmptyGrid()
96
+ const placed = placeWords(words, grid)
97
+ return { grid, clues: generateClues(placed) }
98
+ }
99
+ }
100
+ ```
101
+
102
+ ## Data Storage & Word Management
103
+
104
+ **Database Schema:**
105
+ ```sql
106
+ Topics: id, name, description
107
+ Words: id, word, length, difficulty_level, topic_id
108
+ Clues: id, word_id, clue_text, difficulty
109
+ Generated_Puzzles: id, grid_data, clues_data, created_at (optional caching)
110
+ ```
111
+
112
+ **Word Collections by Topic:**
113
+ - **Animals**: DOG, ELEPHANT, TIGER, WHALE, BUTTERFLY
114
+ - **Science**: ATOM, GRAVITY, MOLECULE, PHOTON, CHEMISTRY
115
+ - **Geography**: MOUNTAIN, OCEAN, DESERT, CONTINENT, RIVER
116
+ - **Technology**: COMPUTER, INTERNET, ALGORITHM, DATABASE, SOFTWARE
117
+
118
+ **Data Sources:**
119
+ - Curated word lists with quality clues
120
+ - Dictionary APIs for definitions
121
+ - Wikipedia API for topic-specific terms
122
+ - Manual curation for puzzle quality
123
+
124
+ **Storage Strategy:**
125
+ - PostgreSQL for structured word/clue data
126
+ - JSON columns for flexible puzzle metadata
127
+ - Indexing on topic_id and word_length for fast queries
128
+ - Caching layer (Redis) for frequent topic combinations
129
+
130
+ ## Project Structure
131
+
132
+ ```
133
+ crossword-app/
134
+ ├── frontend/
135
+ │ ├── src/
136
+ │ │ ├── components/
137
+ │ │ │ ├── TopicSelector.jsx
138
+ │ │ │ ├── PuzzleGrid.jsx
139
+ │ │ │ ├── ClueList.jsx
140
+ │ │ │ └── LoadingSpinner.jsx
141
+ │ │ ├── hooks/
142
+ │ │ │ └── useCrossword.js
143
+ │ │ ├── utils/
144
+ │ │ │ └── gridHelpers.js
145
+ │ │ ├── styles/
146
+ │ │ │ └── puzzle.css
147
+ │ │ └── App.jsx
148
+ │ ├── package.json
149
+ │ └── vite.config.js
150
+ ├── backend/
151
+ │ ├── src/
152
+ │ │ ├── controllers/
153
+ │ │ │ └── puzzleController.js
154
+ │ │ ├── services/
155
+ │ │ │ ├── crosswordGenerator.js
156
+ │ │ │ └── wordService.js
157
+ │ │ ├── models/
158
+ │ │ │ ├── Word.js
159
+ │ │ │ └── Topic.js
160
+ │ │ ├── routes/
161
+ │ │ │ └── api.js
162
+ │ │ └── app.js
163
+ │ ├── data/
164
+ │ │ └── word-lists/
165
+ │ ├── package.json
166
+ │ └── .env
167
+ └── database/
168
+ ├── migrations/
169
+ └── seeds/
170
+ ```
171
+
172
+ **Tech Stack:**
173
+ - **Frontend**: React + Vite, CSS Grid, Axios
174
+ - **Backend**: Node.js + Express, CORS enabled
175
+ - **Database**: PostgreSQL with Prisma ORM
176
+ - **Development**: Nodemon, ESLint, Prettier
177
+
178
+ ## Deployment & Hosting Strategy
179
+
180
+ **Development Environment:**
181
+ - Local PostgreSQL database
182
+ - Frontend: `npm run dev` (Vite dev server)
183
+ - Backend: `npm run dev` (Nodemon)
184
+ - Environment variables in `.env`
185
+
186
+ **Production Deployment:**
187
+ - **Frontend**: Vercel or Netlify (static hosting)
188
+ - **Backend**: Railway, Heroku, or DigitalOcean App Platform
189
+ - **Database**: PostgreSQL on Railway/Heroku/AWS RDS
190
+ - **Domain**: Custom domain with HTTPS
191
+
192
+ **CI/CD Pipeline:**
193
+ - GitHub Actions for automated testing
194
+ - Deploy on push to main branch
195
+ - Environment-specific configs (dev/staging/prod)
196
+
197
+ **Environment Variables:**
198
+ ```
199
+ DATABASE_URL=postgresql://...
200
+ JWT_SECRET=...
201
+ CORS_ORIGIN=https://your-frontend-domain.com
202
+ PORT=3000
203
+ ```
204
+
205
+ **Performance Considerations:**
206
+ - CDN for static assets
207
+ - Database connection pooling
208
+ - API rate limiting
209
+ - Puzzle result caching (Redis)
210
+
211
+ ## Implementation Priority
212
+
213
+ 1. **Phase 1**: Basic word placement algorithm and simple UI
214
+ 2. **Phase 2**: Topic selection and word database
215
+ 3. **Phase 3**: Interactive grid with validation
216
+ 4. **Phase 4**: Polish UI/UX and deployment
217
+ 5. **Phase 5**: Advanced features (difficulty levels, saving puzzles)
docs/database-schema.sql ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Crossword Puzzle Webapp Database Schema
2
+
3
+ -- Topics table
4
+ CREATE TABLE topics (
5
+ id SERIAL PRIMARY KEY,
6
+ name VARCHAR(50) NOT NULL UNIQUE,
7
+ description TEXT,
8
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
9
+ );
10
+
11
+ -- Words table
12
+ CREATE TABLE words (
13
+ id SERIAL PRIMARY KEY,
14
+ word VARCHAR(20) NOT NULL,
15
+ length INTEGER NOT NULL,
16
+ difficulty_level INTEGER DEFAULT 1 CHECK (difficulty_level BETWEEN 1 AND 3),
17
+ topic_id INTEGER REFERENCES topics(id) ON DELETE CASCADE,
18
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
19
+ INDEX idx_topic_length (topic_id, length),
20
+ INDEX idx_difficulty (difficulty_level)
21
+ );
22
+
23
+ -- Clues table
24
+ CREATE TABLE clues (
25
+ id SERIAL PRIMARY KEY,
26
+ word_id INTEGER REFERENCES words(id) ON DELETE CASCADE,
27
+ clue_text TEXT NOT NULL,
28
+ difficulty INTEGER DEFAULT 1 CHECK (difficulty BETWEEN 1 AND 3),
29
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
30
+ );
31
+
32
+ -- Optional: Generated puzzles cache table
33
+ CREATE TABLE generated_puzzles (
34
+ id SERIAL PRIMARY KEY,
35
+ grid_data JSONB NOT NULL,
36
+ clues_data JSONB NOT NULL,
37
+ topics TEXT[] NOT NULL,
38
+ difficulty INTEGER DEFAULT 1,
39
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
40
+ expires_at TIMESTAMP DEFAULT (CURRENT_TIMESTAMP + INTERVAL '24 hours')
41
+ );
42
+
43
+ -- Sample data seeds
44
+ INSERT INTO topics (name, description) VALUES
45
+ ('Animals', 'Creatures from the animal kingdom'),
46
+ ('Science', 'Scientific terms and concepts'),
47
+ ('Geography', 'Places, landforms, and geographical features'),
48
+ ('Technology', 'Computing and technology terms'),
49
+ ('History', 'Historical events, people, and periods'),
50
+ ('Sports', 'Sports, games, and athletic activities');
51
+
52
+ -- Sample words for Animals topic
53
+ INSERT INTO words (word, length, difficulty_level, topic_id) VALUES
54
+ ('DOG', 3, 1, 1),
55
+ ('CAT', 3, 1, 1),
56
+ ('ELEPHANT', 8, 2, 1),
57
+ ('TIGER', 5, 1, 1),
58
+ ('WHALE', 5, 2, 1),
59
+ ('BUTTERFLY', 9, 3, 1),
60
+ ('PENGUIN', 7, 2, 1),
61
+ ('GIRAFFE', 7, 2, 1);
62
+
63
+ -- Sample clues for the words
64
+ INSERT INTO clues (word_id, clue_text, difficulty) VALUES
65
+ (1, 'Man''s best friend', 1),
66
+ (2, 'Feline pet that purrs', 1),
67
+ (3, 'Largest land mammal', 2),
68
+ (4, 'Striped big cat', 1),
69
+ (5, 'Largest marine mammal', 2),
70
+ (6, 'Colorful insect with wings', 3),
71
+ (7, 'Black and white Antarctic bird', 2),
72
+ (8, 'Tallest animal in the world', 2);