diff --git a/index.html b/index.html index eb1a7ed..8234e80 100644 --- a/index.html +++ b/index.html @@ -2,12 +2,12 @@ - + - ruleta + Ruleta de Sostenibilidad TIP
- + diff --git a/package-lock.json b/package-lock.json index d53b2b5..001e3f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,37 @@ "vite": "^8.1.1" } }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/core/node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -1282,7 +1313,6 @@ "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2102,7 +2132,6 @@ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2144,7 +2173,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2307,7 +2335,6 @@ "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", diff --git a/package.json b/package.json index 322e4c9..2693858 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", + "postbuild": "node scripts/generate-scorm-manifest.js", "lint": "oxlint", "preview": "vite preview" }, diff --git a/public/img/01.png b/public/img/01.png new file mode 100644 index 0000000..e678abd Binary files /dev/null and b/public/img/01.png differ diff --git a/public/img/02.png b/public/img/02.png new file mode 100644 index 0000000..80db249 Binary files /dev/null and b/public/img/02.png differ diff --git a/public/img/03.png b/public/img/03.png new file mode 100644 index 0000000..08a38b6 Binary files /dev/null and b/public/img/03.png differ diff --git a/public/img/agua.png b/public/img/agua.png new file mode 100644 index 0000000..d42d890 Binary files /dev/null and b/public/img/agua.png differ diff --git a/public/img/correcto.png b/public/img/correcto.png new file mode 100644 index 0000000..25764d6 Binary files /dev/null and b/public/img/correcto.png differ diff --git a/public/img/energia.png b/public/img/energia.png new file mode 100644 index 0000000..406f9fb Binary files /dev/null and b/public/img/energia.png differ diff --git a/public/img/fondo1.jpg b/public/img/fondo1.jpg new file mode 100644 index 0000000..a37a752 Binary files /dev/null and b/public/img/fondo1.jpg differ diff --git a/public/img/fondo2.jpg b/public/img/fondo2.jpg new file mode 100644 index 0000000..483e434 Binary files /dev/null and b/public/img/fondo2.jpg differ diff --git a/public/img/fondo3.jpg b/public/img/fondo3.jpg new file mode 100644 index 0000000..d0dcf88 Binary files /dev/null and b/public/img/fondo3.jpg differ diff --git a/public/img/fondo4.jpg b/public/img/fondo4.jpg new file mode 100644 index 0000000..1727eb6 Binary files /dev/null and b/public/img/fondo4.jpg differ diff --git a/public/img/incorrecto.png b/public/img/incorrecto.png new file mode 100644 index 0000000..1246895 Binary files /dev/null and b/public/img/incorrecto.png differ diff --git a/public/img/indicador.png b/public/img/indicador.png index 4529887..663157a 100644 Binary files a/public/img/indicador.png and b/public/img/indicador.png differ diff --git a/public/img/logo-solo.png b/public/img/logo-solo.png new file mode 100644 index 0000000..7b082f8 Binary files /dev/null and b/public/img/logo-solo.png differ diff --git a/public/img/logo.png b/public/img/logo.png new file mode 100644 index 0000000..ad04dd3 Binary files /dev/null and b/public/img/logo.png differ diff --git a/public/img/movilidad.png b/public/img/movilidad.png new file mode 100644 index 0000000..0d5e4e6 Binary files /dev/null and b/public/img/movilidad.png differ diff --git a/public/img/residuos.png b/public/img/residuos.png new file mode 100644 index 0000000..825804d Binary files /dev/null and b/public/img/residuos.png differ diff --git a/public/img/ruleta.png b/public/img/ruleta.png deleted file mode 100644 index e03d743..0000000 Binary files a/public/img/ruleta.png and /dev/null differ diff --git a/public/imsmanifest.xml b/public/imsmanifest.xml index f29ba18..61849f5 100644 --- a/public/imsmanifest.xml +++ b/public/imsmanifest.xml @@ -13,9 +13,9 @@ - Ruleta Interactiva + Ruleta de Sostenibilidad TIP - Ruleta Interactiva + Ruleta de Sostenibilidad TIP 80 diff --git a/scripts/generate-scorm-manifest.js b/scripts/generate-scorm-manifest.js new file mode 100644 index 0000000..4ff56de --- /dev/null +++ b/scripts/generate-scorm-manifest.js @@ -0,0 +1,53 @@ +import { readdirSync, statSync, readFileSync, writeFileSync } from 'fs' +import { join, relative, sep } from 'path' + +const DIST = join(import.meta.dirname, '..', 'dist') +const MANIFEST_TEMPLATE = join(import.meta.dirname, '..', 'public', 'imsmanifest.xml') +const MANIFEST_OUT = join(DIST, 'imsmanifest.xml') + +/** + * Escanea recursivamente dist/ y genera un por cada archivo, + * usando '/' como separador (requerido por SCORM). + */ +function scanFiles(dir, basePath = '') { + const entries = readdirSync(dir) + const files = [] + + for (const entry of entries) { + const full = join(dir, entry) + const relativePath = basePath ? `${basePath}/${entry}` : entry + + if (statSync(full).isDirectory()) { + files.push(...scanFiles(full, relativePath)) + } else if (entry !== 'imsmanifest.xml') { + // SCORM usa '/' como separador de ruta + files.push(relativePath.split(sep).join('/')) + } + } + + return files +} + +// Leer template del manifiesto +let manifest = readFileSync(MANIFEST_TEMPLATE, 'utf-8') + +// Escanear archivos en dist/ +const distFiles = scanFiles(DIST) +console.log(`📁 Archivos encontrados en dist/: ${distFiles.length}`) + +// Generar los elements +const fileElements = distFiles + .map((f) => ` `) + .join('\n') + +// Reemplazar el bloque de resources en el manifiesto +// Busca y reemplaza su contenido de manteniendo el resto +const resourceRegex = /(]*>)([\s\S]*?)(<\/resource>)/ +manifest = manifest.replace(resourceRegex, (_, open, _inner, close) => { + return `${open}\n${fileElements}\n ${close}` +}) + +// Escribir manifiesto actualizado en dist/ +writeFileSync(MANIFEST_OUT, manifest, 'utf-8') +console.log('✅ imsmanifest.xml generado con todos los archivos del build') +console.log(` → ${MANIFEST_OUT}`) diff --git a/scripts/generate-scorm-manifest.min.js b/scripts/generate-scorm-manifest.min.js new file mode 100644 index 0000000..efd492a --- /dev/null +++ b/scripts/generate-scorm-manifest.min.js @@ -0,0 +1,2 @@ +// @ts-nocheck +import{readdirSync,statSync,readFileSync,writeFileSync}from"fs";import{join,relative,sep}from"path";const DIST=join(import.meta.dirname,"..","dist"),MANIFEST_TEMPLATE=join(import.meta.dirname,"..","public","imsmanifest.xml"),MANIFEST_OUT=join(DIST,"imsmanifest.xml");function scanFiles(e,i=""){const s=readdirSync(e),n=[];for(const o of s){const s=join(e,o),t=i?`${i}/${o}`:o;statSync(s).isDirectory()?n.push(...scanFiles(s,t)):"imsmanifest.xml"!==o&&n.push(t.split(sep).join("/"))}return n}let manifest=readFileSync(MANIFEST_TEMPLATE,"utf-8");const distFiles=scanFiles(DIST);console.log(`📁 Archivos encontrados en dist/: ${distFiles.length}`);const fileElements=distFiles.map(e=>` `).join("\n"),resourceRegex=/(]*>)([\s\S]*?)(<\/resource>)/;manifest=manifest.replace(resourceRegex,(e,i,s,n)=>`${i}\n${fileElements}\n ${n}`),writeFileSync(MANIFEST_OUT,manifest,"utf-8"),console.log("✅ imsmanifest.xml generado con todos los archivos del build"),console.log(` → ${MANIFEST_OUT}`); \ No newline at end of file diff --git a/src/App.css b/src/App.css index f90339d..749ef0a 100644 --- a/src/App.css +++ b/src/App.css @@ -1,184 +1 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} +/* App.css — Los estilos del proyecto se manejan con Tailwind CSS (ver index.css) */ diff --git a/src/App.tsx b/src/App.tsx index ad25f32..1ae8fad 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { useState } from 'react' -import MainLayout from './layouts/MainLayout' +import ScaleWrapper from './components/layout/ScaleWrapper' +import Intro from './components/screens/Intro' import Cover from './components/screens/Cover' import Game from './components/screens/Game' import Result from './components/screens/Result' @@ -7,7 +8,7 @@ import { consumirIntento, intentosAgotados } from './hooks/useIntentos' import { setScore, setLessonStatus, finishSCORM } from './scorm/scorm' import { useSCORM } from './hooks/useSCORM' -type Screen = 'cover' | 'game' | 'result' +type Screen = 'intro' | 'cover' | 'game' | 'result' interface ResultState { aprobado: boolean @@ -17,20 +18,24 @@ interface ResultState { export default function App() { useSCORM() const [screen, setScreen] = useState(() => - intentosAgotados() ? 'result' : 'cover' + intentosAgotados() ? 'result' : 'intro' ) const [resultState, setResultState] = useState({ aprobado: false, sinIntentos: intentosAgotados(), }) + function handleIntroContinue() { + setScreen('cover') + } + function handleStart() { consumirIntento() setScreen('game') } function handleGameEnd(aprobado: boolean, correctas: number) { - setScore(correctas * 10, 100, 0) + setScore(Math.round((correctas / 12) * 100), 100, 0) setLessonStatus(aprobado ? 'passed' : 'failed') finishSCORM() setResultState({ aprobado, sinIntentos: intentosAgotados() }) @@ -42,16 +47,71 @@ export default function App() { } return ( - - {screen === 'cover' && } - {screen === 'game' && } - {screen === 'result' && ( - + <> + {screen === 'intro' && ( + + + )} - + {screen === 'cover' && ( + setScreen('intro'), + puedeSiguiente: true, + onNext: handleStart, + }} + > + + + )} + {screen === 'game' && ( + + + + )} + {screen === 'result' && ( + setScreen('cover'), + puedeSiguiente: false, + }} + > + + + )} + ) } diff --git a/src/components/game/FeedbackModal.tsx b/src/components/game/FeedbackModal.tsx new file mode 100644 index 0000000..d6497a0 --- /dev/null +++ b/src/components/game/FeedbackModal.tsx @@ -0,0 +1,166 @@ +import { useState, useEffect } from 'react' +import type { Pregunta, Categoria } from '../../types' + +interface FeedbackModalProps { + pregunta: Pregunta + categoria: Categoria + feedbackCorrecto: boolean + tiempoAgotado: boolean + onCerrar: () => void +} + +export default function FeedbackModal({ + pregunta, + categoria: _categoria, + feedbackCorrecto, + tiempoAgotado, + onCerrar, +}: FeedbackModalProps) { + const [visible, setVisible] = useState(false) + + useEffect(() => { + const t = setTimeout(() => setVisible(true), 10) + return () => clearTimeout(t) + }, []) + + const esCorrecto = feedbackCorrecto + const label = esCorrecto ? 'Correcto' : tiempoAgotado ? 'Tiempo agotado' : 'Incorrecto' + const accentColor = esCorrecto ? '#39ad4a' : '#ed1f24' + const imgSrc = esCorrecto ? './img/correcto.png' : './img/incorrecto.png' + const imagenALaIzquierda = !esCorrecto + + const reflexion = pregunta.reflexion ?? pregunta.retroalimentacion + const datoCurioso = pregunta.datoCurioso + + return ( +
+
+ {/* ─── Barra superior flotante (25px sobre el modal) ─── */} +
+ {imagenALaIzquierda && ( + {label} + )} + + {label} + + {!imagenALaIzquierda && ( + {label} + )} +
+ + {/* ─── Tarjeta del modal ─── */} +
+ {/* ─── Botón cerrar ─── */} + + + {/* ─── Tiempo agotado ─── */} + {tiempoAgotado && ( +
+ Tiempo agotado +

+ No te preocupes, gira la ruleta de nuevo para continuar con la siguiente pregunta. +

+
+ )} + + {/* ─── Reflexión ─── */} + {!tiempoAgotado && ( +
+

+ Reflexión: {reflexion} +

+
+ )} + + {/* ─── ¿Sabías qué? ─── */} + {!tiempoAgotado && datoCurioso && ( +
+
+ + ¿Sabías qué? + +
+
+

+ {datoCurioso} +

+
+
+ )} +
+
+
+ ) +} diff --git a/src/components/game/QuestionModal.tsx b/src/components/game/QuestionModal.tsx index 1f8a6e4..1cec568 100644 --- a/src/components/game/QuestionModal.tsx +++ b/src/components/game/QuestionModal.tsx @@ -1,52 +1,20 @@ -import { useEffect, useRef, useState } from 'react' import type { Pregunta, Categoria } from '../../types' +import { TIEMPO_LIMITE } from '../../hooks/useRuleta' interface QuestionModalProps { pregunta: Pregunta categoria: Categoria mostrarFeedback: boolean feedbackCorrecto: boolean + tiempoRestante: number onResponder: (indice: number | null) => void onCerrar: () => void } const LETRAS = ['A', 'B', 'C', 'D'] -const TIEMPO_LIMITE = 30 -export default function QuestionModal({ pregunta, categoria, mostrarFeedback, feedbackCorrecto, onResponder, onCerrar }: QuestionModalProps) { - const [tiempo, setTiempo] = useState(TIEMPO_LIMITE) - const intervalRef = useRef | null>(null) - const tiempoAgotadoRef = useRef(false) - - useEffect(() => { - if (mostrarFeedback) { - if (intervalRef.current) clearInterval(intervalRef.current) - return - } - - setTiempo(TIEMPO_LIMITE) - tiempoAgotadoRef.current = false - intervalRef.current = setInterval(() => { - setTiempo((prev) => { - if (prev <= 1) { - clearInterval(intervalRef.current!) - return 0 - } - return prev - 1 - }) - }, 1000) - - return () => { if (intervalRef.current) clearInterval(intervalRef.current) } - }, [pregunta.id, mostrarFeedback]) - - useEffect(() => { - if (tiempo === 0 && !mostrarFeedback && !tiempoAgotadoRef.current) { - tiempoAgotadoRef.current = true - onResponder(null) - } - }, [tiempo, mostrarFeedback]) - - const porcentaje = (tiempo / TIEMPO_LIMITE) * 100 +export default function QuestionModal({ pregunta, categoria, mostrarFeedback, feedbackCorrecto, tiempoRestante, onResponder, onCerrar }: QuestionModalProps) { + const porcentaje = (tiempoRestante / TIEMPO_LIMITE) * 100 const colorBarra = porcentaje > 50 ? '#22c55e' : porcentaje > 25 ? '#f97316' : '#ef4444' return ( @@ -66,7 +34,7 @@ export default function QuestionModal({ pregunta, categoria, mostrarFeedback, fe
Tiempo restante - {tiempo}s + {tiempoRestante}s
{feedbackCorrecto ?

{pregunta.retroalimentacion}

- :

{tiempo === 0 ? 'Se agotó el tiempo.' : 'Respuesta incorrecta.'}

+ :

{tiempoRestante === 0 ? 'Se agotó el tiempo.' : 'Respuesta incorrecta.'}

} -
-
- - {/* Imagen derecha */} + {/* Imagen izquierda */}
Ruleta Interactiva
+ {/* Texto derecho */} +
+ + {/* Soporte con degradado */} +
+

+ Las reglas son muy sencillas: la ruleta girará, la suerte elegirá una categoría + y te tocará responder una pregunta. Contarás con 30 segundos para responder + cada una. +

+

+ No se trata de encontrar la respuesta perfecta, sino de reflexionar y responder + desde tu experiencia y perspectiva. ¡Prepárate para poner a prueba tus + conocimientos y disfrutar de la dinámica! +

+
+ + {/* Soporte negro */} +
+

+ ¡y que empiece a girar! +

+
+ +
+ +
+
+
) } diff --git a/src/components/screens/Game.tsx b/src/components/screens/Game.tsx index 35cb84b..dbda716 100644 --- a/src/components/screens/Game.tsx +++ b/src/components/screens/Game.tsx @@ -1,13 +1,23 @@ import { useEffect } from 'react' import { useRuleta } from '../../hooks/useRuleta' import { sounds } from '../../audio/AudioController' -import QuestionModal from '../game/QuestionModal' import RuletaSVG from '../game/Ruleta/RuletaSVG' +import FeedbackModal from '../game/FeedbackModal' interface GameProps { onEnd: (aprobado: boolean, correctas: number) => void } +const LETRAS = ['A', 'B', 'C', 'D'] + +/** Mapeo de nombre de categoría → imagen PNG */ +const CAT_IMG: Record = { + 'Agua': 'agua.png', + 'Residuos': 'residuos.png', + 'Energía': 'energia.png', + 'Electromovilidad': 'movilidad.png', +} + export default function Game({ onEnd }: GameProps) { const { estado, categorias, girar, responder, cerrarFeedback, MAX_PREGUNTAS } = useRuleta() @@ -20,49 +30,143 @@ export default function Game({ onEnd }: GameProps) { girar() } + const pregunta = estado.preguntaActual + const categoria = estado.categoriaActual + return ( -
+
- {/* Instrucciones */} -
-

¿Cómo jugar?

-

- Haz clic en la ruleta para girarla y responde correctamente para ganar puntos. -

-
- -
- - {/* Progreso de preguntas */} -
- {Array.from({ length: MAX_PREGUNTAS }).map((_, i) => ( + {/* ─── Contenedor rojo con imagen + nombre de categoría (top-right) ─── */} + {categoria && ( +
+ {/* Círculo blanco con imagen de categoría */} + {CAT_IMG[categoria.nombre] && (
- ))} + className="rounded-full flex items-center justify-center shrink-0" + style={{ + width: '52px', + height: '52px', + backgroundColor: '#FFFFFF', + boxShadow: '0 2px 8px rgba(0,0,0,0.2)', + }} + > + {categoria.nombre} +
+ )} + + {categoria.nombre} + +
+ )} + + {/* ─── Contenido principal: Ruleta + Panel de pregunta ─── */} +
+ + {/* ─── Columna izquierda: Ruleta 40% ─── */} +
+
- {/* Ruleta SVG */} - + {/* ─── Columna derecha: Panel de pregunta 60% ─── */} +
+ {pregunta && !estado.completado && ( +
+ {/* Header con pregunta (fondo transparente) */} +
+

{pregunta.pregunta}

+
+ + {/* Opciones */} +
+ {pregunta.opciones.map((opcion, i) => ( + + ))} +
+
+ )} + + {/* Temporizador debajo del panel */} + {pregunta && !estado.completado && ( +
+ + {estado.tiempoRestante}seg + +
+ )} + + {/* Sin pregunta activa: placeholder */} + {!pregunta && !estado.completado && ( +
+

+ Gira la ruleta para obtener una pregunta +

+

+ Cada respuesta correcta te acerca al objetivo de {MAX_PREGUNTAS} preguntas +

+
+ )} +
- {/* Modal pregunta */} - {estado.preguntaActual && !estado.completado && ( - )} diff --git a/src/components/screens/Intro.tsx b/src/components/screens/Intro.tsx new file mode 100644 index 0000000..d5e2569 --- /dev/null +++ b/src/components/screens/Intro.tsx @@ -0,0 +1,179 @@ +/* ─── SVG de Agua ──────────────────────────────────── */ +function IconoAgua() { + return ( + + {/* Gota de agua */} + + {/* Brillo */} + + {/* Grifo simplificado arriba */} + + + + + ) +} + +/* ─── SVG de Residuos y Circularidad ───────────────── */ +function IconoResiduos() { + return ( + + {/* Contenedor de reciclaje */} + + + + + + {/* Símbolo de reciclaje ♻ */} + + + + + + + ) +} + +/* ─── SVG de Energía ───────────────────────────────── */ +function IconoEnergia() { + return ( + + {/* Sol */} + + {[0, 45, 90, 135, 180, 225, 270, 315].map((angle, i) => ( + + ))} + {/* Colinas verdes */} + + + {/* Aerogenerador */} + + + + + + + + {/* Panel solar */} + + + + + + ) +} + +/* ─── SVG de Electromovilidad ──────────────────────── */ +function IconoElectromovilidad() { + return ( + + {/* Carretera */} + + + + + {/* Coche eléctrico azul */} + + {/* Carrocería */} + + {/* Techo / cabina */} + + {/* Ventanas */} + + + {/* Ruedas */} + + + + + {/* Faro */} + + + {/* Cargador / estación */} + + + {/* Cable */} + + {/* Indicador verde */} + + {/* Rayo */} + + + ) +} + +/* ─── Datos de categorías ──────────────────────────── */ +const CATEGORIAS = [ + { id: 'agua', nombre: 'Agua', Icono: IconoAgua }, + { id: 'residuos', nombre: 'Residuos y', nombre2: 'Circularidad', Icono: IconoResiduos }, + { id: 'energia', nombre: 'Energía', Icono: IconoEnergia }, + { id: 'electromovilidad', nombre: 'Electromovilidad', nombre2: 'y Transporte', nombre3: 'Sostenible', Icono: IconoElectromovilidad }, +] + +export default function Intro() { + return ( +
+ + {/* ─── Contenido scrolleable ──────────────────── */} +
+ + {/* ─── Columna izquierda (60%): bienvenida ───────── */} +
+

¡BIENVENIDOS!

+ Logo Ruleta +
+ + {/* ─── Columna derecha (40%): texto ─────────── */} +
+
+ {/* Texto introductorio */} +

+ Hoy vamos a poner a prueba nuestros conocimientos y a reflexionar juntos sobre + cómo nuestras acciones impactan el planeta. En esta ruleta tenemos{' '} + 4 ejes fundamentales para el futuro: +

+ + {/* Lista de ejes con imágenes */} +
+ {CATEGORIAS.map((cat, i) => { + const imagenes = ['agua.png', 'residuos.png', 'energia.png', 'movilidad.png'] + return ( +
+ {cat.nombre} +
+ + {cat.nombre} + {cat.nombre2 ? ` ${cat.nombre2}` : ''} + {cat.nombre3 ? ` ${cat.nombre3}` : ''} + +
+
+ ) + })} +
+
+
+ +
+
+ ) +} diff --git a/src/components/screens/Result.tsx b/src/components/screens/Result.tsx index acb0601..e32fc47 100644 --- a/src/components/screens/Result.tsx +++ b/src/components/screens/Result.tsx @@ -4,49 +4,85 @@ interface ResultProps { onReintentar: () => void } -export default function Result({ aprobado, sinIntentos, onReintentar }: ResultProps) { +export default function Result(_props: ResultProps) { return ( -
- - {/* Aprobado */} - {aprobado && ( - <> - 🏆 -

¡Felicidades!

-

- Has completado la actividad exitosamente. ¡Excelente desempeño! -

- - )} - - {/* Reprobado con intentos disponibles */} - {!aprobado && !sinIntentos && ( - <> - 💪 -

¡Casi lo logras!

-

- Aún te falta un poco. Vuelve a intentarlo, ¡tú puedes! -

- - - )} +

+ ¡Y con este último giro cerramos nuestra ruleta del día de hoy! +

+
- {/* Sin intentos */} - {sinIntentos && ( - <> - 😔 -

Intentos agotados

-

- Has utilizado todos tus intentos disponibles. Te recomendamos repasar el contenido del curso antes de volver a intentarlo. + {/* Contenido degradado */} +

+

+ Recorrimos desde la gestión del agua y los residuos, hasta la transición hacia energías más limpias y la movilidad del futuro. Más allá de las respuestas correctas o los puntos, lo verdaderamente importante es que la sostenibilidad no se queda en esta pantalla: se construye con las decisiones que tomamos todos los días. +

+

+ Muchísimas gracias a todos por su participación, su energía y sus aportaciones. +

+
+
+
+ + {/* ─── Columna derecha 50% ─── */} +
+ Resultado + +
+

+ ¡Nos llevamos grandes aprendizajes para seguir cuidando el planeta juntos!

- - )} - +
+
) } diff --git a/src/components/ui/ActivityBar.tsx b/src/components/ui/ActivityBar.tsx new file mode 100644 index 0000000..50914d8 --- /dev/null +++ b/src/components/ui/ActivityBar.tsx @@ -0,0 +1,119 @@ +/* ─── Barra inferior de actividad ─────────────── */ + +interface ActivityBarProps { + preguntaActual?: number + totalPreguntas?: number + mensaje: string + onPrev?: () => void + onNext?: () => void + puedeAnterior?: boolean + puedeSiguiente?: boolean +} + +/* ─── Flecha izquierda ────────────────────────── */ +function FlechaIzq({ disabled }: { disabled?: boolean }) { + return ( + + + + ) +} + +/* ─── Flecha derecha ──────────────────────────── */ +function FlechaDer({ disabled }: { disabled?: boolean }) { + return ( + + + + ) +} + +export default function ActivityBar({ + preguntaActual = 0, + totalPreguntas = 0, + mensaje, + onPrev, + onNext, + puedeAnterior = false, + puedeSiguiente = false, +}: ActivityBarProps) { + const counterText = totalPreguntas > 0 + ? `${preguntaActual}/${totalPreguntas}` + : '0/0' + + return ( +
+ {/* ─── Instrucción en soporte rojo ─────────── */} +
+ + {mensaje} + +
+ + {/* ─── Botones de navegación ───────────────── */} +
+ + + + {counterText} + + + +
+
+ ) +} diff --git a/src/components/ui/NavIndicator.tsx b/src/components/ui/NavIndicator.tsx new file mode 100644 index 0000000..4c4caf1 --- /dev/null +++ b/src/components/ui/NavIndicator.tsx @@ -0,0 +1,89 @@ +/* ─── Indicador de navegación (esquina superior derecha) ─── */ + +interface NavIndicatorProps { + preguntaActual: number + totalPreguntas: number + onPrev?: () => void + onNext?: () => void + puedeAnterior: boolean + puedeSiguiente: boolean +} + +/* ─── Flecha izquierda ────────────────────────── */ +function FlechaIzq({ disabled }: { disabled?: boolean }) { + return ( + + + + ) +} + +/* ─── Flecha derecha ──────────────────────────── */ +function FlechaDer({ disabled }: { disabled?: boolean }) { + return ( + + + + ) +} + +export default function NavIndicator({ + preguntaActual, + totalPreguntas, + onPrev, + onNext, + puedeAnterior, + puedeSiguiente, +}: NavIndicatorProps) { + const counterText = totalPreguntas > 0 + ? `${preguntaActual}/${totalPreguntas}` + : '0/0' + + return ( +
+ {/* Flecha izquierda */} + + + {/* Contador */} + + {counterText} + + + {/* Flecha derecha */} + +
+ ) +} diff --git a/src/data/preguntas.json b/src/data/preguntas.json index e37b50f..b1a386a 100644 --- a/src/data/preguntas.json +++ b/src/data/preguntas.json @@ -1,297 +1,324 @@ -{ +{ "categorias": [ { "id": 0, - "nombre": "Historia", - "color": "#7C3AED", - "emoji": "🏛️", + "nombre": "Agua", + "color": "#0284c7", + "emoji": "💧", "preguntas": [ { "id": 1, - "pregunta": "¿En qué año cayó el Muro de Berlín?", - "opciones": ["1987", "1989", "1991", "1993"], + "pregunta": "¿Cuánta agua se puede ahorrar al día si cerramos la llave mientras nos cepillamos los dientes?", + "opciones": ["Menos de 5 litros", "Entre 20 y 30 litros", "Más de 100 litros"], "correcta": 1, - "retroalimentacion": "El Muro de Berlín cayó el 9 de noviembre de 1989, marcando el fin de la Guerra Fría." + "retroalimentacion": "Un gesto de unos segundos, repetido todos los días, se convierte en un hábito que suma litros de agua cuidada sin ningún esfuerzo extra.", + "reflexion": "Un gesto de unos segundos, repetido todos los días, se convierte en un hábito que suma litros de agua cuidada sin ningún esfuerzo extra.", + "datoCurioso": "Si cada persona cerrara la llave al cepillarse los dientes, ahorraríamos millones de litros de agua diariamente en el mundo." }, { "id": 2, - "pregunta": "¿Quién fue el primer presidente de los Estados Unidos?", - "opciones": ["Abraham Lincoln", "Thomas Jefferson", "George Washington", "Benjamin Franklin"], + "pregunta": "¿Aproximadamente cuántos litros de agua se necesitan para producir una sola hamburguesa?", + "opciones": ["200 litros", "800 litros", "2,000 litros"], "correcta": 2, - "retroalimentacion": "George Washington fue el primer presidente de EE.UU., gobernando de 1789 a 1797." + "retroalimentacion": "Nuestras decisiones alimentarias tienen un impacto oculto en el agua; comer con conciencia también es una forma de cuidar este recurso.", + "reflexion": "Nuestras decisiones alimentarias tienen un impacto oculto en el agua; comer con conciencia también es una forma de cuidar este recurso.", + "datoCurioso": "Cultivar el pasto para alimentar al ganado y procesar la carne genera una huella hídrica altísima: hasta 2,000 litros de agua por hamburguesa." }, { "id": 3, - "pregunta": "¿En qué año comenzó la Primera Guerra Mundial?", - "opciones": ["1912", "1914", "1916", "1918"], + "pregunta": "¿Cuánta agua puede desperdiciar aproximadamente una llave que gotea constantemente?", + "opciones": ["Menos de 1 litro al año", "Hasta miles de litros al año", "Exactamente 10 litros al año"], "correcta": 1, - "retroalimentacion": "La Primera Guerra Mundial comenzó en 1914 tras el asesinato del archiduque Francisco Fernando." + "retroalimentacion": "Una fuga pequeña puede parecer insignificante, pero el desperdicio acumulado puede ser enorme.", + "reflexion": "Una fuga pequeña puede parecer insignificante, pero el desperdicio acumulado puede ser enorme.", + "datoCurioso": "Reparar una llave o tubería con fugas es una de las formas más sencillas de evitar el desperdicio de agua." }, { "id": 4, - "pregunta": "¿Qué civilización construyó las pirámides de Giza?", - "opciones": ["Griega", "Romana", "Egipcia", "Mesopotámica"], - "correcta": 2, - "retroalimentacion": "Las pirámides de Giza fueron construidas por los antiguos egipcios hace más de 4,500 años." + "pregunta": "¿Qué porcentaje aproximado del agua del planeta es agua dulce?", + "opciones": ["Menos del 3%", "25%", "Más del 50%"], + "correcta": 0, + "retroalimentacion": "Aunque la Tierra parece tener abundante agua, solo una pequeña fracción es agua dulce.", + "reflexion": "Aunque la Tierra parece tener abundante agua, solo una pequeña fracción es agua dulce.", + "datoCurioso": "Cerca del 97% del agua del planeta es salada y gran parte del agua dulce está congelada o se encuentra bajo tierra." }, { "id": 5, - "pregunta": "¿En qué año llegó Cristóbal Colón a América?", - "opciones": ["1488", "1490", "1492", "1495"], - "correcta": 2, - "retroalimentacion": "Cristóbal Colón llegó a América el 12 de octubre de 1492." + "pregunta": "¿Qué acción ayuda más a reducir el consumo de agua al bañarnos?", + "opciones": ["Tomar duchas más cortas", "Dejar correr el agua mientras nos enjabonamos", "Aumentar la presión del agua"], + "correcta": 0, + "retroalimentacion": "Reducir algunos minutos de la ducha puede generar un ahorro importante cuando se convierte en un hábito diario.", + "reflexion": "Reducir algunos minutos de la ducha puede generar un ahorro importante cuando se convierte en un hábito diario.", + "datoCurioso": "Cada minuto adicional en la ducha representa más agua utilizada, por lo que reducir el tiempo puede hacer una diferencia significativa." }, { "id": 6, - "pregunta": "¿Quién fue Napoleón Bonaparte?", - "opciones": ["Rey de España", "Emperador de Francia", "Presidente de Italia", "General inglés"], + "pregunta": "¿Qué porcentaje aproximado del agua que utilizamos en casa se puede desperdiciar por fugas?", + "opciones": ["Ninguna, las fugas no representan un problema", "Una cantidad importante si no se detectan y reparan", "Exactamente el 90% del consumo"], "correcta": 1, - "retroalimentacion": "Napoleón Bonaparte fue Emperador de Francia entre 1804 y 1815." + "retroalimentacion": "Revisar periódicamente llaves, tuberías y sanitarios ayuda a detectar pérdidas que muchas veces pasan desapercibidas.", + "reflexion": "Revisar periódicamente llaves, tuberías y sanitarios ayuda a detectar pérdidas que muchas veces pasan desapercibidas.", + "datoCurioso": "Algunas fugas pueden ser silenciosas y difíciles de detectar, pero aun así desperdiciar agua continuamente." }, { "id": 7, - "pregunta": "¿En qué país se originó la Revolución Industrial?", - "opciones": ["Francia", "Alemania", "Estados Unidos", "Inglaterra"], - "correcta": 3, - "retroalimentacion": "La Revolución Industrial comenzó en Inglaterra a mediados del siglo XVIII." + "pregunta": "¿Qué actividad tiene una huella hídrica?", + "opciones": ["Solo beber agua", "Solo bañarse", "Prácticamente todos los productos y actividades que realizamos"], + "correcta": 2, + "retroalimentacion": "El agua que utilizamos no siempre es visible; también está presente en la producción de alimentos, ropa, energía y otros productos.", + "reflexion": "El agua que utilizamos no siempre es visible; también está presente en la producción de alimentos, ropa, energía y otros productos.", + "datoCurioso": "La huella hídrica considera el agua utilizada directa e indirectamente para producir bienes y servicios." }, { "id": 8, - "pregunta": "¿Cuál fue el primer país en conceder el voto a la mujer?", - "opciones": ["Francia", "Nueva Zelanda", "Estados Unidos", "Suecia"], - "correcta": 1, - "retroalimentacion": "Nueva Zelanda fue el primer país en conceder el voto a la mujer en 1893." + "pregunta": "¿Cuál de estas acciones ayuda a ahorrar agua al lavar ropa?", + "opciones": ["Utilizar la lavadora con cargas completas", "Lavar una sola prenda por ciclo", "Usar siempre el ciclo más largo"], + "correcta": 0, + "retroalimentacion": "Aprovechar mejor cada ciclo de lavado permite utilizar menos agua y energía por prenda.", + "reflexion": "Aprovechar mejor cada ciclo de lavado permite utilizar menos agua y energía por prenda.", + "datoCurioso": "Planificar las cargas de lavado es una forma sencilla de hacer más eficiente el uso de los recursos." } ] }, { "id": 1, - "nombre": "Geografía", - "color": "#059669", - "emoji": "🌍", + "nombre": "Residuos y Circularidad", + "color": "#16a34a", + "emoji": "♻️", "preguntas": [ { "id": 9, - "pregunta": "¿Cuál es el río más largo del mundo?", - "opciones": ["Amazonas", "Nilo", "Yangtsé", "Misisipi"], + "pregunta": "¿Todo lo que se tira en el contenedor de reciclaje termina reciclándose?", + "opciones": ["Sí, siempre", "No, si está mal separado o sucio contamina el resto"], "correcta": 1, - "retroalimentacion": "El río Nilo, con aproximadamente 6,650 km, es considerado el río más largo del mundo." + "retroalimentacion": "Separar bien nuestros residuos, informándonos antes de tirarlos, evita que se desperdicie el esfuerzo de reciclar.", + "reflexion": "Separar bien nuestros residuos, informándonos antes de tirarlos, evita que se desperdicie el esfuerzo de reciclar.", + "datoCurioso": "Un solo cartón con grasa de pizza puede contaminar el resto del material en el contenedor de reciclaje." }, { "id": 10, - "pregunta": "¿Cuál es el país más grande del mundo por superficie?", - "opciones": ["China", "Canadá", "Estados Unidos", "Rusia"], - "correcta": 3, - "retroalimentacion": "Rusia es el país más grande del mundo con 17.1 millones de km²." + "pregunta": "¿La economía circular —reducir, reutilizar, reparar, reciclar— es algo que solo pueden aplicar las grandes industrias?", + "opciones": ["Sí, requiere infraestructura industrial", "No, cualquier persona puede aplicarla desde casa"], + "correcta": 1, + "retroalimentacion": "La circularidad comienza en las decisiones diarias de cada persona: compostar, reparar, reutilizar y elegir productos duraderos.", + "reflexion": "La circularidad comienza en las decisiones diarias de cada persona: compostar, reparar, reutilizar y elegir productos duraderos.", + "datoCurioso": "Todos podemos aplicar la economía circular en casa, en la escuela y en la comunidad; no es exclusiva de las grandes industrias." }, { "id": 11, - "pregunta": "¿En qué continente se encuentra el desierto del Sahara?", - "opciones": ["Asia", "América", "África", "Australia"], - "correcta": 2, - "retroalimentacion": "El Sahara se encuentra en África y es el desierto cálido más grande del mundo." + "pregunta": "¿Cuál es la mejor forma de reducir la cantidad de residuos que generamos?", + "opciones": ["Comprar más productos desechables", "Reducir el consumo innecesario", "Comprar productos con más empaques"], + "correcta": 1, + "retroalimentacion": "El mejor residuo es el que no se genera. Antes de comprar, podemos preguntarnos si realmente necesitamos el producto.", + "reflexion": "El mejor residuo es el que no se genera. Antes de comprar, podemos preguntarnos si realmente necesitamos el producto.", + "datoCurioso": "La prevención y reducción de residuos son preferibles a tener que gestionar los residuos después de que ya fueron generados." }, { "id": 12, - "pregunta": "¿Cuál es la capital de Australia?", - "opciones": ["Sídney", "Melbourne", "Canberra", "Brisbane"], - "correcta": 2, - "retroalimentacion": "Canberra es la capital de Australia, no Sídney como muchos creen." + "pregunta": "¿Qué significa reutilizar un producto?", + "opciones": ["Usarlo nuevamente para el mismo u otro propósito", "Tirarlo después del primer uso", "Comprar uno nuevo para reemplazarlo"], + "correcta": 0, + "retroalimentacion": "Reutilizar prolonga la vida útil de los productos y evita generar nuevos residuos.", + "reflexion": "Reutilizar prolonga la vida útil de los productos y evita generar nuevos residuos.", + "datoCurioso": "Muchos objetos cotidianos pueden tener una segunda vida antes de convertirse en residuos." }, { "id": 13, - "pregunta": "¿Cuántos océanos hay en el planeta?", - "opciones": ["3", "4", "5", "6"], - "correcta": 2, - "retroalimentacion": "Hay 5 océanos: Pacífico, Atlántico, Índico, Ártico y Antártico." + "pregunta": "¿Qué tipo de residuos pueden utilizarse para hacer composta?", + "opciones": ["Restos de frutas y verduras", "Pilas y baterías", "Vidrio y aluminio"], + "correcta": 0, + "retroalimentacion": "Los residuos orgánicos pueden transformarse en un recurso en lugar de terminar en un relleno sanitario.", + "reflexion": "Los residuos orgánicos pueden transformarse en un recurso en lugar de terminar en un relleno sanitario.", + "datoCurioso": "La composta permite aprovechar materia orgánica y convertirla en un material que puede mejorar la calidad del suelo." }, { "id": 14, - "pregunta": "¿Cuál es el volcán activo más alto del mundo?", - "opciones": ["Etna", "Kilimanjaro", "Ojos del Salado", "Cotopaxi"], - "correcta": 3, - "retroalimentacion": "El Cotopaxi en Ecuador es uno de los volcanes activos más altos del mundo con 5,897 metros." + "pregunta": "¿Por qué es importante separar correctamente los residuos desde el origen?", + "opciones": ["Para facilitar su aprovechamiento y disposición adecuada", "Para que todos los residuos se mezclen mejor", "Porque los residuos separados pesan menos"], + "correcta": 0, + "retroalimentacion": "La separación comienza en el lugar donde generamos el residuo. Si lo hacemos correctamente, ayudamos a que los materiales puedan aprovecharse mejor.", + "reflexion": "La separación comienza en el lugar donde generamos el residuo. Si lo hacemos correctamente, ayudamos a que los materiales puedan aprovecharse mejor.", + "datoCurioso": "En TIP México los residuos se separan en orgánicos, inorgánicos reciclables, inorgánicos no reciclables y papel." }, { "id": 15, - "pregunta": "¿En qué país se encuentra la Torre de Pisa?", - "opciones": ["España", "Francia", "Italia", "Portugal"], - "correcta": 2, - "retroalimentacion": "La Torre de Pisa se encuentra en la ciudad de Pisa, Italia." + "pregunta": "¿Qué deberíamos hacer con las pilas usadas?", + "opciones": ["Tirarlas junto con la basura común", "Llevarlas a un punto de recolección adecuado", "Tirarlas al drenaje"], + "correcta": 1, + "retroalimentacion": "Algunos residuos requieren un manejo especial para evitar impactos ambientales.", + "reflexion": "Algunos residuos requieren un manejo especial para evitar impactos ambientales.", + "datoCurioso": "Las pilas pueden contener materiales que requieren una disposición diferente a la de los residuos comunes." }, { "id": 16, - "pregunta": "¿Cuál es el país más pequeño del mundo?", - "opciones": ["Mónaco", "San Marino", "Liechtenstein", "Ciudad del Vaticano"], - "correcta": 3, - "retroalimentacion": "Ciudad del Vaticano es el país más pequeño del mundo con apenas 0.44 km²." + "pregunta": "¿Cuál de estas opciones genera menos residuos?", + "opciones": ["Usar una botella desechable nueva todos los días", "Utilizar una botella reutilizable", "Comprar varias botellas para tener reservas"], + "correcta": 1, + "retroalimentacion": "Sustituir productos de un solo uso por alternativas reutilizables es una forma sencilla de reducir la cantidad de residuos que generamos.", + "reflexion": "Sustituir productos de un solo uso por alternativas reutilizables es una forma sencilla de reducir la cantidad de residuos que generamos.", + "datoCurioso": "Las decisiones de consumo cotidiano pueden ayudar a disminuir significativamente los residuos de un solo uso." } ] }, { "id": 2, - "nombre": "Ciencia", - "color": "#DC2626", - "emoji": "🔬", + "nombre": "Energía", + "color": "#ea580c", + "emoji": "⚡", "preguntas": [ { "id": 17, - "pregunta": "¿Cuál es el elemento más abundante en el universo?", - "opciones": ["Oxígeno", "Carbono", "Hidrógeno", "Helio"], + "pregunta": "¿Cuánta menos energía consumen los focos LED comparados con los incandescentes?", + "opciones": ["20%", "50%", "Hasta 80%"], "correcta": 2, - "retroalimentacion": "El hidrógeno es el elemento más abundante del universo, constituyendo el 75% de su masa." + "retroalimentacion": "Cambiar la iluminación es una de las acciones más simples y de mayor retorno para reducir el consumo energético en casa y en el trabajo.", + "reflexion": "Cambiar la iluminación es una de las acciones más simples y de mayor retorno para reducir el consumo energético en casa y en el trabajo.", + "datoCurioso": "Cambiar un solo foco incandescente por uno LED puede significar un ahorro de cientos de pesos al año, además de reducir el consumo de energía." }, { "id": 18, - "pregunta": "¿A qué velocidad viaja la luz en el vacío?", - "opciones": ["200,000 km/s", "300,000 km/s", "400,000 km/s", "150,000 km/s"], - "correcta": 1, - "retroalimentacion": "La luz viaja a aproximadamente 299,792 km/s en el vacío." + "pregunta": "¿Los aparatos electrónicos en \"modo espera\" (standby) siguen consumiendo energía aunque parezcan apagados?", + "opciones": ["Sí, es lo que se conoce como \"consumo fantasma\"", "No, si la pantalla está apagada no consumen nada"], + "correcta": 0, + "retroalimentacion": "Desconectar los aparatos o usar regletas con interruptor es un cambio pequeño que evita un gasto de energía invisible pero constante.", + "reflexion": "Desconectar los aparatos o usar regletas con interruptor es un cambio pequeño que evita un gasto de energía invisible pero constante.", + "datoCurioso": "Un televisor en modo espera sigue consumiendo electricidad aunque parezca apagado; este consumo fantasma, multiplicado por millones de personas, es significativo." }, { "id": 19, - "pregunta": "¿Cuántos huesos tiene el cuerpo humano adulto?", - "opciones": ["196", "206", "216", "226"], + "pregunta": "¿Qué significa ser eficiente energéticamente?", + "opciones": ["Utilizar más energía para obtener mejores resultados", "Obtener el mismo resultado utilizando menos energía", "No utilizar energía nunca"], "correcta": 1, - "retroalimentacion": "El cuerpo humano adulto tiene 206 huesos." + "retroalimentacion": "La eficiencia energética no significa dejar de hacer las cosas, sino hacerlas mejor utilizando menos recursos.", + "reflexion": "La eficiencia energética no significa dejar de hacer las cosas, sino hacerlas mejor utilizando menos recursos.", + "datoCurioso": "Un equipo eficiente puede ofrecer el mismo servicio que otro utilizando menos electricidad." }, { "id": 20, - "pregunta": "¿Cuál es el planeta más grande del sistema solar?", - "opciones": ["Saturno", "Neptuno", "Júpiter", "Urano"], - "correcta": 2, - "retroalimentacion": "Júpiter es el planeta más grande del sistema solar." + "pregunta": "¿Qué fuente de energía aprovechan los paneles solares?", + "opciones": ["La energía del viento", "La energía del sol", "La energía de las olas"], + "correcta": 1, + "retroalimentacion": "Las energías renovables aprovechan recursos naturales que se regeneran continuamente.", + "reflexion": "Las energías renovables aprovechan recursos naturales que se regeneran continuamente.", + "datoCurioso": "La energía solar puede utilizarse para generar electricidad y reducir la dependencia de fuentes fósiles." }, { "id": 21, - "pregunta": "¿Qué gas es el más abundante en la atmósfera terrestre?", - "opciones": ["Oxígeno", "Dióxido de carbono", "Nitrógeno", "Argón"], - "correcta": 2, - "retroalimentacion": "El nitrógeno constituye aproximadamente el 78% de la atmósfera terrestre." + "pregunta": "¿Qué acción ayuda a reducir el consumo de energía en una oficina?", + "opciones": ["Dejar los equipos encendidos toda la noche", "Apagar o configurar los equipos cuando no se utilizan", "Mantener todas las pantallas encendidas"], + "correcta": 1, + "retroalimentacion": "La gestión responsable de los equipos de trabajo puede reducir consumos innecesarios.", + "reflexion": "La gestión responsable de los equipos de trabajo puede reducir consumos innecesarios.", + "datoCurioso": "Configurar computadoras y monitores para entrar en modo ahorro de energía ayuda a reducir el consumo durante los periodos de inactividad." }, { "id": 22, - "pregunta": "¿Cuántos cromosomas tiene una célula humana normal?", - "opciones": ["23", "44", "46", "48"], - "correcta": 2, - "retroalimentacion": "Las células humanas normales tienen 46 cromosomas, organizados en 23 pares." + "pregunta": "¿Qué ocurre cuando utilizamos menos energía eléctrica?", + "opciones": ["Siempre aumentan las emisiones", "Podemos reducir el consumo de recursos y las emisiones asociadas a la generación de electricidad", "No ocurre nada"], + "correcta": 1, + "retroalimentacion": "Ahorrar energía puede beneficiar tanto al medio ambiente como a la economía de las personas y las organizaciones.", + "reflexion": "Ahorrar energía puede beneficiar tanto al medio ambiente como a la economía de las personas y las organizaciones.", + "datoCurioso": "El impacto ambiental de consumir electricidad depende, entre otros factores, de cómo se genera esa electricidad." }, { "id": 23, - "pregunta": "¿Quién formuló la teoría de la relatividad?", - "opciones": ["Isaac Newton", "Albert Einstein", "Nikola Tesla", "Stephen Hawking"], + "pregunta": "¿Cuál de estas acciones ayuda a utilizar de manera más eficiente el aire acondicionado?", + "opciones": ["Mantener puertas y ventanas abiertas", "Mantener cerrados los espacios climatizados", "Encenderlo con las puertas abiertas"], "correcta": 1, - "retroalimentacion": "Albert Einstein formuló la teoría de la relatividad especial en 1905 y la general en 1915." + "retroalimentacion": "Evitar la pérdida de aire acondicionado permite que el equipo trabaje menos y consuma menos energía.", + "reflexion": "Evitar la pérdida de aire acondicionado permite que el equipo trabaje menos y consuma menos energía.", + "datoCurioso": "El uso adecuado de los sistemas de climatización puede ayudar a reducir significativamente el consumo energético." }, { "id": 24, - "pregunta": "¿Cuál es el símbolo químico del oro?", - "opciones": ["Go", "Ag", "Au", "Or"], - "correcta": 2, - "retroalimentacion": "El símbolo químico del oro es Au, del latín Aurum." + "pregunta": "¿Qué tipo de iluminación suele ser más eficiente?", + "opciones": ["La iluminación incandescente", "La iluminación LED", "Todas consumen exactamente lo mismo"], + "correcta": 1, + "retroalimentacion": "Elegir tecnologías eficientes permite obtener la iluminación que necesitamos utilizando menos energía.", + "reflexion": "Elegir tecnologías eficientes permite obtener la iluminación que necesitamos utilizando menos energía.", + "datoCurioso": "Además de consumir menos energía, los focos LED suelen tener una vida útil más larga que los focos incandescentes." } ] }, { "id": 3, - "nombre": "Arte y Literatura", - "color": "#D97706", - "emoji": "🎨", + "nombre": "Electromovilidad y Transporte Sostenible", + "color": "#7c3aed", + "emoji": "🚗", "preguntas": [ { "id": 25, - "pregunta": "¿Quién escribió 'Cien años de soledad'?", - "opciones": ["Mario Vargas Llosa", "Pablo Neruda", "Gabriel García Márquez", "Jorge Luis Borges"], - "correcta": 2, - "retroalimentacion": "Gabriel García Márquez escribió Cien años de soledad en 1967, obra cumbre del realismo mágico." + "pregunta": "¿Qué acción tiene mayor impacto positivo para reducir tu huella de carbono en el transporte diario?", + "opciones": ["Usar transporte público o compartir el auto", "Viajar siempre en auto propio, aunque sea solo"], + "correcta": 0, + "retroalimentacion": "Elegir cómo nos transportamos es una de las decisiones cotidianas con mayor peso en nuestra huella ambiental.", + "reflexion": "Elegir cómo nos transportamos es una de las decisiones cotidianas con mayor peso en nuestra huella ambiental.", + "datoCurioso": "Un solo autobús puede reemplazar hasta 40 autos, reduciendo la congestión, el consumo de combustible y la contaminación." }, { "id": 26, - "pregunta": "¿Quién pintó 'La noche estrellada'?", - "opciones": ["Pablo Picasso", "Claude Monet", "Salvador Dalí", "Vincent van Gogh"], - "correcta": 3, - "retroalimentacion": "La noche estrellada fue pintada por Vincent van Gogh en 1889." + "pregunta": "¿TIP México cuenta con infraestructura de carga para vehículos eléctricos e híbridos en sus instalaciones?", + "opciones": ["Sí, cuenta con 10 cargadores eléctricos", "No, aún no se ha implementado"], + "correcta": 0, + "retroalimentacion": "La movilidad sostenible también se construye desde la infraestructura que facilita la transición hacia vehículos de menor impacto ambiental.", + "reflexion": "La movilidad sostenible también se construye desde la infraestructura que facilita la transición hacia vehículos de menor impacto ambiental.", + "datoCurioso": "TIP México cuenta con 10 cargadores eléctricos instalados como parte de su compromiso con la movilidad sostenible." }, { "id": 27, - "pregunta": "¿En qué país nació Mozart?", - "opciones": ["Alemania", "Austria", "Italia", "Francia"], + "pregunta": "¿Cuál es la principal fuente de energía de un vehículo eléctrico?", + "opciones": ["Gasolina", "Electricidad almacenada en una batería", "Diésel"], "correcta": 1, - "retroalimentacion": "Wolfgang Amadeus Mozart nació en Salzburgo, Austria, en 1756." + "retroalimentacion": "La electromovilidad cambia la forma en que utilizamos la energía para movernos.", + "reflexion": "La electromovilidad cambia la forma en que utilizamos la energía para movernos.", + "datoCurioso": "Los vehículos eléctricos utilizan uno o más motores eléctricos alimentados por la energía almacenada en sus baterías." }, { "id": 28, - "pregunta": "¿Quién escribió 'Don Quijote de la Mancha'?", - "opciones": ["Lope de Vega", "Miguel de Cervantes", "Francisco de Quevedo", "Garcilaso de la Vega"], - "correcta": 1, - "retroalimentacion": "Don Quijote de la Mancha fue escrita por Miguel de Cervantes y publicada en 1605." + "pregunta": "¿Qué caracteriza a un vehículo híbrido?", + "opciones": ["Combina un motor de combustión con un sistema eléctrico", "Funciona únicamente con gasolina", "No utiliza ningún tipo de batería"], + "correcta": 0, + "retroalimentacion": "La tecnología híbrida combina diferentes sistemas de propulsión para mejorar la eficiencia en determinadas condiciones.", + "reflexion": "La tecnología híbrida combina diferentes sistemas de propulsión para mejorar la eficiencia en determinadas condiciones.", + "datoCurioso": "Los vehículos híbridos pueden utilizar un motor de combustión interna y uno o más motores eléctricos." }, { "id": 29, - "pregunta": "¿Quién compuso la Quinta Sinfonía?", - "opciones": ["Mozart", "Bach", "Beethoven", "Chopin"], - "correcta": 2, - "retroalimentacion": "La Quinta Sinfonía fue compuesta por Ludwig van Beethoven entre 1804 y 1808." + "pregunta": "¿Qué es un vehículo híbrido enchufable (PHEV)?", + "opciones": ["Un vehículo que puede cargarse conectándolo a una fuente eléctrica y también utiliza un motor de combustión", "Un vehículo que solo utiliza gasolina", "Un vehículo que no tiene batería"], + "correcta": 0, + "retroalimentacion": "Los vehículos híbridos enchufables combinan la posibilidad de conducir utilizando electricidad con la flexibilidad de un motor de combustión.", + "reflexion": "Los vehículos híbridos enchufables combinan la posibilidad de conducir utilizando electricidad con la flexibilidad de un motor de combustión.", + "datoCurioso": "Los PHEV pueden recorrer cierta distancia utilizando únicamente energía eléctrica, dependiendo del modelo y de la carga disponible." }, { "id": 30, - "pregunta": "¿En qué museo se encuentra la Mona Lisa?", - "opciones": ["Museo del Prado", "Museo Británico", "Museo del Louvre", "Museo Uffizi"], - "correcta": 2, - "retroalimentacion": "La Mona Lisa se encuentra en el Museo del Louvre en París, Francia." + "pregunta": "¿Qué sucede con parte de la energía durante el frenado regenerativo?", + "opciones": ["Puede recuperarse y almacenarse nuevamente en la batería", "Desaparece completamente", "Se transforma en gasolina"], + "correcta": 0, + "retroalimentacion": "Esta tecnología permite aprovechar parte de la energía del movimiento que normalmente se perdería durante el frenado.", + "reflexion": "Esta tecnología permite aprovechar parte de la energía del movimiento que normalmente se perdería durante el frenado.", + "datoCurioso": "El frenado regenerativo convierte parte de la energía cinética del vehículo en electricidad." }, { "id": 31, - "pregunta": "¿Quién escribió 'Romeo y Julieta'?", - "opciones": ["Charles Dickens", "William Shakespeare", "Jane Austen", "Victor Hugo"], - "correcta": 1, - "retroalimentacion": "Romeo y Julieta fue escrita por William Shakespeare alrededor de 1595." + "pregunta": "¿Qué elemento es fundamental para impulsar la adopción de vehículos eléctricos?", + "opciones": ["Una infraestructura adecuada de carga", "Eliminar todas las alternativas de transporte", "Evitar nuevas tecnologías"], + "correcta": 0, + "retroalimentacion": "Contar con puntos de carga accesibles facilita que más personas y empresas consideren la movilidad eléctrica.", + "reflexion": "Contar con puntos de carga accesibles facilita que más personas y empresas consideren la movilidad eléctrica.", + "datoCurioso": "La infraestructura de carga es un elemento clave para integrar los vehículos eléctricos a las actividades diarias." }, { "id": 32, - "pregunta": "¿Qué movimiento artístico lideró Salvador Dalí?", - "opciones": ["Cubismo", "Impresionismo", "Surrealismo", "Expresionismo"], - "correcta": 2, - "retroalimentacion": "Salvador Dalí fue uno de los principales representantes del Surrealismo." + "pregunta": "¿Cuál de estas opciones puede ayudar a reducir las emisiones asociadas al transporte?", + "opciones": ["Usar transporte público, compartir el auto o utilizar vehículos de menor emisión", "Utilizar siempre un vehículo individual aunque el trayecto sea corto", "Mantener el vehículo encendido cuando está estacionado"], + "correcta": 0, + "retroalimentacion": "La movilidad sostenible no depende de una única solución; combinar diferentes alternativas puede ayudarnos a reducir el impacto ambiental de nuestros traslados.", + "reflexion": "La movilidad sostenible no depende de una única solución; combinar diferentes alternativas puede ayudarnos a reducir el impacto ambiental de nuestros traslados.", + "datoCurioso": "La movilidad sostenible incluye opciones como el transporte público, la movilidad activa, el uso compartido de vehículos y la electromovilidad." } ] } - ], - "bancoGenerico": [ - { - "id": 101, - "pregunta": "¿Cuál es el océano más grande del mundo?", - "opciones": ["Atlántico", "Índico", "Pacífico", "Ártico"], - "correcta": 2, - "retroalimentacion": "El océano Pacífico es el más grande, cubriendo más de 165 millones de km²." - }, - { - "id": 102, - "pregunta": "¿Cuántos planetas tiene el sistema solar?", - "opciones": ["7", "8", "9", "10"], - "correcta": 1, - "retroalimentacion": "El sistema solar tiene 8 planetas desde que Plutón fue reclasificado en 2006." - }, - { - "id": 103, - "pregunta": "¿Cuál es el animal terrestre más rápido?", - "opciones": ["León", "Guepardo", "Antílope", "Caballo"], - "correcta": 1, - "retroalimentacion": "El guepardo puede alcanzar velocidades de hasta 120 km/h, siendo el animal terrestre más rápido." - }, - { - "id": 104, - "pregunta": "¿En qué año se fundó la ONU?", - "opciones": ["1943", "1945", "1947", "1950"], - "correcta": 1, - "retroalimentacion": "La ONU fue fundada el 24 de octubre de 1945 tras el fin de la Segunda Guerra Mundial." - }, - { - "id": 105, - "pregunta": "¿Cuál es el idioma más hablado en el mundo?", - "opciones": ["Inglés", "Español", "Mandarín", "Hindi"], - "correcta": 2, - "retroalimentacion": "El mandarín es el idioma con más hablantes nativos, con más de 900 millones." - } ] } diff --git a/src/hooks/useIntentos.ts b/src/hooks/useIntentos.ts index 4262c47..45bca56 100644 --- a/src/hooks/useIntentos.ts +++ b/src/hooks/useIntentos.ts @@ -2,11 +2,19 @@ const KEY_INTENTOS = 'ruleta_intentos_usados' const KEY_AGOTADOS = 'ruleta_intentos_agotados' const MAX_INTENTOS = 3 +let intentosInfinitos = false + +export function setIntentosInfinitos(valor: boolean) { + intentosInfinitos = valor +} + function getIntentosUsados(): number { + if (intentosInfinitos) return 0 return parseInt(sessionStorage.getItem(KEY_INTENTOS) ?? '0', 10) } function setIntentosUsados(n: number): void { + if (intentosInfinitos) return sessionStorage.setItem(KEY_INTENTOS, String(n)) if (n >= MAX_INTENTOS) { sessionStorage.setItem(KEY_AGOTADOS, 'true') @@ -14,14 +22,17 @@ function setIntentosUsados(n: number): void { } export function intentosAgotados(): boolean { + if (intentosInfinitos) return false return sessionStorage.getItem(KEY_AGOTADOS) === 'true' } export function intentosRestantes(): number { + if (intentosInfinitos) return 999 return Math.max(0, MAX_INTENTOS - getIntentosUsados()) } export function consumirIntento(): void { + if (intentosInfinitos) return setIntentosUsados(getIntentosUsados() + 1) } diff --git a/src/hooks/useRuleta.ts b/src/hooks/useRuleta.ts index a98ba70..d3bb486 100644 --- a/src/hooks/useRuleta.ts +++ b/src/hooks/useRuleta.ts @@ -1,13 +1,15 @@ -import { useState, useRef } from 'react' +import { useState, useRef, useEffect, useMemo } from 'react' import type { Pregunta, Categoria, PreguntasData } from '../types' import data from '../data/preguntas.json' import { sounds } from '../audio/AudioController' -const { categorias, bancoGenerico } = data as PreguntasData -const MAX_PREGUNTAS = 10 -const MIN_CORRECTAS = 8 +const { categorias } = data as PreguntasData +const MAX_PREGUNTAS = 12 +const PREGUNTAS_POR_CATEGORIA = 3 +const MIN_CORRECTAS = 10 // 80% de 12 para aprobar const SECCIONES = categorias.length const ANGULO_SECCION = 360 / SECCIONES +export const TIEMPO_LIMITE = 30 export interface EstadoJuego { isSpinning: boolean @@ -20,11 +22,34 @@ export interface EstadoJuego { correctas: number completado: boolean aprobado: boolean - preguntasUsadas: Record - bancoPreguntasUsadas: number[] + /** Cuántas preguntas se han usado ya por categoría (id → conteo) */ + preguntasUsadasPorCategoria: Record + /** Pool de 3 preguntas pre-seleccionadas por categoría (id → preguntas) */ + preguntasPool: Record + tiempoRestante: number +} + +/** + * Pre-selecciona aleatoriamente 3 preguntas de cada categoría. + * Se ejecuta una sola vez al montar el hook. + */ +function crearPool(): Record { + const pool: Record = {} + for (const cat of categorias) { + const indices = Array.from({ length: cat.preguntas.length }, (_, i) => i) + // Fisher‑Yates shuffle + for (let i = indices.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[indices[i], indices[j]] = [indices[j], indices[i]] + } + pool[cat.id] = indices.slice(0, PREGUNTAS_POR_CATEGORIA).map((i) => cat.preguntas[i]) + } + return pool } export function useRuleta() { + const poolInicial = useMemo(() => crearPool(), []) + const [estado, setEstado] = useState({ isSpinning: false, rotacion: 0, @@ -36,26 +61,66 @@ export function useRuleta() { correctas: 0, completado: false, aprobado: false, - preguntasUsadas: {}, - bancoPreguntasUsadas: [], + preguntasUsadasPorCategoria: {}, + preguntasPool: poolInicial, + tiempoRestante: TIEMPO_LIMITE, }) const rotacionRef = useRef(0) - function seleccionarPregunta(categoria: Categoria, preguntasUsadas: Record, bancoPreguntasUsadas: number[]): { pregunta: Pregunta, esGenerica: boolean } { - const usadas = preguntasUsadas[categoria.id] ?? [] - const disponibles = categoria.preguntas.filter((_, i) => !usadas.includes(i)) + // ─── Temporizador por pregunta ─────────────────────────────────────────── + useEffect(() => { + if (estado.mostrarFeedback || !estado.preguntaActual || estado.completado) return - if (disponibles.length > 0) { - const idx = Math.floor(Math.random() * disponibles.length) - return { pregunta: disponibles[idx], esGenerica: false } + const interval = setInterval(() => { + setEstado((prev) => { + if (prev.mostrarFeedback || !prev.preguntaActual) return prev + + if (prev.tiempoRestante <= 1) { + sounds.incorrect?.play() + const nuevasRespondidas = prev.preguntasRespondidas + 1 + const completado = nuevasRespondidas >= MAX_PREGUNTAS + const aprobado = completado && prev.correctas >= MIN_CORRECTAS + return { + ...prev, + tiempoRestante: 0, + preguntasRespondidas: nuevasRespondidas, + mostrarFeedback: true, + feedbackCorrecto: false, + completado, + aprobado, + } + } + + return { ...prev, tiempoRestante: prev.tiempoRestante - 1 } + }) + }, 1000) + + return () => clearInterval(interval) + }, [estado.preguntaActual?.id, estado.mostrarFeedback, estado.completado]) + + /** + * Busca la siguiente pregunta disponible. + * 1. Intenta usar la categoría donde cayó la ruleta (si le quedan preguntas). + * 2. Si no, recorre las demás categorías en orden circular. + * 3. Retorna null si ya se usaron las 12 preguntas. + */ + function obtenerSiguientePregunta( + catIndice: number, + usadasPorCat: Record, + pool: Record, + ): { pregunta: Pregunta; categoria: Categoria } | null { + // Recorrer desde catIndice en círculo + for (let vuelta = 0; vuelta < SECCIONES; vuelta++) { + const idx = (catIndice + vuelta) % SECCIONES + const cat = categorias[idx] + const usadas = usadasPorCat[cat.id] ?? 0 + const preguntasCat = pool[cat.id] ?? [] + if (usadas < preguntasCat.length) { + return { pregunta: preguntasCat[usadas], categoria: cat } + } } - - // Banco genérico - const disponiblesGenericas = bancoGenerico.filter((_, i) => !bancoPreguntasUsadas.includes(i)) - const pool = disponiblesGenericas.length > 0 ? disponiblesGenericas : bancoGenerico - const idx = Math.floor(Math.random() * pool.length) - return { pregunta: pool[idx], esGenerica: true } + return null } function girar() { @@ -73,33 +138,36 @@ export function useRuleta() { setTimeout(() => { const anguloNormalizado = ((360 - (rotacionRef.current % 360)) + 360) % 360 const indiceCategoria = Math.floor(anguloNormalizado / ANGULO_SECCION) % SECCIONES - const categoria = categorias[indiceCategoria] setEstado((prev) => { - const { pregunta, esGenerica } = seleccionarPregunta(categoria, prev.preguntasUsadas, prev.bancoPreguntasUsadas) - const originalIdx = esGenerica - ? bancoGenerico.indexOf(pregunta) - : categoria.preguntas.indexOf(pregunta) + const resultado = obtenerSiguientePregunta( + indiceCategoria, + prev.preguntasUsadasPorCategoria, + prev.preguntasPool, + ) + + if (!resultado) { + // Ya no hay preguntas — marcar como completado + return { + ...prev, + completado: true, + aprobado: prev.correctas >= MIN_CORRECTAS, + } + } return { ...prev, - preguntaActual: pregunta, - categoriaActual: categoria, - preguntasUsadas: esGenerica ? prev.preguntasUsadas : { - ...prev.preguntasUsadas, - [categoria.id]: [...(prev.preguntasUsadas[categoria.id] ?? []), originalIdx], - }, - bancoPreguntasUsadas: esGenerica - ? [...prev.bancoPreguntasUsadas, originalIdx] - : prev.bancoPreguntasUsadas, + preguntaActual: resultado.pregunta, + categoriaActual: resultado.categoria, + tiempoRestante: TIEMPO_LIMITE, } }) }, 4000) } - function responder(indiceOpcion: number | null) { - if (!estado.preguntaActual) return - const esCorrecta = indiceOpcion !== null && indiceOpcion === estado.preguntaActual.correcta + function responder(indiceOpcion: number) { + if (!estado.preguntaActual || !estado.categoriaActual) return + const esCorrecta = indiceOpcion === estado.preguntaActual.correcta if (esCorrecta) sounds.correct?.play() else sounds.incorrect?.play() @@ -117,6 +185,11 @@ export function useRuleta() { feedbackCorrecto: esCorrecta, completado, aprobado, + preguntasUsadasPorCategoria: { + ...prev.preguntasUsadasPorCategoria, + [prev.categoriaActual!.id]: + (prev.preguntasUsadasPorCategoria[prev.categoriaActual!.id] ?? 0) + 1, + }, })) } @@ -127,8 +200,9 @@ export function useRuleta() { categoriaActual: null, mostrarFeedback: false, isSpinning: false, + tiempoRestante: TIEMPO_LIMITE, })) } - return { estado, categorias, girar, responder, cerrarFeedback, MAX_PREGUNTAS, MIN_CORRECTAS } + return { estado, categorias, girar, responder, cerrarFeedback, MAX_PREGUNTAS, MIN_CORRECTAS, TIEMPO_LIMITE } } diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index ebda555..dd48824 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -1,23 +1,43 @@ import type { ReactNode } from 'react' import { useScale } from '../hooks/useScale' import Header from '../components/layout/Header' -import Footer from '../components/layout/Footer' +import ActivityBar from '../components/ui/ActivityBar' interface MainLayoutProps { children: ReactNode + /** Props para el ActivityBar. Si no se pasa, la pantalla hija debe renderizar su propio ActivityBar. */ + barProps?: { + preguntaActual?: number + totalPreguntas?: number + mensaje: string + puedeAnterior?: boolean + puedeSiguiente?: boolean + onPrev?: () => void + onNext?: () => void + } } -export default function MainLayout({ children }: MainLayoutProps) { +export default function MainLayout({ children, barProps }: MainLayoutProps) { const scaleRef = useScale() return (
-
+
{children}
-
) diff --git a/src/main.tsx b/src/main.tsx index 572d6ac..9c0b60a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,6 +2,10 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' +import { setIntentosInfinitos } from './hooks/useIntentos' + +// Activar intentos infinitos +setIntentosInfinitos(true) // Simulador SCORM solo en desarrollo if (import.meta.env.DEV) { diff --git a/src/types/index.ts b/src/types/index.ts index b196eb5..0d57e12 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,6 +4,10 @@ export interface Pregunta { opciones: string[] correcta: number retroalimentacion: string + /** Texto de reflexión educativa (opcional, usa retroalimentacion si no se provee) */ + reflexion?: string + /** Dato curioso para la sección "¿Sabías qué?" (opcional) */ + datoCurioso?: string } export interface Categoria { @@ -16,5 +20,4 @@ export interface Categoria { export interface PreguntasData { categorias: Categoria[] - bancoGenerico: Pregunta[] }