Generador de puzzles educativos para docentes y estudiantes. Creá puzzles, exportalos a PDF o compartilos como juegos digitales interactivos.
En producción: https://tools.edualex.uy
- React 19 + TypeScript 6
- Vite 8 como bundler
- Tailwind CSS 4 para estilos
- Zustand para estado global
- React Router 7 para navegación (BrowserRouter)
- jsPDF + html2canvas para exportación a PDF
- Lucide React para iconos
- React Toastify para notificaciones
- qrcode.react para generación local de QR
- nanoid para IDs cortos de puzzles
- @noble/hashes para Argon2id (hashing de contraseñas en Workers)
- vite-plugin-pwa + workbox-window para PWA instalable y offline
- tailwind-animations para animaciones de Tailwind
- Cloudflare Pages + Pages Functions + KV para backend y almacenamiento
- Cloudflare Durable Objects para seguimiento de progreso en vivo
| Puzzle | Ruta | Jugable |
|---|---|---|
| Sopa de letras | /sopa-de-letras |
/jugar/sopa-de-letras/:id |
| Crucigrama | /crucigrama |
/jugar/crucigrama/:id |
| Rellenar huecos | /rellenar-huecos |
/jugar/rellenar-huecos/:id |
| Adivina la palabra | /adivina-la-palabra |
/jugar/adivina-la-palabra/:id |
| Anagrama | /anagrama |
/jugar/anagrama/:id |
| Ordenar oración | /ordenar-oracion |
/jugar/ordenar-oracion/:id |
| Relacionar columnas | /relacionar-columnas |
/jugar/relacionar-columnas/:id |
| Memoria | /memoria |
/jugar/memoria/:id |
| Rosco | /rosco |
/jugar/rosco/:id |
| Cadenas de palabras | /cadenas-de-palabras |
/jugar/cadenas-de-palabras/:id |
pnpm installpnpm devLevanta el servidor en http://localhost:5173.
pnpm build
pnpm exec wrangler pages dev dist --kv=PUZZLES --kv=USERS --kv=SESSIONSLevanta el servidor en http://127.0.0.1:8788 con las Pages Functions y KV namespaces locales.
Para probar el flujo completo de autenticación (envío de emails), creá un archivo .dev.vars junto a wrangler.toml con tu clave. Este archivo no se versiona:
RESEND_API_KEY="tu_key_aqui"Luego iniciá Pages Functions normalmente:
pnpm build
pnpm exec wrangler pages dev dist --kv=PUZZLES --kv=USERS --kv=SESSIONSAlternativamente, para una única sesión de PowerShell:
$env:RESEND_API_KEY = "tu_key_aqui"
pnpm build
pnpm exec wrangler pages dev dist --kv=PUZZLES --kv=USERS --kv=SESSIONSpnpm buildCompila TypeScript y genera el bundle de producción en dist/.
pnpm lintTestean generadores de puzzles y utilidades. Corren con Vitest en entorno Node, sin dependencias externas.
pnpm test # Todos los tests unitarios
pnpm test:unit # Igual que arribaLevantan un servidor wrangler pages dev local con KV namespaces y prueban la API completa (crear/recuperar puzzles, auth, puzzles guardados, flujo de juego).
pnpm test:integrationEl script scripts/run-integration-tests.sh:
- Ejecuta
pnpm build - Levanta
wrangler pages dev distcon KV namespaces locales yENVIRONMENT=development - Espera a que el servidor responda
- Ejecuta vitest con
vitest.integration.config.ts - Detiene wrangler al terminar
pnpm test:all # Unit + Integrationsrc/
├── App.tsx # Router, registro de puzzles y checkSession
├── main.tsx # Entry point
├── index.css # Estilos globales (Tailwind)
├── components/
│ ├── layout/ # Header, Layout, PuzzlePageLayout, PlayableLayout
│ ├── playable/ # Componentes de juegos interactivos
│ ├── puzzles/ # Previews y inputs de cada puzzle
│ ├── auth/ # UserMenu, SavePuzzleButton
│ └── ui/ # Button, PageHeader, DownloadDropdown, ShareModal
├── hooks/ # Custom hooks (useAuth, usePuzzleLoader, useNoIndexMeta, etc.)
├── lib/
│ ├── puzzles/ # Generadores de cada puzzle (con types y registry)
│ ├── pdf/ # Generación de PDFs
│ ├── png/ # Exportación a PNG
│ └── share/ # api.ts (savePuzzle/loadPuzzle) + types.ts
├── pages/
│ ├── HomePage.tsx # Landing page
│ ├── LoginPage.tsx # Inicio de sesión
│ ├── SignupPage.tsx # Registro de cuenta
│ ├── VerifyPage.tsx # Verificación de email
│ ├── ForgotPasswordPage.tsx # Recuperar contraseña
│ ├── ResetPasswordPage.tsx # Nueva contraseña
│ ├── MyPuzzlesPage.tsx # Mis puzzles guardados
│ ├── PuzzleProgressPage.tsx # Progreso en vivo del docente
│ ├── play/ # Páginas jugables (/jugar/*)
│ └── NotFoundPage.tsx
└── store/ # Zustand stores (auth-store, saved-puzzles-store, puzzle-store)
functions/
├── api/
│ ├── auth/ # signup, login, logout, me, verify, forgot, reset
│ ├── account.ts # DELETE /api/account (eliminar cuenta)
│ ├── progress/[id].ts # POST /api/progress/:id (progreso de estudiantes)
│ └── puzzles/
│ ├── index.ts # POST /api/puzzles (crear puzzle)
│ ├── [id].ts # GET /api/puzzles/:id (obtener puzzle)
│ └── saved/[[path]].ts # GET/POST /api/puzzles/saved, GET/DELETE/:id, share, progreso
└── lib/ # Helpers (auth, email, rate-limit, env, types, validation, puzzle-id)
progress-worker/ # Worker separado con Durable Objects (progreso en vivo)
public/
├── _redirects # SPA routing (/* → /index.html 200)
├── _headers # Headers de seguridad (CSP, HSTS, etc.)
├── favicon.svg
├── robots.txt
├── sitemap.xml
├── llms.txt # Resumen para LLMs y herramientas de IA
├── pwa-192x192.png # Ícono PWA
└── pwa-512x512.png # Ícono PWA
| Ruta | Página |
|---|---|
/ |
Inicio |
/sopa-de-letras |
Generador de sopa de letras |
/crucigrama |
Generador de crucigramas |
/rellenar-huecos |
Generador de rellenar huecos |
/adivina-la-palabra |
Generador de ahorcado |
/anagrama |
Generador de anagrama |
/ordenar-oracion |
Generador de ordenar oración |
/relacionar-columnas |
Generador de relacionar columnas |
/memoria |
Generador de memoria |
/rosco |
Generador de rosco |
/cadenas-de-palabras |
Generador de cadenas de palabras (Wordle) |
| Ruta | Página |
|---|---|
/iniciar-sesion |
Inicio de sesión |
/crear-cuenta |
Registro de cuenta |
/verificar |
Verificación de email |
/recuperar-cuenta |
Recuperar contraseña |
/restablecer-contrasena |
Nueva contraseña |
/mis-puzzles |
Mis puzzles guardados |
/mis-puzzles/:id/progreso |
Progreso en vivo del puzzle |
| Ruta | Descripción |
|---|---|
/jugar |
Hub con input de código y demo interactiva |
/jugar/sopa-de-letras/:id |
Sopa de letras jugable |
/jugar/crucigrama/:id |
Crucigrama jugable |
/jugar/rellenar-huecos/:id |
Rellenar huecos jugable |
/jugar/adivina-la-palabra/:id |
Ahorcado jugable |
/jugar/anagrama/:id |
Anagrama jugable |
/jugar/ordenar-oracion/:id |
Ordenar oración jugable |
/jugar/relacionar-columnas/:id |
Relacionar columnas jugable |
/jugar/memoria/:id |
Memoria jugable |
/jugar/rosco/:id |
Rosco jugable |
/jugar/cadenas-de-palabras/:id |
Cadenas de palabras jugable |
| Method | Path | Descripción |
|---|---|---|
| POST | /api/puzzles |
Crear puzzle (body: { type, puzzle }) → retorna { id } |
| GET | /api/puzzles/:id |
Obtener puzzle → retorna { type, puzzle } o 404 |
| Method | Path | Descripción |
|---|---|---|
| POST | /api/auth/signup |
Crear cuenta (body: { email, password, displayName? }) |
| POST | /api/auth/login |
Iniciar sesión (body: { email, password }) |
| POST | /api/auth/logout |
Cerrar sesión |
| GET | /api/auth/me |
Obtener usuario autenticado |
| GET | /api/auth/verify |
Verificar email (query: ?token=xxx) |
| POST | /api/auth/forgot |
Solicitar reset de contraseña (body: { email }) |
| POST | /api/auth/reset |
Resetear contraseña (body: { token, newPassword }) |
| DELETE | /api/account |
Eliminar cuenta autenticada (body: { confirm: email }) |
| Method | Path | Descripción |
|---|---|---|
| POST | /api/puzzles/save |
Guardar puzzle (body: { type, title, data }) |
| GET | /api/puzzles/saved |
Listar puzzles guardados del usuario |
| GET | /api/puzzles/saved/:id |
Obtener puzzle guardado |
| DELETE | /api/puzzles/saved/:id |
Eliminar puzzle guardado |
| POST | /api/puzzles/saved/:id/share |
Compartir puzzle guardado |
| GET | /api/puzzles/saved/:id/progreso |
Ver progreso en vivo del docente |
| DELETE | /api/puzzles/saved/:id/progreso |
Borrar el progreso de la actividad |
| Method | Path | Descripción |
|---|---|---|
| POST | /api/progress/:id |
Reportar progreso anónimo (Sopa, Crucigrama, Rellenar huecos, Relacionar columnas, Memoria, Rosco y Cadenas de palabras) |
- Generación automática de puzzles a partir de texto o palabras ingresadas
- Exportación a PDF (versión estudiante y versión solución)
- Exportación a PNG
- Previsualización interactiva antes de descargar
- Registro con verificación por email
- Inicio/cierre de sesión
- Recuperación de contraseña
- Puzzles guardados persistentes en la cuenta del usuario
- Listado y gestión de puzzles guardados
- IDs cortos (8 caracteres) para URLs limpias y compartibles
- Backend en Cloudflare: Pages Functions + KV para almacenamiento
- QR local: generado con
qrcode.react(sin proxy externo) - Código de puzzle: los alumnos pueden ingresar solo el código en
/jugar - Expiración automática: los puzzles expiran en 1 día (24 horas)
- Contador de intentos por puzzle en localStorage
- Apodo anónimo y editable por dispositivo para el seguimiento docente
- Case-insensitive:
F3D78213yf3d78213funcionan igual
- Instalable como app en el escritorio y en el móvil (manifest + service worker)
- Funciona offline con el bundle precacheado (
vite-plugin-pwa+ Workbox) - Aviso de nueva versión disponible para recargar
- El docente genera el puzzle y hace clic en "Compartir"
- El sistema guarda el puzzle en KV y genera un ID de 8 caracteres
- Se muestra el QR, el código y el link completo
- El alumno puede:
- Escanear el QR
- Copiar el link completo
- Ingresar solo el código en
/jugar
- El docente genera el puzzle y hace clic en "Guardar"
- El puzzle se guarda en su cuenta (persistente)
- Puede acceder a sus puzzles desde
/mis-puzzles - Puede jugarlos, compartirlos o eliminarlos
- El estudiante abre un puzzle compartido compatible y confirma un apodo anónimo.
- El juego reporta los aciertos y la presencia cada 10 segundos.
- El docente abre Mis puzzles → Progreso para ver el avance, actualizado cada 4 segundos.
- Puzzles con seguimiento en vivo: Sopa de letras, Crucigrama, Rellenar huecos, Relacionar columnas, Memoria, Rosco y Cadenas de palabras.
- Crear la carpeta en
src/lib/puzzles/<nombre>/con ungenerator.tsque implemente la interfaz del registry. - Registrar el puzzle en
src/App.tsxconregisterPuzzle(). - Crear la página en
src/pages/<Nombre>Page.tsx. - Agregar la ruta en
App.tsxdentro del<Routes>. - Agregar la entrada en el array
TABSdesrc/components/layout/Header.tsx. - Agregar la card en
src/pages/HomePage.tsx. - Crear el componente jugable en
src/components/playable/<Nombre>Game.tsx. - Crear la página jugable en
src/pages/play/Play<Nombre>Page.tsx. - Agregar la ruta
/jugar/<nombre>/:idenApp.tsx. - Agregar tipos y conversores en
src/lib/share/types.ts. - Agregar el slug y la etiqueta en
src/lib/puzzles/slugs.ts(PUZZLE_TYPE_TO_SLUG,SLUG_TO_PUZZLE_TYPE,PUZZLE_TYPE_LABELS). - Agregar el tipo en
ALLOWED_TYPESenfunctions/api/puzzles/index.tsy enALLOWED_PUZZLE_TYPESenfunctions/lib/env.ts. - Si el puzzle es jugable en vivo, agregarlo en
functions/api/progress/[id].tsy en el Workerprogress-worker/si corresponde. - Actualizar
public/sitemap.xml,public/llms.txty este README.
El proyecto está desplegado en Cloudflare Pages con dominio propio tools.edualex.uy.
# Build de producción
pnpm build
# Deploy a Cloudflare Pages
pnpm exec wrangler pages deploy dist-
Crear KV namespace:
pnpm exec wrangler kv namespace create PUZZLES -
Configurar
wrangler.toml:name = "edutools" compatibility_date = "2025-01-01" pages_build_output_dir = "dist" [[kv_namespaces]] binding = "PUZZLES" id = "<TU_KV_NAMESPACE_ID>" preview_id = "<TU_KV_PREVIEW_ID>"
-
Configurar custom domain en Cloudflare Pages Dashboard:
- Ir a Settings → Custom domains
- Agregar
tools.edualex.uy
-
Configurar KV binding en Cloudflare Pages Dashboard:
- Ir a Settings → Functions → Bindings
- Agregar binding de tipo KV Namespace
- Variable name:
PUZZLES - Seleccionar el namespace creado
pnpm build
pnpm exec wrangler dev --config progress-worker/wrangler.jsonc --port 8787
pnpm exec wrangler pages dev dist --port 8788 --kv PUZZLES --kv USERS --kv SESSIONS --do PROGRESS=PuzzleProgress@edutools-progressPara tests de integración, ver sección "Tests".
El Worker separado progress-worker/ usa un Durable Object por puzzle para conservar el estado en tiempo real.
pnpm exec wrangler deploy --config progress-worker/wrangler.jsoncDespués verificá que wrangler.toml incluya el binding PROGRESS con class_name = "PuzzleProgress" y script_name = "edutools-progress". Los deploys automáticos de Pages en test y main usan ese binding; si cambia código dentro de progress-worker/, desplegalo nuevamente con el comando anterior.
alexcuadro — tools.edualex.uy