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]>
- .gitignore +42 -0
- README.md +85 -0
- crossword-app/README.md +209 -0
- crossword-app/backend/data/word-lists/animals.json +27 -0
- crossword-app/backend/data/word-lists/geography.json +32 -0
- crossword-app/backend/data/word-lists/science.json +32 -0
- crossword-app/backend/data/word-lists/technology.json +32 -0
- crossword-app/backend/package-lock.json +0 -0
- crossword-app/backend/package.json +49 -0
- crossword-app/backend/src/app.js +94 -0
- crossword-app/backend/src/controllers/puzzleController.js +65 -0
- crossword-app/backend/src/models/Topic.js +144 -0
- crossword-app/backend/src/models/Word.js +112 -0
- crossword-app/backend/src/routes/api.js +22 -0
- crossword-app/backend/src/services/crosswordGenerator.js +313 -0
- crossword-app/backend/src/services/wordService.js +123 -0
- crossword-app/database/migrations/001_initial_schema.sql +91 -0
- crossword-app/database/seeds/seed_data.sql +214 -0
- crossword-app/frontend/index.html +14 -0
- crossword-app/frontend/package-lock.json +0 -0
- crossword-app/frontend/package.json +42 -0
- crossword-app/frontend/src/App.jsx +127 -0
- crossword-app/frontend/src/components/ClueList.jsx +29 -0
- crossword-app/frontend/src/components/LoadingSpinner.jsx +12 -0
- crossword-app/frontend/src/components/PuzzleGrid.jsx +77 -0
- crossword-app/frontend/src/components/TopicSelector.jsx +36 -0
- crossword-app/frontend/src/hooks/useCrossword.js +93 -0
- crossword-app/frontend/src/main.jsx +9 -0
- crossword-app/frontend/src/styles/puzzle.css +332 -0
- crossword-app/frontend/src/utils/gridHelpers.js +58 -0
- crossword-app/frontend/vite.config.js +31 -0
- docs/TODO.md +111 -0
- docs/api-specification.md +244 -0
- docs/crossword-app-plan.md +217 -0
- 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);
|