diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b6a17bb --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Firebase Configuration +# Copy this file to .env and fill in your Firebase project credentials. +# Get these values from: Firebase Console > Project Settings > General > Your apps > SDK setup + +VITE_FIREBASE_API_KEY=your_api_key_here +VITE_FIREBASE_AUTH_DOMAIN=your_project_id.firebaseapp.com +VITE_FIREBASE_PROJECT_ID=your_project_id +VITE_FIREBASE_STORAGE_BUCKET=your_project_id.appspot.com +VITE_FIREBASE_MESSAGING_SENDER_ID=your_messaging_sender_id +VITE_FIREBASE_APP_ID=your_app_id +VITE_FIREBASE_MEASUREMENT_ID=G-XXXXXXXXXX diff --git a/.gitignore b/.gitignore index a547bf3..7ef0b74 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,11 @@ dist dist-ssr *.local +# Environment variables — never commit credentials +.env +.env.local +.env.*.local + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/GOOGLE_ANALYTICS_SETUP.md b/GOOGLE_ANALYTICS_SETUP.md new file mode 100644 index 0000000..c6ccf11 --- /dev/null +++ b/GOOGLE_ANALYTICS_SETUP.md @@ -0,0 +1,183 @@ +# Google Analytics - Guía de Implementación + +## ¿Qué se ha instalado? + +Google Analytics ya está integrado en tu proyecto y funcionando automáticamente. Aquí te explico qué hace: + +--- + +## 📊 Rastreo Automático (Ya está funcionando) + +### 1. **Rastreo de Páginas Vistas** + +- Google Analytics rastrea automáticamente cada vez que un usuario navega a una nueva página +- Se registran las rutas: `/`, `/quizzes`, `/animacion/:animacion`, `/quiz/:quizName`, etc. + +### 2. **Rastreo de Interacciones Automáticas** + +Gracias a la "Medición Mejorada" que configuramos, Google Analytics registra: + +- ✅ **Visitas de página** - Cada página que visita +- ✅ **Desplazamientos (Scroll)** - Si bajan en la página +- ✅ **Clics de salida** - Links que salen de tu sitio +- ✅ **Tiempo en página** - Cuánto tiempo pasan en cada sección +- ✅ **Rebotes** - Si salen sin interactuar + +--- + +## 🎯 Rastreo Personalizado (Eventos Específicos) + +Ahora puedes rastrear eventos específicos que importan para tu negocio. + +### Ejemplo 1: Rastrear cuando inicia un quiz + +**En un componente de botón de quiz:** + +```jsx +import { trackEvent } from '../utils/googleAnalytics'; + +function QuizButton({ quizName }) { + const handleClick = () => { + // Rastrear el evento + trackEvent('quiz_iniciado', { + nombre_quiz: quizName, + timestamp: new Date().toISOString(), + }); + + // Luego navegar al quiz + navigate(`/quiz/${quizName}`); + }; + + return ; +} +``` + +### Ejemplo 2: Rastrear cuando completa un quiz + +```jsx +import { trackEvent } from '../utils/googleAnalytics'; + +function QuizResults({ score, totalQuestions }) { + useEffect(() => { + // Rastrear que completó el quiz + trackEvent('quiz_completado', { + puntuacion: score, + total_preguntas: totalQuestions, + porcentaje: (score / totalQuestions) * 100, + }); + }, [score, totalQuestions]); + + return ( +
+

+ Resultado: {score}/{totalQuestions} +

+
+ ); +} +``` + +### Ejemplo 3: Rastrear vistas de animaciones + +```jsx +import { trackEvent } from '../utils/googleAnalytics'; + +function AnimationView({ animationName }) { + useEffect(() => { + trackEvent('animacion_vista', { + nombre_animacion: animationName, + timestamp: new Date().toISOString(), + }); + }, [animationName]); + + return
{/* Contenido de la animación */}
; +} +``` + +--- + +## 📈 ¿Dónde ver los datos en Google Analytics? + +1. **Ir a:** https://analytics.google.com +2. **Seleccionar tu propiedad:** CodeScape3D +3. **Ver en el panel:** + - **Usuarios en tiempo real** - Quién está usando ahora + - **Páginas principales** - Las secciones más visitadas + - **Duración promedio de sesión** - Cuánto tiempo pasan + - **Tasa de rebote** - Quiénes entran y se van sin interactuar + - **Eventos** - Los eventos personalizados que registraste + +--- + +## 📝 ID de Medición + +Tu ID de medición es: **G-6G1Z8N0TNS** + +Este ID ya está configurado en tu proyecto y está enviando datos a Google Analytics. + +--- + +## 🔍 Funciones Disponibles + +### `trackEvent(eventName, eventParams)` + +Rastrear un evento personalizado: + +```jsx +import { trackEvent } from '../utils/googleAnalytics'; + +trackEvent('evento_personalizado', { + parametro1: 'valor1', + parametro2: 'valor2', +}); +``` + +### `trackPageView(pageName)` + +Rastrear una página manualmente: + +```jsx +import { trackPageView } from '../utils/googleAnalytics'; + +trackPageView('/mi-pagina-especial'); +``` + +--- + +## 🚀 Pasos Siguientes Recomendados + +Para maximizar el valor de Google Analytics, considera rastrear: + +1. **Búsquedas** - Si tu app tiene búsqueda + + ```jsx + trackEvent('busqueda_realizada', { query: searchTerm }); + ``` + +2. **Intentos fallidos** - Errores en quizzes + + ```jsx + trackEvent('respuesta_incorrecta', { quiz_name: name }); + ``` + +3. **Descargas/Exportes** - Si los usuarios pueden descargar recursos + + ```jsx + trackEvent('contenido_descargado', { tipo: 'pdf' }); + ``` + +4. **Tiempo en secciones** - Para ver qué les cuesta más trabajo + +--- + +## ⚠️ Importante + +- Google Analytics tarda **~24-48 horas** en mostrar datos nuevos en el dashboard (datos históricos aparecen más rápido) +- Los cambios en eventos personalizados se ven casi inmediatamente +- **Consulta la política de privacidad** de tu sitio - Los usuarios deben saber que estás recopilando datos + +--- + +## 📞 Preguntas? + +¿Necesitas rastrear algo específico? Implementa un evento personalizado usando `trackEvent()` con los parámetros que necesites. diff --git a/README.md b/README.md index 220f4ba..fc0cb1a 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ # CODE-SCAPE-3D + +OVA de Programación y Teoría de Grafos diff --git a/index.html b/index.html index e739f7b..432743f 100644 --- a/index.html +++ b/index.html @@ -17,5 +17,6 @@
+ diff --git a/package-lock.json b/package-lock.json index 6852f45..a23cd97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,12 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.18.0", "@mui/material": "^5.14.3", "@reduxjs/toolkit": "^1.9.5", "animate.css": "^4.1.1", "chart.js": "^4.3.3", + "firebase": "^12.10.0", "i18next": "^23.11.5", "prop-types": "^15.8.1", "react": "^18.2.0", @@ -21,7 +23,8 @@ "react-dom": "^18.2.0", "react-i18next": "^14.1.2", "react-redux": "^8.1.2", - "react-router-dom": "^6.14.2" + "react-router-dom": "^6.14.2", + "web-vitals": "^5.1.0" }, "devDependencies": { "@babel/preset-env": "^7.22.9", @@ -2529,6 +2532,651 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@firebase/ai": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.9.0.tgz", + "integrity": "sha512-NPvBBuvdGo9x3esnABAucFYmqbBmXvyTMimBq2PCuLZbdANZoHzGlx7vfzbwNDaEtCBq4RGGNMliLIv6bZ+PtA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.20", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.20.tgz", + "integrity": "sha512-adGTNVUWH5q66tI/OQuKLSN6mamPpfYhj0radlH2xt+3eL6NFPtXoOs+ulvs+UsmK27vNFx5FjRDfWk+TyduHg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.26", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.26.tgz", + "integrity": "sha512-0j2ruLOoVSwwcXAF53AMoniJKnkwiTjGVfic5LDzqiRkR13vb5j6TXMeix787zbLeQtN/m1883Yv1TxI0gItbA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.20", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.1", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.9", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.9.tgz", + "integrity": "sha512-3gtUX0e584MYkKBQMgSECMvE1Dwzg+eONefDQ0wxVSe5YMBsZwdN5pL7UapwWBlV8+i8QCztF9TP947tEjZAGA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.1.tgz", + "integrity": "sha512-gmKfwQ2k8aUQlOyRshc+fOQLq0OwUmibIZvpuY1RDNu2ho0aTMlwxOuEiJeYOs7AxzhSx7gnXPFNsXCFbnvXUQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.1.tgz", + "integrity": "sha512-yjSvSl5B1u4CirnxhzirN1uiTRCRfx+/qtfbyeyI+8Cx8Cw1RWAIO/OqytPSVwLYbJJ1vEC3EHfxazRaMoWKaA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.1", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.9.tgz", + "integrity": "sha512-e5LzqjO69/N2z7XcJeuMzIp4wWnW696dQeaHAUpQvGk89gIWHAIvG6W+mA3UotGW6jBoqdppEJ9DnuwbcBByug==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.14.9", + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.3.tgz", + "integrity": "sha512-nHOkupcYuGVxI1AJJ/OBhLPaRokbP14Gq4nkkoVvf1yvuREEWqdnrYB/CdsSnPxHMAnn5wJIKngxBF9jNX7s/Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.12.1", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.1", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/@firebase/auth": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.1.tgz", + "integrity": "sha512-nXKj7d5bMBlnq6XpcQQpmnSVwEeHBkoVbY/+Wk0P1ebLSICoH4XPtvKOFlXKfIHmcS84mLQ99fk3njlDGKSDtw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.1.tgz", + "integrity": "sha512-mFzsm7CLHR60o08S23iLUY8m/i6kLpOK87wdEFPLhdlCahaxKmWOwSVGiWoENYSmFJJoDhrR3gKSCxz7ENdIww==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.4.0.tgz", + "integrity": "sha512-vLXM6WHNIR3VtEeYNUb/5GTsUOyl3Of4iWNZHBe1i9f88sYFnxybJNWVBjvJ7flhCyF8UdxGpzWcUnv6F5vGfg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.1.tgz", + "integrity": "sha512-LwIXe8+mVHY5LBPulWECOOIEXDiatyECp/BOlu0gOhe+WOcKjWHROaCbLlkFTgHMY7RHr5MOxkLP/tltWAH3dA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.1.tgz", + "integrity": "sha512-heAEVZ9Z8c8PnBUcmGh91JHX0cXcVa1yESW/xkLuwaX7idRFyLiN8sl73KXpR8ZArGoPXVQDanBnk6SQiekRCQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/database": "1.1.1", + "@firebase/database-types": "1.0.17", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.17.tgz", + "integrity": "sha512-4eWaM5fW3qEIHjGzfi3cf0Jpqi1xQsAdT6rSDE1RZPrWu8oGjgrq6ybMjobtyHQFgwGCykBm4YM89qDzc+uG/w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.14.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.12.0.tgz", + "integrity": "sha512-PM47OyiiAAoAMB8kkq4Je14mTciaRoAPDd3ng3Ckqz9i2TX9D9LfxIRcNzP/OxzNV4uBKRq6lXoOggkJBQR3Gw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "@firebase/webchannel-wrapper": "1.0.5", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.6.tgz", + "integrity": "sha512-NgVyR4hHHN2FvSNQOtbgBOuVsEdD/in30d9FKbEvvITiAChrBN2nBstmhfjI4EOTnHaP8zigwvkNYFI9yKGAkQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/firestore": "4.12.0", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.2.tgz", + "integrity": "sha512-tHduUD+DeokM3NB1QbHCvEMoL16e8Z8JSkmuVA4ROoJKPxHn8ibnecHPO2e3nVCJR1D9OjuKvxz4gksfq92/ZQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.1", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.2.tgz", + "integrity": "sha512-YNxgnezvZDkqxqXa6cT7/oTeD4WXbxgIP7qZp4LFnathQv5o2omM6EoIhXiT9Ie5AoQDcIhG9Y3/dj+DFJGaGQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/functions": "0.13.2", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.20", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.20.tgz", + "integrity": "sha512-LOzvR7XHPbhS0YB5ANXhqXB5qZlntPpwU/4KFwhSNpXNsGk/sBQ9g5hepi0y0/MfenJLe2v7t644iGOOElQaHQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/util": "1.14.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.20.tgz", + "integrity": "sha512-9C9pL/DIEGucmoPj8PlZTnztbX3nhNj5RTYVpUM7wQq/UlHywaYv99969JU/WHLvi9ptzIogXYS9d1eZ6XFe9g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.24", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.24.tgz", + "integrity": "sha512-UtKoubegAhHyehcB7iQjvQ8OVITThPbbWk3g2/2ze42PrQr6oe6OmCElYQkBrE5RDCeMTNucXejbdulrQ2XwVg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.14.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.24.tgz", + "integrity": "sha512-wXH8FrKbJvFuFe6v98TBhAtvgknxKIZtGM/wCVsfpOGmaAE80bD8tBxztl+uochjnFb9plihkd6mC4y7sZXSpA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/messaging": "0.12.24", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.10.tgz", + "integrity": "sha512-8nRFld+Ntzp5cLKzZuG9g+kBaSn8Ks9dmn87UQGNFDygbmR6ebd8WawauEXiJjMj1n70ypkvAOdE+lzeyfXtGA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.23.tgz", + "integrity": "sha512-c7qOAGBUAOpIuUlHu1axWcrCVtIYKPMhH0lMnoCDWnPwn1HcPuPUBVTWETbC7UWw71RMJF8DpirfWXzMWJQfgA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.10", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance/node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.1.tgz", + "integrity": "sha512-L86TReBnPiiJOWd7k9iaiE9f7rHtMpjAoYN0fH2ey2ZRzsOChHV0s5sYf1+IIUYzplzsE46pjlmAUNkRRKwHSQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.22.tgz", + "integrity": "sha512-uW/eNKKtRBot2gnCC5mnoy5Voo2wMzZuQ7dwqqGHU176fO9zFgMwKiRzk+aaC99NLrFk1KOmr0ZVheD+zdJmjQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.8.1", + "@firebase/remote-config-types": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz", + "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.1.tgz", + "integrity": "sha512-uIpYgBBsv1vIET+5xV20XT7wwqV+H4GFp6PBzfmLUcEgguS4SWNFof56Z3uOC2lNDh0KDda1UflYq2VwD9Nefw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.1.tgz", + "integrity": "sha512-bgl3FHHfXAmBgzIK/Fps6Xyv2HiAQlSTov07CBL+RGGhrC5YIk4lruS8JVIC+UkujRdYvnf8cpQFGn2RCilJ/A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/storage": "0.14.1", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.14.0.tgz", + "integrity": "sha512-/gnejm7MKkVIXnSJGpc9L2CvvvzJvtDPeAEq5jAwgVlf/PeNxot+THx/bpD20wQ8uL5sz0xqgXy1nisOYMU+mw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz", + "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", @@ -3419,6 +4067,32 @@ "url": "https://opencollective.com/mui" } }, + "node_modules/@mui/icons-material": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.18.0.tgz", + "integrity": "sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.0.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@mui/material": { "version": "5.14.3", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.3.tgz", @@ -3648,6 +4322,70 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@reduxjs/toolkit": { "version": "1.9.5", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", @@ -4072,8 +4810,7 @@ "node_modules/@types/node": { "version": "20.4.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz", - "integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==", - "dev": true + "integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -4300,7 +5037,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -5093,7 +5829,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -5520,8 +6255,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/entities": { "version": "4.5.0", @@ -5697,7 +6431,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -6183,6 +6916,18 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -6267,6 +7012,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.10.0.tgz", + "integrity": "sha512-tAjHnEirksqWpa+NKDUSUMjulOnsTcsPC1X1rQ+gwPtjlhJS572na91CwaBXQJHXharIrfj7sw/okDkXOsphjA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.9.0", + "@firebase/analytics": "0.10.20", + "@firebase/analytics-compat": "0.2.26", + "@firebase/app": "0.14.9", + "@firebase/app-check": "0.11.1", + "@firebase/app-check-compat": "0.4.1", + "@firebase/app-compat": "0.5.9", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.12.1", + "@firebase/auth-compat": "0.6.3", + "@firebase/data-connect": "0.4.0", + "@firebase/database": "1.1.1", + "@firebase/database-compat": "2.1.1", + "@firebase/firestore": "4.12.0", + "@firebase/firestore-compat": "0.4.6", + "@firebase/functions": "0.13.2", + "@firebase/functions-compat": "0.4.2", + "@firebase/installations": "0.6.20", + "@firebase/installations-compat": "0.2.20", + "@firebase/messaging": "0.12.24", + "@firebase/messaging-compat": "0.2.24", + "@firebase/performance": "0.7.10", + "@firebase/performance-compat": "0.2.23", + "@firebase/remote-config": "0.8.1", + "@firebase/remote-config-compat": "0.2.22", + "@firebase/storage": "0.14.1", + "@firebase/storage-compat": "0.4.1", + "@firebase/util": "1.14.0" + } + }, + "node_modules/firebase/node_modules/@firebase/auth": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.1.tgz", + "integrity": "sha512-nXKj7d5bMBlnq6XpcQQpmnSVwEeHBkoVbY/+Wk0P1ebLSICoH4XPtvKOFlXKfIHmcS84mLQ99fk3njlDGKSDtw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -6411,7 +7216,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -6673,6 +7477,12 @@ "void-elements": "3.1.0" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -6762,8 +7572,7 @@ "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "dev": true + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" }, "node_modules/identity-obj-proxy": { "version": "3.0.0", @@ -6994,7 +7803,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -9549,6 +10357,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -9701,6 +10515,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10598,6 +11418,30 @@ "react-is": "^16.13.1" } }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -10947,7 +11791,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11187,7 +12030,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -11461,7 +12303,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -11553,7 +12394,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -11902,6 +12742,12 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -12287,6 +13133,12 @@ "makeerror": "1.0.12" } }, + "node_modules/web-vitals": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", + "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -12296,6 +13148,29 @@ "node": ">=12" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -12646,7 +13521,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -12663,7 +13537,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -12678,7 +13551,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -12689,8 +13561,7 @@ "node_modules/wrap-ansi/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/wrappy": { "version": "1.0.2", @@ -12751,7 +13622,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -12775,7 +13645,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -12793,7 +13662,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } @@ -14443,6 +15311,460 @@ "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==", "dev": true }, + "@firebase/ai": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.9.0.tgz", + "integrity": "sha512-NPvBBuvdGo9x3esnABAucFYmqbBmXvyTMimBq2PCuLZbdANZoHzGlx7vfzbwNDaEtCBq4RGGNMliLIv6bZ+PtA==", + "requires": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/analytics": { + "version": "0.10.20", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.20.tgz", + "integrity": "sha512-adGTNVUWH5q66tI/OQuKLSN6mamPpfYhj0radlH2xt+3eL6NFPtXoOs+ulvs+UsmK27vNFx5FjRDfWk+TyduHg==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/analytics-compat": { + "version": "0.2.26", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.26.tgz", + "integrity": "sha512-0j2ruLOoVSwwcXAF53AMoniJKnkwiTjGVfic5LDzqiRkR13vb5j6TXMeix787zbLeQtN/m1883Yv1TxI0gItbA==", + "requires": { + "@firebase/analytics": "0.10.20", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.1", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==" + }, + "@firebase/app": { + "version": "0.14.9", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.9.tgz", + "integrity": "sha512-3gtUX0e584MYkKBQMgSECMvE1Dwzg+eONefDQ0wxVSe5YMBsZwdN5pL7UapwWBlV8+i8QCztF9TP947tEjZAGA==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "@firebase/app-check": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.1.tgz", + "integrity": "sha512-gmKfwQ2k8aUQlOyRshc+fOQLq0OwUmibIZvpuY1RDNu2ho0aTMlwxOuEiJeYOs7AxzhSx7gnXPFNsXCFbnvXUQ==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/app-check-compat": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.1.tgz", + "integrity": "sha512-yjSvSl5B1u4CirnxhzirN1uiTRCRfx+/qtfbyeyI+8Cx8Cw1RWAIO/OqytPSVwLYbJJ1vEC3EHfxazRaMoWKaA==", + "requires": { + "@firebase/app-check": "0.11.1", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==" + }, + "@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==" + }, + "@firebase/app-compat": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.9.tgz", + "integrity": "sha512-e5LzqjO69/N2z7XcJeuMzIp4wWnW696dQeaHAUpQvGk89gIWHAIvG6W+mA3UotGW6jBoqdppEJ9DnuwbcBByug==", + "requires": { + "@firebase/app": "0.14.9", + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==" + }, + "@firebase/auth-compat": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.3.tgz", + "integrity": "sha512-nHOkupcYuGVxI1AJJ/OBhLPaRokbP14Gq4nkkoVvf1yvuREEWqdnrYB/CdsSnPxHMAnn5wJIKngxBF9jNX7s/Q==", + "requires": { + "@firebase/auth": "1.12.1", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.1", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/auth": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.1.tgz", + "integrity": "sha512-nXKj7d5bMBlnq6XpcQQpmnSVwEeHBkoVbY/+Wk0P1ebLSICoH4XPtvKOFlXKfIHmcS84mLQ99fk3njlDGKSDtw==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + } + } + }, + "@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==" + }, + "@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "requires": {} + }, + "@firebase/component": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.1.tgz", + "integrity": "sha512-mFzsm7CLHR60o08S23iLUY8m/i6kLpOK87wdEFPLhdlCahaxKmWOwSVGiWoENYSmFJJoDhrR3gKSCxz7ENdIww==", + "requires": { + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/data-connect": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.4.0.tgz", + "integrity": "sha512-vLXM6WHNIR3VtEeYNUb/5GTsUOyl3Of4iWNZHBe1i9f88sYFnxybJNWVBjvJ7flhCyF8UdxGpzWcUnv6F5vGfg==", + "requires": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/database": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.1.tgz", + "integrity": "sha512-LwIXe8+mVHY5LBPulWECOOIEXDiatyECp/BOlu0gOhe+WOcKjWHROaCbLlkFTgHMY7RHr5MOxkLP/tltWAH3dA==", + "requires": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "@firebase/database-compat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.1.tgz", + "integrity": "sha512-heAEVZ9Z8c8PnBUcmGh91JHX0cXcVa1yESW/xkLuwaX7idRFyLiN8sl73KXpR8ZArGoPXVQDanBnk6SQiekRCQ==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/database": "1.1.1", + "@firebase/database-types": "1.0.17", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/database-types": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.17.tgz", + "integrity": "sha512-4eWaM5fW3qEIHjGzfi3cf0Jpqi1xQsAdT6rSDE1RZPrWu8oGjgrq6ybMjobtyHQFgwGCykBm4YM89qDzc+uG/w==", + "requires": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.14.0" + } + }, + "@firebase/firestore": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.12.0.tgz", + "integrity": "sha512-PM47OyiiAAoAMB8kkq4Je14mTciaRoAPDd3ng3Ckqz9i2TX9D9LfxIRcNzP/OxzNV4uBKRq6lXoOggkJBQR3Gw==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "@firebase/webchannel-wrapper": "1.0.5", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + } + }, + "@firebase/firestore-compat": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.6.tgz", + "integrity": "sha512-NgVyR4hHHN2FvSNQOtbgBOuVsEdD/in30d9FKbEvvITiAChrBN2nBstmhfjI4EOTnHaP8zigwvkNYFI9yKGAkQ==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/firestore": "4.12.0", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "requires": {} + }, + "@firebase/functions": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.2.tgz", + "integrity": "sha512-tHduUD+DeokM3NB1QbHCvEMoL16e8Z8JSkmuVA4ROoJKPxHn8ibnecHPO2e3nVCJR1D9OjuKvxz4gksfq92/ZQ==", + "requires": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.1", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/functions-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.2.tgz", + "integrity": "sha512-YNxgnezvZDkqxqXa6cT7/oTeD4WXbxgIP7qZp4LFnathQv5o2omM6EoIhXiT9Ie5AoQDcIhG9Y3/dj+DFJGaGQ==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/functions": "0.13.2", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==" + }, + "@firebase/installations": { + "version": "0.6.20", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.20.tgz", + "integrity": "sha512-LOzvR7XHPbhS0YB5ANXhqXB5qZlntPpwU/4KFwhSNpXNsGk/sBQ9g5hepi0y0/MfenJLe2v7t644iGOOElQaHQ==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/util": "1.14.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "@firebase/installations-compat": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.20.tgz", + "integrity": "sha512-9C9pL/DIEGucmoPj8PlZTnztbX3nhNj5RTYVpUM7wQq/UlHywaYv99969JU/WHLvi9ptzIogXYS9d1eZ6XFe9g==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "requires": {} + }, + "@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/messaging": { + "version": "0.12.24", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.24.tgz", + "integrity": "sha512-UtKoubegAhHyehcB7iQjvQ8OVITThPbbWk3g2/2ze42PrQr6oe6OmCElYQkBrE5RDCeMTNucXejbdulrQ2XwVg==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.14.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "@firebase/messaging-compat": { + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.24.tgz", + "integrity": "sha512-wXH8FrKbJvFuFe6v98TBhAtvgknxKIZtGM/wCVsfpOGmaAE80bD8tBxztl+uochjnFb9plihkd6mC4y7sZXSpA==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/messaging": "0.12.24", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==" + }, + "@firebase/performance": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.10.tgz", + "integrity": "sha512-8nRFld+Ntzp5cLKzZuG9g+kBaSn8Ks9dmn87UQGNFDygbmR6ebd8WawauEXiJjMj1n70ypkvAOdE+lzeyfXtGA==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "dependencies": { + "web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==" + } + } + }, + "@firebase/performance-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.23.tgz", + "integrity": "sha512-c7qOAGBUAOpIuUlHu1axWcrCVtIYKPMhH0lMnoCDWnPwn1HcPuPUBVTWETbC7UWw71RMJF8DpirfWXzMWJQfgA==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.10", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==" + }, + "@firebase/remote-config": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.1.tgz", + "integrity": "sha512-L86TReBnPiiJOWd7k9iaiE9f7rHtMpjAoYN0fH2ey2ZRzsOChHV0s5sYf1+IIUYzplzsE46pjlmAUNkRRKwHSQ==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/installations": "0.6.20", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/remote-config-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.22.tgz", + "integrity": "sha512-uW/eNKKtRBot2gnCC5mnoy5Voo2wMzZuQ7dwqqGHU176fO9zFgMwKiRzk+aaC99NLrFk1KOmr0ZVheD+zdJmjQ==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.8.1", + "@firebase/remote-config-types": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/remote-config-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz", + "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==" + }, + "@firebase/storage": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.1.tgz", + "integrity": "sha512-uIpYgBBsv1vIET+5xV20XT7wwqV+H4GFp6PBzfmLUcEgguS4SWNFof56Z3uOC2lNDh0KDda1UflYq2VwD9Nefw==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/storage-compat": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.1.tgz", + "integrity": "sha512-bgl3FHHfXAmBgzIK/Fps6Xyv2HiAQlSTov07CBL+RGGhrC5YIk4lruS8JVIC+UkujRdYvnf8cpQFGn2RCilJ/A==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/storage": "0.14.1", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + }, + "@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "requires": {} + }, + "@firebase/util": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.14.0.tgz", + "integrity": "sha512-/gnejm7MKkVIXnSJGpc9L2CvvvzJvtDPeAEq5jAwgVlf/PeNxot+THx/bpD20wQ8uL5sz0xqgXy1nisOYMU+mw==", + "requires": { + "tslib": "^2.1.0" + } + }, + "@firebase/webchannel-wrapper": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz", + "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==" + }, + "@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "requires": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "requires": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + } + }, "@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", @@ -15119,6 +16441,14 @@ "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.3.tgz", "integrity": "sha512-QxvrcDqphZoXRjsAmCaQylmWjC/8/qKWwIde1MJMna5YIst3R9O0qhKRPu36/OE2d8AeTbCVjRcRvNqhhW8jyg==" }, + "@mui/icons-material": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.18.0.tgz", + "integrity": "sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg==", + "requires": { + "@babel/runtime": "^7.23.9" + } + }, "@mui/material": { "version": "5.14.3", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.3.tgz", @@ -15237,6 +16567,60 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "@reduxjs/toolkit": { "version": "1.9.5", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", @@ -15575,8 +16959,7 @@ "@types/node": { "version": "20.4.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz", - "integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==", - "dev": true + "integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==" }, "@types/parse-json": { "version": "4.0.0", @@ -15768,8 +17151,7 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "3.2.1", @@ -16318,7 +17700,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -16646,8 +18027,7 @@ "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "entities": { "version": "4.5.0", @@ -16791,8 +18171,7 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-string-regexp": { "version": "1.0.5", @@ -17151,6 +18530,14 @@ "reusify": "^1.0.4" } }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, "fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -17222,6 +18609,54 @@ "path-exists": "^4.0.0" } }, + "firebase": { + "version": "12.10.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.10.0.tgz", + "integrity": "sha512-tAjHnEirksqWpa+NKDUSUMjulOnsTcsPC1X1rQ+gwPtjlhJS572na91CwaBXQJHXharIrfj7sw/okDkXOsphjA==", + "requires": { + "@firebase/ai": "2.9.0", + "@firebase/analytics": "0.10.20", + "@firebase/analytics-compat": "0.2.26", + "@firebase/app": "0.14.9", + "@firebase/app-check": "0.11.1", + "@firebase/app-check-compat": "0.4.1", + "@firebase/app-compat": "0.5.9", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.12.1", + "@firebase/auth-compat": "0.6.3", + "@firebase/data-connect": "0.4.0", + "@firebase/database": "1.1.1", + "@firebase/database-compat": "2.1.1", + "@firebase/firestore": "4.12.0", + "@firebase/firestore-compat": "0.4.6", + "@firebase/functions": "0.13.2", + "@firebase/functions-compat": "0.4.2", + "@firebase/installations": "0.6.20", + "@firebase/installations-compat": "0.2.20", + "@firebase/messaging": "0.12.24", + "@firebase/messaging-compat": "0.2.24", + "@firebase/performance": "0.7.10", + "@firebase/performance-compat": "0.2.23", + "@firebase/remote-config": "0.8.1", + "@firebase/remote-config-compat": "0.2.22", + "@firebase/storage": "0.14.1", + "@firebase/storage-compat": "0.4.1", + "@firebase/util": "1.14.0" + }, + "dependencies": { + "@firebase/auth": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.12.1.tgz", + "integrity": "sha512-nXKj7d5bMBlnq6XpcQQpmnSVwEeHBkoVbY/+Wk0P1ebLSICoH4XPtvKOFlXKfIHmcS84mLQ99fk3njlDGKSDtw==", + "requires": { + "@firebase/component": "0.7.1", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.14.0", + "tslib": "^2.1.0" + } + } + } + }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -17329,8 +18764,7 @@ "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-intrinsic": { "version": "1.2.1", @@ -17517,6 +18951,11 @@ "void-elements": "3.1.0" } }, + "http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==" + }, "http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -17570,8 +19009,7 @@ "idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", - "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", - "dev": true + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" }, "identity-obj-proxy": { "version": "3.0.0", @@ -17731,8 +19169,7 @@ "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-generator-fn": { "version": "2.1.0", @@ -19562,6 +20999,11 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -19659,6 +21101,11 @@ } } }, + "long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -20275,6 +21722,25 @@ "react-is": "^16.13.1" } }, + "protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + } + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -20511,8 +21977,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, "require-from-string": { "version": "2.0.2", @@ -20674,8 +22139,7 @@ "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safe-regex-test": { "version": "1.0.0", @@ -20874,7 +22338,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -20945,7 +22408,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -21210,6 +22672,11 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -21450,12 +22917,32 @@ "makeerror": "1.0.12" } }, + "web-vitals": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", + "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==" + }, "webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==" + }, "whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -21769,7 +23256,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -21780,7 +23266,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -21789,7 +23274,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -21797,8 +23281,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" } } }, @@ -21840,8 +23323,7 @@ "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yallist": { "version": "3.1.1", @@ -21859,7 +23341,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, "requires": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -21873,8 +23354,7 @@ "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" }, "yocto-queue": { "version": "0.1.0", diff --git a/package.json b/package.json index e8842e7..461261e 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,12 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@mui/icons-material": "^5.18.0", "@mui/material": "^5.14.3", "@reduxjs/toolkit": "^1.9.5", "animate.css": "^4.1.1", "chart.js": "^4.3.3", + "firebase": "^12.10.0", "i18next": "^23.11.5", "prop-types": "^15.8.1", "react": "^18.2.0", @@ -26,7 +28,8 @@ "react-dom": "^18.2.0", "react-i18next": "^14.1.2", "react-redux": "^8.1.2", - "react-router-dom": "^6.14.2" + "react-router-dom": "^6.14.2", + "web-vitals": "^5.1.0" }, "devDependencies": { "@babel/preset-env": "^7.22.9", @@ -42,17 +45,17 @@ "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", + "husky": "^8.0.3", "identity-obj-proxy": "^3.0.0", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", + "lint-staged": "^14.0.0", "postcss": "^8.4.27", + "prettier": "^3.0.1", "tailwindcss": "^3.3.3", "vite": "^4.4.5", "vite-plugin-pwa": "^0.16.4", - "whatwg-fetch": "^3.6.17", - "husky": "^8.0.3", - "lint-staged": "^14.0.0", - "prettier": "^3.0.1" + "whatwg-fetch": "^3.6.17" }, "lint-staged": { "**/*.{js,jsx,ts,tsx,json,css,md}": [ diff --git a/src/App.jsx b/src/App.jsx index d38f380..dc91987 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,36 +9,177 @@ import { HomeQuiz, HomeMiniJuegos, } from './components'; -import { QuizResults, QuizView, QuizAnswers, SortingMethodsPage } from './quiz'; +import { + QuizResults, + QuizView, + QuizAnswers, + SortingMethodsPage, + SearchMethodsPage, + DataStructuresPage, +} from './quiz'; import { AnimationView } from './animations'; import { AppTheme } from './theme'; import { Provider } from 'react-redux'; import { store } from './store'; +import { usePageTracking } from './hooks/usePageTracking'; +import useAuth from './hooks/useAuth'; +import useSessionClose from './hooks/useSessionClose'; +import LoginButton from './components/Auth/LoginButton'; + +function ProtectedRoute({ user, children }) { + if (!user) { + return ( +
+ 🔒 +

+ Inicia sesión para continuar +

+

+ Necesitas una cuenta para acceder a este contenido. +

+ +
+ ); + } + return children; +} + +function AppContent() { + usePageTracking(); + const { user, loading, loginTime } = useAuth(); + useSessionClose(loginTime); + + if (loading) { + return ( +
+ Cargando… +
+ ); + } + + return ( +
+ + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + } /> + +
+ ); +} function App() { return ( -
- - - } /> - } /> - } /> - } - /> - } /> - } /> - } /> - } /> - } /> - } /> - -
-
+
diff --git a/src/animations/binarySearch/algorithms/index.js b/src/animations/binarySearch/algorithms/index.js new file mode 100644 index 0000000..feee8b5 --- /dev/null +++ b/src/animations/binarySearch/algorithms/index.js @@ -0,0 +1,156 @@ +/** + * Algoritmo de Búsqueda Binaria + * Complejidad: O(log n) + * Requiere que el array esté ordenado + */ +export const busquedaBinaria = (arr, target) => { + const trace = []; + let left = 0; + let right = arr.length - 1; + let found = false; + let foundIndex = -1; + + // Línea 0: function busquedaBinaria(arr, target) { + // Línea 1: let izquierda = 0; + // Línea 2: let derecha = arr.length - 1; + trace.push({ + array: [...arr], + currentIndex: -1, + leftIndex: left, + rightIndex: right, + midIndex: -1, + target, + found: false, + message: `🔍 Iniciando búsqueda binaria del valor ${target} en un arreglo de ${arr.length} elementos`, + currentLine: 0, + }); + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + + // Línea 3: while (izquierda <= derecha) { + trace.push({ + array: [...arr], + currentIndex: mid, + leftIndex: left, + rightIndex: right, + midIndex: mid, + target, + found: false, + message: `🔄 Iteración: Buscando en rango [${left}...${right}] - ${right - left + 1} elementos`, + currentLine: 3, + }); + + // Línea 4: let medio = Math.floor((izq + der) / 2); + trace.push({ + array: [...arr], + currentIndex: mid, + leftIndex: left, + rightIndex: right, + midIndex: mid, + target, + found: false, + message: `🎯 Calculando punto medio: (${left} + ${right}) ÷ 2 = ${mid} → arr[${mid}] = ${arr[mid]}`, + currentLine: 4, + }); + + // Línea 5: if (arr[medio] === target) { + trace.push({ + array: [...arr], + currentIndex: mid, + leftIndex: left, + rightIndex: right, + midIndex: mid, + target, + found: false, + message: `⚖️ Comparando: arr[${mid}] = ${arr[mid]} vs objetivo ${target}`, + currentLine: 5, + }); + + if (arr[mid] === target) { + found = true; + foundIndex = mid; + + // Línea 6: return medio; // Encontrado + trace.push({ + array: [...arr], + currentIndex: mid, + leftIndex: left, + rightIndex: right, + midIndex: mid, + target, + found: true, + message: `✅ ¡ÉXITO! Elemento ${target} encontrado en la posición ${mid}`, + currentLine: 6, + }); + break; + } else if (arr[mid] < target) { + // Línea 7: } else if (arr[medio] < target) { + // Línea 8: izquierda = medio + 1; + trace.push({ + array: [...arr], + currentIndex: mid, + leftIndex: left, + rightIndex: right, + midIndex: mid, + target, + found: false, + message: `➡️ ${arr[mid]} < ${target}: Descartando mitad izquierda, buscando en [${mid + 1}...${right}]`, + currentLine: 8, + }); + left = mid + 1; + } else { + // Línea 9: } else { + // Línea 10: derecha = medio - 1; + trace.push({ + array: [...arr], + currentIndex: mid, + leftIndex: left, + rightIndex: right, + midIndex: mid, + target, + found: false, + message: `⬅️ ${arr[mid]} > ${target}: Descartando mitad derecha, buscando en [${left}...${mid - 1}]`, + currentLine: 10, + }); + right = mid - 1; + } + } + + if (!found) { + // Línea 13: return -1; // No encontrado + trace.push({ + array: [...arr], + currentIndex: -1, + leftIndex: left, + rightIndex: right, + midIndex: -1, + target, + found: false, + message: `❌ Elemento ${target} NO encontrado. No existe en el arreglo.`, + currentLine: 13, + }); + } + + return trace; +}; + +export const BusquedaBinariaCode = () => { + return `public static int busquedaBinaria(int[] arr, int target) { + int izquierda = 0; + int derecha = arr.length - 1; + while (izquierda <= derecha) { + int medio = (izquierda + derecha) / 2; + if (arr[medio] == target) { + return medio; + } else if (arr[medio] < target) { + izquierda = medio + 1; + } else { + derecha = medio - 1; + } + } + return -1; +}`; +}; + +export default busquedaBinaria; diff --git a/src/animations/binarySearch/components/BinarySearchChart.jsx b/src/animations/binarySearch/components/BinarySearchChart.jsx new file mode 100644 index 0000000..e7750c2 --- /dev/null +++ b/src/animations/binarySearch/components/BinarySearchChart.jsx @@ -0,0 +1,197 @@ +import { useSelector } from 'react-redux'; + +export const BinarySearchChart = () => { + const { + array, + leftIndex, + rightIndex, + midIndex, + foundIndex, + description, + searchValue, + } = useSelector(state => state.binarySearch); + + const getNodeSize = () => { + const count = array.length; + if (count <= 5) return { nodeWidth: 64, fontSize: 18, gap: 8 }; + if (count <= 8) return { nodeWidth: 52, fontSize: 14, gap: 6 }; + if (count <= 12) return { nodeWidth: 44, fontSize: 12, gap: 4 }; + if (count <= 16) return { nodeWidth: 36, fontSize: 11, gap: 3 }; + return { nodeWidth: 30, fontSize: 10, gap: 2 }; + }; + + const nodeStyles = getNodeSize(); + + if (array.length === 0) { + return ( +
+
+
+ 🎯 Carga un arreglo para comenzar +
+
+
+ ); + } + + const getIndicator = index => { + const indicators = []; + if (leftIndex === index) indicators.push('L'); + if (rightIndex === index) indicators.push('R'); + if (midIndex === index) indicators.push('M'); + return indicators.join(' | '); + }; + + const getIndicatorColor = index => { + if (midIndex === index) return 'text-white font-bold text-xs'; + if (leftIndex === index) return 'text-white font-bold text-xs'; + if (rightIndex === index) return 'text-white font-bold text-xs'; + return 'text-gray-400 text-xs'; + }; + + const getBackgroundColor = index => { + // Nodo encontrado - verde brillante + if (foundIndex === index) { + return 'bg-gradient-to-br from-green-400 to-green-600 border-2 border-green-300 shadow-2xl shadow-green-500/70 scale-110'; + } + + // No encontrado - rojo + if (foundIndex === -2) { + return 'bg-gradient-to-br from-red-400/70 to-red-600/70 border-2 border-red-400 shadow-lg shadow-red-500/50'; + } + + // Nodo del medio - verde con borde negro + if (midIndex === index) { + return 'bg-green-500 border-2 border-black shadow-xl shadow-green-400/60 scale-110 transition-all duration-300'; + } + + // Nodo izquierdo - verde con borde morado + if (leftIndex === index) { + return 'bg-green-500 border-2 border-purple-500 shadow-lg shadow-green-400/50 scale-105 transition-all duration-300'; + } + + // Nodo derecho - verde con borde rojo + if (rightIndex === index) { + return 'bg-green-500 border-2 border-red-500 shadow-lg shadow-green-400/50 scale-105 transition-all duration-300'; + } + + // Dentro del rango activo - verde normal + if ( + leftIndex !== -1 && + rightIndex !== -1 && + index >= leftIndex && + index <= rightIndex + ) { + return 'bg-green-500 border-2 border-green-400 shadow-lg shadow-green-400/50 transition-all duration-300'; + } + + // Fuera del rango - gris oscuro + return 'bg-gray-600 border-2 border-gray-700 opacity-40 transition-all duration-300'; + }; + + return ( +
+ {/* Descripción minimalista */} + {description && ( +
+
+

{description}

+
+
+ )} + + {/* Visualización del arreglo */} +
+
+ {array.map((value, index) => ( +
+ {/* Indicador superior */} +
+ {midIndex === index && ( + + ↓ + + )} +
+ + {/* Nodo */} +
+ + {value} + +
+ + {/* Índice */} + + [{index}] + + + {/* Etiquetas L, M, R debajo */} +
+ {leftIndex === index && ( + L + )} + {midIndex === index && ( + M + )} + {rightIndex === index && ( + R + )} +
+
+ ))} +
+
+ + {/* Leyenda */} +
+
+
+ Izquierda (L) +
+
+
+ Medio (M) +
+
+
+ Derecha (R) +
+
+
+ Encontrado +
+
+
+ ); +}; + +export default BinarySearchChart; diff --git a/src/animations/binarySearch/components/BinarySearchCode.jsx b/src/animations/binarySearch/components/BinarySearchCode.jsx new file mode 100644 index 0000000..26abfb3 --- /dev/null +++ b/src/animations/binarySearch/components/BinarySearchCode.jsx @@ -0,0 +1,158 @@ +import { useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { BusquedaBinariaCode } from '../algorithms'; +import { SvgIconDropdown, svgAlgo } from '../../../assets/svg/SvgConstans'; +import { useTranslation } from 'react-i18next'; + +// Componente de codigo para renderizar +const BusquedaBinariaCodeComponent = ({ codeRef }) => { + const code = BusquedaBinariaCode(); + const lines = code.split('\n'); + + return ( +
+      {lines.map((line, index) => (
+        
+ {line} +
+ ))} +
+ ); +}; + +export const BinarySearchCode = () => { + const codeRef = useRef(null); + const [prevCurrentLine, setPrevCurrentLine] = useState(null); + const { history, stepHistory } = useSelector(state => state.binarySearch); + const [infoVisible, setInfoVisible] = useState(false); + const { t } = useTranslation(); + + useEffect(() => { + // Si no hay historial o estamos en -1, limpiar + if (history === -1 || !stepHistory || stepHistory.length === 0) { + if ( + prevCurrentLine !== null && + codeRef.current?.childNodes[prevCurrentLine] + ) { + codeRef.current.childNodes[prevCurrentLine].classList.remove( + 'bg-secondary' + ); + } + setPrevCurrentLine(null); + return; + } + + // Obtener el paso actual + const currentStep = stepHistory[history]; + if (!currentStep || currentStep.currentLine === undefined) return; + + const currentLine = currentStep.currentLine; + const currentElement = codeRef.current?.childNodes[currentLine]; + const prevElement = + prevCurrentLine !== null + ? codeRef.current?.childNodes[prevCurrentLine] + : null; + + // Quitar highlight de línea anterior + if (prevElement && prevCurrentLine !== currentLine) { + prevElement.classList.remove('bg-secondary'); + } + + // Agregar highlight a línea actual + if (currentElement && currentLine !== prevCurrentLine) { + currentElement.classList.add('bg-secondary'); + } + + setPrevCurrentLine(currentLine); + }, [history, stepHistory, prevCurrentLine]); + + return ( +
+ {/* Header del panel de código */} +
+
+ {svgAlgo} + {t('algorithm') || 'Algoritmo'} +
+ +
+ + {/* Contenido del código */} +
+
+ +
+
+ + {/* Panel de información */} + {infoVisible && ( +
+ +
+ )} +
+ ); +}; + +export default BinarySearchCode; diff --git a/src/animations/binarySearch/components/BinarySearchControls.jsx b/src/animations/binarySearch/components/BinarySearchControls.jsx new file mode 100644 index 0000000..eb9e323 --- /dev/null +++ b/src/animations/binarySearch/components/BinarySearchControls.jsx @@ -0,0 +1,456 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { + setArrayBinarySearch, + actionButtonBinarySearch, + updateVisualizationBinarySearch, + incrementHistoryBinarySearch, + decrementHistoryBinarySearch, + setPlayingBinarySearch, + setTimeIdBinarySearch, + restoreTimeIdBinarySearch, + restoreRepeatBinarySearch, + resetBinarySearch, +} from '../../../store'; +import { busquedaBinaria } from '../algorithms'; +import { Alert, Button, Snackbar, TextField } from '@mui/material'; +import { useCallback, useState, useRef, useEffect } from 'react'; +import { BasicButton } from '../../../components'; +import { + svgPause, + svgBack, + svgPlay, + svgRepeat, + svgForward, +} from '../../../assets/svg/SvgConstans'; + +export const BinarySearchControls = () => { + const binarySearchState = useSelector(state => state.binarySearch); + const dispatch = useDispatch(); + + const [inputArray, setInputArray] = useState('1, 3, 5, 7, 12, 15, 20, 25'); + const [searchValue, setSearchValue] = useState(''); + const [toast, setToast] = useState(''); + const [open, setOpen] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); + + const animationExecutedRef = useRef(false); + const currentActionRef = useRef(null); + + const handleArrayChange = useCallback(event => { + setInputArray(event.target.value); + }, []); + + const handleSearchValueChange = useCallback(event => { + const inputValue = event.target.value; + if (inputValue === '' || !isNaN(inputValue)) { + setSearchValue(inputValue); + } + }, []); + + const showError = useCallback(message => { + setToast(message); + setOpen(true); + }, []); + + const clearActiveTimeouts = useCallback(() => { + if (binarySearchState.timeIds && binarySearchState.timeIds.length > 0) { + binarySearchState.timeIds.forEach(timeoutId => clearTimeout(timeoutId)); + dispatch(restoreTimeIdBinarySearch()); + } + }, [binarySearchState.timeIds, dispatch]); + + const runAnimation = useCallback( + (stepHistory, startFromBeginning = false) => { + if (!stepHistory || stepHistory.length === 0) { + setIsAnimating(false); + return; + } + + clearActiveTimeouts(); + dispatch(setPlayingBinarySearch(true)); + setIsAnimating(true); + + const timeIds = []; + if (startFromBeginning) { + dispatch(updateVisualizationBinarySearch(stepHistory[0])); + dispatch(incrementHistoryBinarySearch()); + } + + const startIndex = startFromBeginning ? 1 : 0; + + // Velocidad mejorada: 800ms para permitir ver mejor cada paso + const animationSpeed = 800; + + for (let i = startIndex; i < stepHistory.length; i++) { + const step = stepHistory[i]; + const timeoutId = setTimeout( + () => { + dispatch(updateVisualizationBinarySearch(step)); + dispatch(incrementHistoryBinarySearch()); + }, + (i - startIndex) * animationSpeed + ); + timeIds.push(timeoutId); + } + + const finalTimeoutId = setTimeout( + () => { + dispatch(setPlayingBinarySearch(false)); + dispatch(restoreTimeIdBinarySearch()); + setIsAnimating(false); + animationExecutedRef.current = false; + }, + (stepHistory.length - startIndex) * animationSpeed + ); + + timeIds.push(finalTimeoutId); + dispatch(setTimeIdBinarySearch(timeIds)); + }, + [dispatch, clearActiveTimeouts] + ); + + const executeSearch = useCallback( + value => { + if (isAnimating) { + showError('Espera a que termine la animación actual'); + return; + } + + if (binarySearchState.array.length === 0) { + showError('Primero carga un arreglo'); + return; + } + + try { + clearActiveTimeouts(); + dispatch(setPlayingBinarySearch(false)); + + const arr = [...binarySearchState.array]; + const trace = busquedaBinaria(arr, value); + + animationExecutedRef.current = false; + currentActionRef.current = 'busquedaBinaria'; + + dispatch(actionButtonBinarySearch({ trace, value })); + } catch (error) { + console.error('Error executing search:', error); + showError('Error al ejecutar la búsqueda'); + setIsAnimating(false); + } + }, + [ + dispatch, + binarySearchState.array, + isAnimating, + showError, + clearActiveTimeouts, + ] + ); + + useEffect(() => { + if ( + binarySearchState.stepHistory && + binarySearchState.stepHistory.length > 0 && + binarySearchState.funAction === currentActionRef.current && + !binarySearchState.isPlaying && + !animationExecutedRef.current && + binarySearchState.history === -1 + ) { + animationExecutedRef.current = true; + runAnimation(binarySearchState.stepHistory, true); + } + }, [ + binarySearchState.stepHistory, + binarySearchState.funAction, + binarySearchState.isPlaying, + binarySearchState.history, + runAnimation, + ]); + + const handleLoadArray = () => { + const values = inputArray + .split(',') + .map(v => parseInt(v.trim(), 10)) + .filter(v => !isNaN(v)) + .sort((a, b) => a - b); // Ordenar automáticamente + + if (values.length === 0) { + showError('Ingresa valores válidos separados por comas'); + return; + } + + if (values.length > 20) { + showError('Máximo 20 elementos permitidos'); + return; + } + + dispatch(setArrayBinarySearch(values)); + dispatch(resetBinarySearch()); + clearActiveTimeouts(); + setInputArray(values.join(', ')); // Actualizar input con array ordenado + setToast('Arreglo cargado y ordenado'); + setOpen(true); + }; + + const handleRandomArray = () => { + const length = Math.floor(Math.random() * 8) + 5; + const randomArray = Array.from( + { length }, + () => Math.floor(Math.random() * 50) + 1 + ).sort((a, b) => a - b); + setInputArray(randomArray.join(', ')); + dispatch(setArrayBinarySearch(randomArray)); + dispatch(resetBinarySearch()); + clearActiveTimeouts(); + setToast('Arreglo aleatorio ordenado generado'); + setOpen(true); + }; + + const handleBuscar = () => { + const numValue = searchValue === '' ? null : parseInt(searchValue); + if (numValue === null) { + showError('Ingresa un valor válido para buscar'); + return; + } + executeSearch(numValue); + setSearchValue(''); + }; + + const onPlayPause = e => { + e.preventDefault(); + if (binarySearchState.isPlaying) { + pause(); + } else { + play(); + } + }; + + const play = () => { + if ( + binarySearchState.stepHistory && + binarySearchState.stepHistory.length > 0 + ) { + const currentHistory = binarySearchState.history || -1; + const remainingSteps = binarySearchState.stepHistory.slice( + Math.max(0, currentHistory + 1) + ); + + if (remainingSteps.length > 0) { + animationExecutedRef.current = true; + runAnimation(remainingSteps, false); + } else { + dispatch(restoreRepeatBinarySearch()); + animationExecutedRef.current = true; + runAnimation(binarySearchState.stepHistory, true); + } + } + }; + + const pause = () => { + dispatch(setPlayingBinarySearch(false)); + clearActiveTimeouts(); + setIsAnimating(false); + }; + + const goBackward = () => { + if (binarySearchState.isPlaying) pause(); + const currentHistory = binarySearchState.history || -1; + if (currentHistory > 0 && binarySearchState.stepHistory) { + const previousStep = binarySearchState.stepHistory[currentHistory - 1]; + dispatch(decrementHistoryBinarySearch()); + dispatch(updateVisualizationBinarySearch(previousStep)); + } + }; + + const goForward = () => { + if (binarySearchState.isPlaying) pause(); + const currentHistory = binarySearchState.history || -1; + if ( + binarySearchState.stepHistory && + currentHistory < binarySearchState.stepHistory.length - 1 + ) { + const nextStep = binarySearchState.stepHistory[currentHistory + 1]; + dispatch(incrementHistoryBinarySearch()); + dispatch(updateVisualizationBinarySearch(nextStep)); + } + }; + + const repeat = () => { + if ( + binarySearchState.stepHistory && + binarySearchState.stepHistory.length > 0 + ) { + clearActiveTimeouts(); + dispatch(restoreRepeatBinarySearch()); + animationExecutedRef.current = true; + runAnimation(binarySearchState.stepHistory, true); + } + }; + + useEffect(() => { + return () => clearActiveTimeouts(); + }, [clearActiveTimeouts]); + + const playPauseIcon = binarySearchState.isPlaying ? svgPause : svgPlay; + + return ( +
+ {/* Controles de reproducción */} +
+ + {svgBack} + + + {playPauseIcon} + + = + binarySearchState.stepHistory.length - 1 + } + title="Avanzar" + > + {svgForward} + + + {svgRepeat} + +
+ + {/* Input del arreglo con botones Cargar y Aleatorio en la misma línea */} +
+ + + +
+ + {/* Input de búsqueda */} +
+ + +
+ + {/* Indicador de progreso elegante */} + {binarySearchState.stepHistory && + binarySearchState.stepHistory.length > 0 && ( +
+
+ + Paso {Math.max(0, binarySearchState.history + 1)} de{' '} + {binarySearchState.stepHistory.length} + + + {binarySearchState.isPlaying + ? '▶️ En progreso...' + : '⏸️ Pausado'} + +
+
+
+
+
+ )} + + setOpen(false)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + setOpen(false)}> + {toast} + + +
+ ); +}; + +export default BinarySearchControls; diff --git a/src/animations/binarySearch/components/index.js b/src/animations/binarySearch/components/index.js new file mode 100644 index 0000000..2427008 --- /dev/null +++ b/src/animations/binarySearch/components/index.js @@ -0,0 +1,3 @@ +export { BinarySearchChart } from './BinarySearchChart'; +export { BinarySearchControls } from './BinarySearchControls'; +export { BinarySearchCode } from './BinarySearchCode'; diff --git a/src/animations/binarySearch/index.js b/src/animations/binarySearch/index.js new file mode 100644 index 0000000..399cdff --- /dev/null +++ b/src/animations/binarySearch/index.js @@ -0,0 +1,4 @@ +export { BinarySearchChart } from './components/BinarySearchChart'; +export { BinarySearchControls } from './components/BinarySearchControls'; +export { BinarySearchCode } from './components/BinarySearchCode'; +export { busquedaBinaria, BusquedaBinariaCode } from './algorithms'; diff --git a/src/animations/doubleList/algorithms/DoubleListAlgorithm.jsx b/src/animations/doubleList/algorithms/DoubleListAlgorithm.jsx new file mode 100644 index 0000000..5277604 --- /dev/null +++ b/src/animations/doubleList/algorithms/DoubleListAlgorithm.jsx @@ -0,0 +1,1009 @@ +import NodoDoble from './NodoDoble'; + +// Función para copiar lista doble completa +export const copiarListaDoble = cabeza => { + if (cabeza === null) return null; + + let nuevaCabeza = new NodoDoble(cabeza.getValue()); + let actualNuevo = nuevaCabeza; + let actualViejo = cabeza.getNext(); + + while (actualViejo !== null) { + const nuevoNodo = new NodoDoble(actualViejo.getValue()); + nuevoNodo.setPrev(actualNuevo); + actualNuevo.setNext(nuevoNodo); + actualNuevo = nuevoNodo; + actualViejo = actualViejo.getNext(); + } + + return nuevaCabeza; +}; + +// Función para crear registro de animación +export const crearRegistro = ( + cabeza, + primerConjunto = [], + segundoConjunto = [], + tercerConjunto = [], + indiceActual = 0, + nodoResaltado = null, + puntero = null, + descripcion = '', + codigoResaltado = [] +) => { + return { + head: copiarListaDoble(cabeza), + firstSet: [...primerConjunto], + secondSet: [...segundoConjunto], + thirdSet: [...tercerConjunto], + currentIndex: indiceActual, + highlightedNode: nodoResaltado, + pointer: puntero, + description: descripcion, + highlightedCode: [...codigoResaltado], + }; +}; + +// Algoritmo: Insertar al inicio +export const InsertarAlInicio = (cabeza, valor) => { + const historialPasos = []; + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + 'Estado inicial de la lista doblemente enlazada', + [0] + ) + ); + + const nuevoNodo = new NodoDoble(valor); + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [], + [], + 1, + null, + null, + `Creando nuevo nodo con valor ${valor}`, + [1] + ) + ); + + if (cabeza === null) { + cabeza = nuevoNodo; + historialPasos.push( + crearRegistro( + cabeza, + [], + [valor], + [], + 2, + null, + null, + 'Lista vacía: el nuevo nodo se convierte en la cabeza', + [2, 3] + ) + ); + } else { + nuevoNodo.setNext(cabeza); + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [], + [], + 2, + null, + null, + 'Conectando nuevo nodo con la cabeza actual (next)', + [2] + ) + ); + + cabeza.setPrev(nuevoNodo); + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [], + [], + 3, + null, + null, + 'Conectando cabeza actual con nuevo nodo (prev)', + [3] + ) + ); + + cabeza = nuevoNodo; + historialPasos.push( + crearRegistro( + cabeza, + [], + [valor], + [], + 4, + null, + null, + 'Actualizando cabeza al nuevo nodo', + [4] + ) + ); + } + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 5, + null, + null, + 'Inserción al inicio completada exitosamente', + [5] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Algoritmo: Insertar al final +export const InsertarAlFinal = (cabeza, valor) => { + const historialPasos = []; + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + 'Estado inicial de la lista doblemente enlazada', + [0] + ) + ); + + const nuevoNodo = new NodoDoble(valor); + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [], + [], + 1, + null, + null, + `Creando nuevo nodo con valor ${valor}`, + [1] + ) + ); + + if (cabeza === null) { + cabeza = nuevoNodo; + historialPasos.push( + crearRegistro( + cabeza, + [], + [valor], + [], + 2, + null, + null, + 'Lista vacía: el nuevo nodo se convierte en la cabeza', + [2] + ) + ); + } else { + let nodoActual = cabeza; + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [nodoActual.getValue()], + [], + 2, + nodoActual.getValue(), + null, + 'Iniciando búsqueda del último nodo', + [3, 4] + ) + ); + + while (nodoActual.getNext() !== null) { + nodoActual = nodoActual.getNext(); + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [nodoActual.getValue()], + [], + 3, + nodoActual.getValue(), + null, + `Recorriendo lista, nodo actual: ${nodoActual.getValue()}`, + [5, 6] + ) + ); + } + + nodoActual.setNext(nuevoNodo); + nuevoNodo.setPrev(nodoActual); + historialPasos.push( + crearRegistro( + cabeza, + [], + [nuevoNodo.getValue()], + [], + 4, + null, + null, + 'Conectando último nodo con el nuevo nodo (doble enlace)', + [7, 8] + ) + ); + } + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 5, + null, + null, + 'Inserción al final completada exitosamente', + [9] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Algoritmo: Insertar en posición +export const InsertarEnPosicion = (cabeza, valor, posicion) => { + const historialPasos = []; + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + `Insertando valor ${valor} en posición ${posicion}`, + [0] + ) + ); + + if (posicion < 0) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Error: Posición inválida (menor que 0)', + [1] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + if (posicion === 0) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Posición 0 detectada, redirigiendo a insertar al inicio', + [1] + ) + ); + return InsertarAlInicio(cabeza, valor); + } + + const nuevoNodo = new NodoDoble(valor); + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [], + [], + 1, + null, + null, + `Creando nuevo nodo con valor ${valor}`, + [2] + ) + ); + + let actual = cabeza; + let contador = 0; + + if (actual === null) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 2, + null, + null, + 'Error: No se puede insertar en posición mayor que 0 en lista vacía', + [3] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + while (contador < posicion - 1 && actual !== null) { + historialPasos.push( + crearRegistro( + cabeza, + [actual.getValue()], + [], + [], + 2, + actual.getValue(), + null, + `Navegando a posición ${contador}, nodo actual: ${actual.getValue()}`, + [4, 5] + ) + ); + actual = actual.getNext(); + contador++; + } + + if (actual === null) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 3, + null, + null, + `Error: Posición ${posicion} fuera de rango`, + [6] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + historialPasos.push( + crearRegistro( + cabeza, + [actual.getValue()], + [], + [], + 3, + actual.getValue(), + null, + `Posicionado en nodo anterior (posición ${contador})`, + [6] + ) + ); + + nuevoNodo.setNext(actual.getNext()); + nuevoNodo.setPrev(actual); + + if (actual.getNext() !== null) { + actual.getNext().setPrev(nuevoNodo); + } + actual.setNext(nuevoNodo); + + historialPasos.push( + crearRegistro( + cabeza, + [], + [valor], + [], + 4, + nuevoNodo.getValue(), + null, + `Insertando nodo en posición ${posicion} con doble enlace`, + [7, 8, 9, 10] + ) + ); + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 5, + null, + null, + 'Inserción en posición completada exitosamente', + [11] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Algoritmo: Eliminar del inicio +export const EliminarDelInicio = cabeza => { + const historialPasos = []; + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + 'Eliminando nodo del inicio de la lista', + [0] + ) + ); + + if (cabeza === null) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Error: No se puede eliminar de una lista vacía', + [1] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + const valorEliminado = cabeza.getValue(); + historialPasos.push( + crearRegistro( + cabeza, + [], + [valorEliminado], + [], + 2, + cabeza.getValue(), + null, + `Identificando nodo a eliminar: ${valorEliminado}`, + [2] + ) + ); + + cabeza = cabeza.getNext(); + + if (cabeza !== null) { + cabeza.setPrev(null); + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 3, + null, + null, + 'Actualizando cabeza y eliminando enlace prev', + [3, 4] + ) + ); + } else { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 3, + null, + null, + 'La lista ahora está vacía', + [3] + ) + ); + } + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 4, + null, + null, + `Nodo ${valorEliminado} eliminado exitosamente`, + [5] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Algoritmo: Eliminar del final +export const EliminarDelFinal = cabeza => { + const historialPasos = []; + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + 'Eliminando nodo del final de la lista', + [0] + ) + ); + + if (cabeza === null) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Error: No se puede eliminar de una lista vacía', + [1] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + if (cabeza.getNext() === null) { + const valorEliminado = cabeza.getValue(); + historialPasos.push( + crearRegistro( + cabeza, + [], + [valorEliminado], + [], + 2, + valorEliminado, + null, + `Único nodo encontrado: ${valorEliminado}`, + [2] + ) + ); + cabeza = null; + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 3, + null, + null, + 'La lista ahora está vacía', + [3] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + let nodoActual = cabeza; + historialPasos.push( + crearRegistro( + cabeza, + [nodoActual.getValue()], + [], + [], + 2, + nodoActual.getValue(), + null, + 'Buscando el último nodo', + [2] + ) + ); + + while (nodoActual.getNext() !== null) { + nodoActual = nodoActual.getNext(); + historialPasos.push( + crearRegistro( + cabeza, + [nodoActual.getValue()], + [], + [], + 3, + nodoActual.getValue(), + null, + `Recorriendo lista, nodo actual: ${nodoActual.getValue()}`, + [3, 4] + ) + ); + } + + const valorEliminado = nodoActual.getValue(); + historialPasos.push( + crearRegistro( + cabeza, + [], + [valorEliminado], + [], + 4, + valorEliminado, + null, + `Último nodo encontrado: ${valorEliminado}`, + [5] + ) + ); + + nodoActual.getPrev().setNext(null); + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 5, + null, + null, + `Nodo ${valorEliminado} eliminado exitosamente`, + [6] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Algoritmo: Eliminar en posición +export const EliminarEnPosicion = (cabeza, posicion) => { + const historialPasos = []; + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + `Eliminando nodo en posición ${posicion}`, + [0] + ) + ); + + if (cabeza === null) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Error: No se puede eliminar de una lista vacía', + [1] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + if (posicion < 0) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Error: Posición inválida', + [1] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + if (posicion === 0) { + return EliminarDelInicio(cabeza); + } + + let actual = cabeza; + let contador = 0; + + while (contador < posicion && actual !== null) { + historialPasos.push( + crearRegistro( + cabeza, + [actual.getValue()], + [], + [], + 2, + actual.getValue(), + null, + `Navegando a posición ${contador}`, + [2, 3] + ) + ); + actual = actual.getNext(); + contador++; + } + + if (actual === null) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 3, + null, + null, + `Error: Posición ${posicion} fuera de rango`, + [4] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + const valorEliminado = actual.getValue(); + historialPasos.push( + crearRegistro( + cabeza, + [], + [valorEliminado], + [], + 4, + valorEliminado, + null, + `Nodo a eliminar encontrado: ${valorEliminado}`, + [5] + ) + ); + + if (actual.getPrev() !== null) { + actual.getPrev().setNext(actual.getNext()); + } + if (actual.getNext() !== null) { + actual.getNext().setPrev(actual.getPrev()); + } + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 5, + null, + null, + `Nodo ${valorEliminado} eliminado exitosamente (enlaces actualizados)`, + [6, 7] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Algoritmo: Buscar +export const Buscar = (cabeza, valor) => { + const historialPasos = []; + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + `Buscando valor ${valor} en la lista`, + [0] + ) + ); + + if (cabeza === null) { + historialPasos.push( + crearRegistro(cabeza, [], [], [], 1, null, null, 'La lista está vacía', [ + 1, + ]) + ); + return { historialPasos, nuevaCabeza: cabeza, encontrado: false }; + } + + let nodoActual = cabeza; + let posicion = 0; + + while (nodoActual !== null) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [nodoActual.getValue()], + 2, + nodoActual.getValue(), + nodoActual.getValue(), + `Comparando con nodo en posición ${posicion}: ${nodoActual.getValue()}`, + [2, 3] + ) + ); + + if (nodoActual.getValue() === valor) { + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [], + [], + 3, + valor, + valor, + `¡Valor ${valor} encontrado en posición ${posicion}!`, + [4] + ) + ); + return { + historialPasos, + nuevaCabeza: cabeza, + encontrado: true, + posicion, + }; + } + + nodoActual = nodoActual.getNext(); + posicion++; + } + + historialPasos.push( + crearRegistro( + cabeza, + [], + [valor], + [], + 4, + null, + null, + `Valor ${valor} no encontrado en la lista`, + [5] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza, encontrado: false }; +}; + +// Componentes de código para visualización +export const InsertarAlInicioCode = ({ codeRef }) => ( +
+

{'public void insertarAlInicio(int valor) {'}

+

{' NodeDoble nuevoNodo = new NodeDoble(valor);'}

+

{' nuevoNodo.setNext(cabeza);'}

+

{' if (cabeza != null) cabeza.setPrev(nuevoNodo);'}

+

{' cabeza = nuevoNodo;'}

+

{'}'}

+
+); + +export const InsertarAlFinalCode = ({ codeRef }) => ( +
+

{'public void insertarAlFinal(int valor) {'}

+

{' NodeDoble nuevoNodo = new NodeDoble(valor);'}

+

{' if (cabeza == null) { cabeza = nuevoNodo; return; }'}

+

{' NodeDoble actual = cabeza;'}

+

{' while (actual.getNext() != null)'}

+

{' actual = actual.getNext();'}

+

{' actual.setNext(nuevoNodo);'}

+

{' nuevoNodo.setPrev(actual);'}

+

{'}'}

+
+); + +export const InsertarEnPosicionCode = ({ codeRef }) => ( +
+

{'public void insertarEnPosicion(int valor, int pos) {'}

+

{' if (pos == 0) { insertarAlInicio(valor); return; }'}

+

{' NodeDoble nuevoNodo = new NodeDoble(valor);'}

+

{' NodeDoble actual = cabeza;'}

+

{' for (int i = 0; i < pos - 1 && actual != null; i++)'}

+

{' actual = actual.getNext();'}

+

{' if (actual == null) return;'}

+

{' nuevoNodo.setNext(actual.getNext());'}

+

{' nuevoNodo.setPrev(actual);'}

+

{' if (actual.getNext() != null)'}

+

{' actual.getNext().setPrev(nuevoNodo);'}

+

{' actual.setNext(nuevoNodo);'}

+

{'}'}

+
+); + +export const EliminarDelInicioCode = ({ codeRef }) => ( +
+

{'public int eliminarDelInicio() {'}

+

{' if (cabeza == null) return -1;'}

+

{' int valor = cabeza.getValue();'}

+

{' cabeza = cabeza.getNext();'}

+

{' if (cabeza != null) cabeza.setPrev(null);'}

+

{' return valor;'}

+

{'}'}

+
+); + +export const EliminarDelFinalCode = ({ codeRef }) => ( +
+

{'public int eliminarDelFinal() {'}

+

{' if (cabeza == null) return -1;'}

+

{' if (cabeza.getNext() == null) {'}

+

{' int valor = cabeza.getValue();'}

+

{' cabeza = null;'}

+

{' return valor;'}

+

{' }'}

+

{' NodeDoble actual = cabeza;'}

+

{' while (actual.getNext() != null)'}

+

{' actual = actual.getNext();'}

+

{' int valor = actual.getValue();'}

+

{' actual.getPrev().setNext(null);'}

+

{' return valor;'}

+

{'}'}

+
+); + +export const EliminarEnPosicionCode = ({ codeRef }) => ( +
+

{'public int eliminarEnPosicion(int pos) {'}

+

{' if (pos == 0) return eliminarDelInicio();'}

+

{' NodeDoble actual = cabeza;'}

+

{' for (int i = 0; i < pos && actual != null; i++)'}

+

{' actual = actual.getNext();'}

+

{' if (actual == null) return -1;'}

+

{' int valor = actual.getValue();'}

+

{' if (actual.getPrev() != null)'}

+

{' actual.getPrev().setNext(actual.getNext());'}

+

{' if (actual.getNext() != null)'}

+

{' actual.getNext().setPrev(actual.getPrev());'}

+

{' return valor;'}

+

{'}'}

+
+); + +export const BuscarCode = ({ codeRef }) => ( +
+

{'public int buscar(int valor) {'}

+

{' if (cabeza == null) return -1;'}

+

{' NodeDoble actual = cabeza;'}

+

{' int pos = 0;'}

+

{' while (actual != null) {'}

+

{' if (actual.getValue() == valor) return pos;'}

+

{' actual = actual.getNext();'}

+

{' pos++;'}

+

{' }'}

+

{' return -1;'}

+

{'}'}

+
+); diff --git a/src/animations/doubleList/algorithms/NodoDoble.js b/src/animations/doubleList/algorithms/NodoDoble.js new file mode 100644 index 0000000..5d6290a --- /dev/null +++ b/src/animations/doubleList/algorithms/NodoDoble.js @@ -0,0 +1,45 @@ +class NodoDoble { + constructor(value) { + this.value = value; + this.next = null; + this.prev = null; + } + + getValue() { + return this.value; + } + + setValue(value) { + this.value = value; + } + + getNext() { + return this.next; + } + + setNext(nextNode) { + this.next = nextNode; + } + + getPrev() { + return this.prev; + } + + setPrev(prevNode) { + this.prev = prevNode; + } + + hasNext() { + return this.next !== null; + } + + hasPrev() { + return this.prev !== null; + } + + toString() { + return `NodoDoble(${this.value})`; + } +} + +export default NodoDoble; diff --git a/src/animations/doubleList/algorithms/helpers.jsx b/src/animations/doubleList/algorithms/helpers.jsx new file mode 100644 index 0000000..9b71aaf --- /dev/null +++ b/src/animations/doubleList/algorithms/helpers.jsx @@ -0,0 +1,36 @@ +export const newTraceDoubleList = head => { + return [ + { + head: head, + firstSet: [], + secondSet: [], + thirdSet: [], + isHead: head ? head.getValue() : -1, + currentIndex: null, + highlightedNode: null, + pointer: null, + }, + ]; +}; + +export const moveInHistoryRecordDoubleList = ( + stepHistory, + head, + firstSet = [], + secondSet = [], + thirdSet = [], + currentIndex = null, + highlightedNode = null, + pointer = null +) => { + stepHistory.push({ + head: head, + firstSet: [...firstSet], + secondSet: [...secondSet], + thirdSet: [...thirdSet], + isHead: head ? head.getValue() : -1, + currentIndex: currentIndex, + highlightedNode: highlightedNode, + pointer: pointer, + }); +}; diff --git a/src/animations/doubleList/algorithms/index.js b/src/animations/doubleList/algorithms/index.js new file mode 100644 index 0000000..6d72cab --- /dev/null +++ b/src/animations/doubleList/algorithms/index.js @@ -0,0 +1,2 @@ +export * from './DoubleListAlgorithm'; +export { default as NodoDoble } from './NodoDoble'; diff --git a/src/animations/doubleList/components/DoubleListChart.jsx b/src/animations/doubleList/components/DoubleListChart.jsx new file mode 100644 index 0000000..2c2350b --- /dev/null +++ b/src/animations/doubleList/components/DoubleListChart.jsx @@ -0,0 +1,231 @@ +import { useSelector } from 'react-redux'; +import { useState, useEffect, useMemo } from 'react'; + +// Función para convertir lista a array +const listToArray = (node, arr = []) => { + if (!node) return arr; + arr.push(node); + return listToArray(node.next, arr); +}; + +export const DoubleListChart = () => { + const { head, firstSet, secondSet, thirdSet, highlightedNode, pointer } = + useSelector(state => state.doubleList); + + const nodes = useMemo(() => listToArray(head), [head]); + const nodeCount = nodes.length; + + // Calcular tamaño dinámico basado en cantidad de nodos + const getNodeSize = () => { + if (nodeCount <= 4) + return { + size: 'normal', + nodeWidth: 64, + fontSize: 18, + arrowWidth: 32, + padding: 16, + }; + if (nodeCount <= 6) + return { + size: 'medium', + nodeWidth: 52, + fontSize: 14, + arrowWidth: 24, + padding: 12, + }; + if (nodeCount <= 8) + return { + size: 'small', + nodeWidth: 44, + fontSize: 12, + arrowWidth: 16, + padding: 8, + }; + return { + size: 'tiny', + nodeWidth: 36, + fontSize: 10, + arrowWidth: 12, + padding: 6, + }; + }; + + const nodeStyles = getNodeSize(); + + if (!head) { + return ( +
+
+
+ 📋 Lista doble vacía +
+
+
+ ); + } + + return ( +
+
+
+ {nodes.map((node, index) => ( +
+ {/* Nodo */} + + + {/* Flecha conectora doble */} + {node.getNext() && ( + + )} +
+ ))} +
+
+
+ ); +}; + +const NodeComponent = ({ + value, + firstSet, + secondSet, + thirdSet, + isHighlighted, + isPointed, + nodeStyles, + isHead = false, +}) => { + const [isAnimating, setIsAnimating] = useState(false); + + const getBackgroundColor = () => { + if (firstSet.includes(value)) + return 'bg-gradient-to-br from-green-400 to-green-600'; + if (secondSet.includes(value)) + return 'bg-gradient-to-br from-red-400 to-red-600'; + if (thirdSet.includes(value)) + return 'bg-gradient-to-br from-yellow-400 to-yellow-600'; + return 'bg-gradient-to-br from-purple-400 to-purple-600'; + }; + + useEffect(() => { + if (isHighlighted) { + setIsAnimating(true); + const timer = setTimeout(() => setIsAnimating(false), 1000); + return () => clearTimeout(timer); + } + }, [isHighlighted]); + + const isDangerNode = secondSet.includes(value); + + return ( +
+ {/* Indicador HEAD - solo para el primer nodo */} + {isHead && ( +
+
+ HEAD +
+
+ )} + + {/* Puntero */} + {isPointed && ( +
+
+ 🎯 Puntero +
+
+
+ )} + + {/* Nodo principal */} +
+ {/* Efecto de brillo */} +
+ + {/* Valor del nodo */} +
+ {value} +
+ + {/* Efecto de resaltado */} + {isHighlighted && ( +
+ )} +
+
+ ); +}; + +// Componente de flecha doble (bidireccional) +const DoubleArrowComponent = ({ arrowWidth }) => { + return ( +
+ {/* Flecha hacia adelante (arriba) */} +
+
+
+
+ + {/* Espacio entre flechas */} +
+ + {/* Flecha hacia atrás (abajo) */} +
+
+
+
+
+ ); +}; diff --git a/src/animations/doubleList/components/DoubleListCode.jsx b/src/animations/doubleList/components/DoubleListCode.jsx new file mode 100644 index 0000000..c197cf6 --- /dev/null +++ b/src/animations/doubleList/components/DoubleListCode.jsx @@ -0,0 +1,173 @@ +import { useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + InsertarAlInicioCode, + InsertarAlFinalCode, + InsertarEnPosicionCode, + EliminarDelInicioCode, + EliminarDelFinalCode, + EliminarEnPosicionCode, + BuscarCode, +} from '../algorithms/DoubleListAlgorithm'; +import { SvgIconDropdown, svgAlgo } from '../../../assets/svg/SvgConstans'; +import { useTranslation } from 'react-i18next'; + +export const DoubleListCode = () => { + const codeRef = useRef(null); + const [prevCurrentLine, setPrevCurrentLine] = useState(null); + const { funAction, history, stepHistory } = useSelector( + state => state.doubleList + ); + const [infoVisible, setInfoVisible] = useState(false); + const { t } = useTranslation(); + + // Efecto para resaltar línea de código actual + useEffect(() => { + if (history === -1 || !stepHistory || history >= stepHistory.length) { + if ( + prevCurrentLine !== null && + codeRef.current?.childNodes[prevCurrentLine] + ) { + codeRef.current.childNodes[prevCurrentLine].classList.remove( + 'bg-secondary' + ); + setPrevCurrentLine(null); + } + return; + } + + const currentStep = stepHistory[history]; + if (!currentStep) return; + + const currentLine = currentStep.currentIndex; + const currentElement = codeRef.current?.childNodes[currentLine]; + const prevElement = + prevCurrentLine !== null + ? codeRef.current?.childNodes[prevCurrentLine] + : null; + + if (prevElement && prevCurrentLine !== currentLine) { + prevElement.classList.remove('bg-secondary'); + } + + if (currentElement && currentLine !== prevCurrentLine) { + currentElement.classList.add('bg-secondary'); + } + + setPrevCurrentLine(currentLine); + }, [history, stepHistory, funAction, prevCurrentLine]); + + // Renderizar código según la operación + const renderCode = funAction => { + switch (funAction) { + case 'insertarAlInicio': + return ; + case 'insertarAlFinal': + return ; + case 'insertarEnPosicion': + return ; + case 'eliminarDelInicio': + return ; + case 'eliminarDelFinal': + return ; + case 'eliminarEnPosicion': + return ; + case 'buscar': + return ; + default: + return ( +
+

+ {t('selectOperation') || + 'Selecciona una operación para ver su algoritmo'} +

+
+ ); + } + }; + + return ( +
+ {/* Header del panel de código */} +
+
+ {svgAlgo} + {t('algorithm') || 'Algoritmo'} +
+ +
+ + {/* Contenido del código */} +
+
{renderCode(funAction)}
+
+ + {/* Panel de información */} + {infoVisible && ( +
+
    +
  • + + {t('definition') || 'Definición'}: + + + Lista doblemente enlazada donde cada nodo tiene punteros al + siguiente y al anterior + +
  • +
  • + Ventajas: +
      +
    • + + Navegación bidireccional + + : Se puede recorrer en ambas direcciones +
    • +
    • + Eliminación eficiente: + No necesita el nodo anterior +
    • +
    • + Inserción flexible: + Antes o después de cualquier nodo +
    • +
    +
  • +
  • + + Operaciones: + +
      +
    • + Inserción: Inicio, + final o posición +
    • +
    • + Eliminación: Inicio, + final o posición +
    • +
    • + Búsqueda: Recorrido + bidireccional +
    • +
    +
  • +
+
+ )} +
+ ); +}; diff --git a/src/animations/doubleList/components/DoubleListControls.jsx b/src/animations/doubleList/components/DoubleListControls.jsx new file mode 100644 index 0000000..31bd47c --- /dev/null +++ b/src/animations/doubleList/components/DoubleListControls.jsx @@ -0,0 +1,719 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { + decrementHistoryDoubleList, + incrementHistoryDoubleList, + actionButtonDoubleList, + restoreRepeatDoubleList, + restoreTimeIdDoubleList, + setHeadDoubleList, + setPlayingDoubleList, + setTimeIdDoubleList, + updateVisualizationDoubleList, +} from '../../../store'; +import { Alert, Button, Snackbar, TextField } from '@mui/material'; +import { useCallback, useState, useRef, useEffect } from 'react'; +import { BasicButton } from '../../../components'; +import { + svgPause, + svgBack, + svgPlay, + svgRepeat, + svgForward, +} from '../../../assets/svg/SvgConstans'; +import { registrarClick } from '../../../utils/clickRegistry'; + +export const DoubleListControls = () => { + const doubleListState = useSelector(state => state.doubleList); + const dispatch = useDispatch(); + + // Estados para los inputs + const [value, setValue] = useState(''); + const [position, setPosition] = useState(''); + const [searchValue, setSearchValue] = useState(''); + const [deletePosition, setDeletePosition] = useState(''); + + // Estados para el toast/notificaciones + const [toast, setToast] = useState(''); + const [open, setOpen] = useState(false); + + // Estado para controlar las animaciones + const [isAnimating, setIsAnimating] = useState(false); + + // Referencias para controlar la ejecución de animaciones + const animationExecutedRef = useRef(false); + const currentFunActionRef = useRef(null); + + // Función para obtener el tamaño actual de la lista + const getListSize = useCallback(() => { + if (!doubleListState.head) return 0; + let count = 0; + let current = doubleListState.head; + while (current !== null) { + count++; + current = current.getNext(); + } + return count; + }, [doubleListState.head]); + + // Función para verificar si un elemento ya existe en la lista + const elementExists = useCallback( + value => { + if (!doubleListState.head) return false; + let current = doubleListState.head; + while (current !== null) { + if (current.getValue() === value) return true; + current = current.getNext(); + } + return false; + }, + [doubleListState.head] + ); + + // Manejadores de cambio para los inputs + const handleValueChange = useCallback(event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue !== null)) { + setValue(inputValue); + } + }, []); + + const handlePositionChange = useCallback(event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue >= 0)) { + setPosition(inputValue); + } + }, []); + + const handleSearchValueChange = useCallback(event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue !== null)) { + setSearchValue(inputValue); + } + }, []); + + const handleDeletePositionChange = useCallback(event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue >= 0)) { + setDeletePosition(inputValue); + } + }, []); + + // Función para mostrar mensajes de error + const showError = useCallback(message => { + setToast(message); + setOpen(true); + }, []); + + // Función para limpiar timeouts activos + const clearActiveTimeouts = useCallback(() => { + if (doubleListState.timeIds && doubleListState.timeIds.length > 0) { + doubleListState.timeIds.forEach(timeoutId => { + clearTimeout(timeoutId); + }); + dispatch(restoreTimeIdDoubleList()); + } + }, [doubleListState.timeIds, dispatch]); + + // Función para ejecutar las animaciones paso a paso + const runAnimation = useCallback( + (stepHistory, startFromBeginning = false) => { + if (!stepHistory || stepHistory.length === 0) { + setIsAnimating(false); + return; + } + + clearActiveTimeouts(); + dispatch(setPlayingDoubleList(true)); + setIsAnimating(true); + + const timeIds = []; + + if (startFromBeginning) { + dispatch(updateVisualizationDoubleList(stepHistory[0])); + dispatch(incrementHistoryDoubleList()); + } + + const startIndex = startFromBeginning ? 1 : 0; + + for (let i = startIndex; i < stepHistory.length; i++) { + const step = stepHistory[i]; + const timeoutId = setTimeout( + () => { + dispatch(updateVisualizationDoubleList(step)); + dispatch(incrementHistoryDoubleList()); + }, + (i - startIndex) * (250 / (doubleListState.timeStep || 1)) + ); + + timeIds.push(timeoutId); + } + + const finalTimeoutId = setTimeout( + () => { + const finalState = stepHistory[stepHistory.length - 1]; + if (finalState) { + dispatch(updateVisualizationDoubleList(finalState)); + } + + dispatch(setPlayingDoubleList(false)); + dispatch(restoreTimeIdDoubleList()); + setIsAnimating(false); + animationExecutedRef.current = false; + }, + (stepHistory.length - startIndex) * + (250 / (doubleListState.timeStep || 1)) + ); + + timeIds.push(finalTimeoutId); + dispatch(setTimeIdDoubleList(timeIds)); + }, + [dispatch, doubleListState.timeStep, clearActiveTimeouts] + ); + + // Función para ejecutar animaciones de manera robusta + const executeAnimation = useCallback( + (action, value, position = null) => { + if (isAnimating) { + showError('Espera a que termine la animación actual'); + return; + } + + try { + clearActiveTimeouts(); + dispatch(setPlayingDoubleList(false)); + + const actionPayload = { action, value }; + if (position !== null) { + actionPayload.position = position; + } + + // Marcar que se va a ejecutar una nueva animación + animationExecutedRef.current = false; + currentFunActionRef.current = action; + + dispatch(setHeadDoubleList(doubleListState.head)); + dispatch(actionButtonDoubleList(actionPayload)); + } catch (error) { + console.error('Error executing animation:', error); + showError('Error al ejecutar la animación'); + setIsAnimating(false); + } + }, + [ + dispatch, + doubleListState.head, + isAnimating, + showError, + clearActiveTimeouts, + ] + ); + + // Efecto para manejar la ejecución automática de animaciones + useEffect(() => { + if ( + doubleListState.stepHistory && + doubleListState.stepHistory.length > 0 && + doubleListState.funAction && + doubleListState.funAction === currentFunActionRef.current && + !doubleListState.playing && + !animationExecutedRef.current && + doubleListState.history === -1 + ) { + animationExecutedRef.current = true; + runAnimation(doubleListState.stepHistory, true); + } + }, [ + doubleListState.stepHistory, + doubleListState.funAction, + doubleListState.playing, + doubleListState.history, + runAnimation, + ]); + + // Limpiar refs cuando cambia la acción + useEffect(() => { + if (doubleListState.funAction !== currentFunActionRef.current) { + animationExecutedRef.current = false; + currentFunActionRef.current = doubleListState.funAction; + } + }, [doubleListState.funAction]); + + // Handlers para operaciones de lista + const handleInsertarAlInicio = () => { + const numValue = value === '' ? null : parseInt(value); + + if (numValue === null) { + showError('Ingresa un valor válido'); + return; + } + + if (elementExists(numValue)) { + showError('El elemento ya existe en la lista'); + return; + } + + executeAnimation('insertarAlInicio', numValue); + registrarClick( + 'Insertar al inicio', + `Insertar ${numValue} al inicio de la lista doble` + ); + setValue(''); + }; + + const handleInsertarAlFinal = () => { + const numValue = value === '' ? null : parseInt(value); + + if (numValue === null) { + showError('Ingresa un valor válido'); + return; + } + + if (elementExists(numValue)) { + showError('El elemento ya existe en la lista'); + return; + } + + executeAnimation('insertarAlFinal', numValue); + registrarClick( + 'Insertar al final', + `Insertar ${numValue} al final de la lista doble` + ); + setValue(''); + }; + + const handleInsertarEnPosicion = () => { + const numValue = value === '' ? null : parseInt(value); + const pos = position === '' ? null : parseInt(position); + + if (numValue === null) { + showError('Ingresa un valor válido para insertar'); + return; + } + + if (elementExists(numValue)) { + showError('El elemento ya existe en la lista'); + return; + } + + if (pos === null || pos < 0) { + showError('Ingresa una posición válida (≥ 0)'); + return; + } + + const maxPosition = getListSize(); + if (pos > maxPosition) { + showError(`La posición máxima válida es ${maxPosition}`); + return; + } + + executeAnimation('insertarEnPosicion', numValue, pos); + registrarClick( + 'Insertar en posición', + `Insertar ${numValue} en posición ${pos} en lista doble` + ); + setValue(''); + setPosition(''); + }; + + const handleEliminarDelInicio = () => { + if (!doubleListState.head) { + showError('La lista está vacía'); + return; + } + + executeAnimation('eliminarDelInicio', null); + registrarClick( + 'Eliminar del inicio', + 'Eliminar primer elemento de la lista doble' + ); + }; + + const handleEliminarDelFinal = () => { + if (!doubleListState.head) { + showError('La lista está vacía'); + return; + } + + executeAnimation('eliminarDelFinal', null); + registrarClick( + 'Eliminar del final', + 'Eliminar último elemento de la lista doble' + ); + }; + + const handleEliminarEnPosicion = () => { + if (!doubleListState.head) { + showError('La lista está vacía'); + return; + } + + const pos = deletePosition === '' ? null : parseInt(deletePosition); + + if (pos === null || pos < 0) { + showError('Ingresa una posición válida (≥ 0)'); + return; + } + + const maxPosition = getListSize() - 1; + if (pos > maxPosition) { + showError(`La posición máxima válida es ${maxPosition}`); + return; + } + + executeAnimation('eliminarEnPosicion', null, pos); + registrarClick( + 'Eliminar en posición', + `Eliminar elemento en posición ${pos} de lista doble` + ); + setDeletePosition(''); + }; + + const handleBuscar = () => { + if (!doubleListState.head) { + showError('La lista está vacía'); + return; + } + + const numValue = searchValue === '' ? null : parseInt(searchValue); + + if (numValue === null) { + showError('Ingresa un valor válido para buscar'); + return; + } + + executeAnimation('buscar', numValue); + registrarClick('Buscar', `Buscar ${numValue} en la lista doble`); + setSearchValue(''); + }; + + // Controles de reproducción manual + const onPlayPause = e => { + e.preventDefault(); + if (doubleListState.playing) { + pause(); + } else { + play(); + } + }; + + const play = () => { + if (doubleListState.stepHistory && doubleListState.stepHistory.length > 0) { + const currentHistory = doubleListState.history || -1; + const remainingSteps = doubleListState.stepHistory.slice( + Math.max(0, currentHistory + 1) + ); + + if (remainingSteps.length > 0) { + animationExecutedRef.current = true; + runAnimation(remainingSteps, false); + } else { + dispatch(restoreRepeatDoubleList()); + animationExecutedRef.current = true; + runAnimation(doubleListState.stepHistory, true); + } + } + }; + + const pause = () => { + dispatch(setPlayingDoubleList(false)); + clearActiveTimeouts(); + setIsAnimating(false); + }; + + const goBackward = () => { + if (doubleListState.playing) { + pause(); + } + + const currentHistory = doubleListState.history || -1; + if (currentHistory > 0 && doubleListState.stepHistory) { + const previousStep = doubleListState.stepHistory[currentHistory - 1]; + dispatch(decrementHistoryDoubleList()); + dispatch(updateVisualizationDoubleList(previousStep)); + } + }; + + const goForward = () => { + if (doubleListState.playing) { + pause(); + } + + const currentHistory = doubleListState.history || -1; + if ( + doubleListState.stepHistory && + currentHistory < doubleListState.stepHistory.length - 1 + ) { + const nextStep = doubleListState.stepHistory[currentHistory + 1]; + dispatch(incrementHistoryDoubleList()); + dispatch(updateVisualizationDoubleList(nextStep)); + } + }; + + const repeat = () => { + if (doubleListState.stepHistory && doubleListState.stepHistory.length > 0) { + clearActiveTimeouts(); + dispatch(restoreRepeatDoubleList()); + animationExecutedRef.current = true; + runAnimation(doubleListState.stepHistory, true); + } + }; + + // Limpiar timeouts al desmontar el componente + useEffect(() => { + return () => { + clearActiveTimeouts(); + }; + }, [clearActiveTimeouts]); + + // Determinar el icono de play/pause + const playPauseIcon = doubleListState.playing ? svgPause : svgPlay; + + return ( +
+ {/* Controles de reproducción */} +
+ + {svgBack} + + + + {playPauseIcon} + + + = + doubleListState.stepHistory.length - 1 + } + title="Avanzar" + > + {svgForward} + + + + {svgRepeat} + +
+ + {/* Controles Insertar Inicio/Final */} +
+ + + +
+ + {/* Controles para Insertar en Posición */} +
+ + +
+ + {/* Controles para Eliminar en Posición */} +
+ 0 ? getListSize() - 1 : 0, + }} + /> + +
+ + {/* Control Eliminar Inicio/Final */} +
+ + +
+ + {/* Control Buscar */} +
+ + +
+ + {/* Notificaciones */} + setOpen(false)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + > + setOpen(false)} + severity="error" + sx={{ width: '100%' }} + > + {toast} + + +
+ ); +}; diff --git a/src/animations/doubleList/components/index.js b/src/animations/doubleList/components/index.js new file mode 100644 index 0000000..b9063de --- /dev/null +++ b/src/animations/doubleList/components/index.js @@ -0,0 +1,3 @@ +export * from './DoubleListChart'; +export * from './DoubleListControls'; +export * from './DoubleListCode'; diff --git a/src/animations/doubleList/index.js b/src/animations/doubleList/index.js new file mode 100644 index 0000000..44413ea --- /dev/null +++ b/src/animations/doubleList/index.js @@ -0,0 +1,2 @@ +export * from './algorithms'; +export * from './components'; diff --git a/src/animations/index.js b/src/animations/index.js index 6aeab94..53a9c6e 100644 --- a/src/animations/index.js +++ b/src/animations/index.js @@ -1,3 +1,8 @@ export * from './views'; export * from './sorting'; export * from './stack'; +export * from './queue'; +export * from './simpleList'; +export * from './doubleList'; +export * from './linearSearch'; +export * from './binarySearch'; diff --git a/src/animations/linearSearch/algorithms/index.js b/src/animations/linearSearch/algorithms/index.js new file mode 100644 index 0000000..da44bb9 --- /dev/null +++ b/src/animations/linearSearch/algorithms/index.js @@ -0,0 +1,99 @@ +export const createTrace = ( + array, + currentIndex = -1, + foundIndex = -1, + searchValue = null, + description = '', + currentLine = -1 +) => { + return { + array: [...array], + currentIndex, + foundIndex, + searchValue, + description, + currentLine, + }; +}; + +export const busquedaLineal = (array, target) => { + const trace = []; + const arr = [...array]; + + // Línea 0: function busquedaLineal(arreglo, objetivo) { + trace.push( + createTrace( + arr, + -1, + -1, + target, + `Iniciando búsqueda lineal del valor ${target}`, + 0 + ) + ); + + for (let i = 0; i < arr.length; i++) { + // Línea 1: for (let i = 0; i < arreglo.length; i++) { + trace.push( + createTrace( + arr, + i, + -1, + target, + `Iteración ${i}: revisando posición ${i}`, + 1 + ) + ); + + // Línea 2: if (arreglo[i] === objetivo) { + trace.push( + createTrace( + arr, + i, + -1, + target, + `Comparando arr[${i}] = ${arr[i]} con ${target}`, + 2 + ) + ); + + if (arr[i] === target) { + // Línea 3: return i; // Encontrado + trace.push( + createTrace( + arr, + i, + i, + target, + `¡Encontrado! El valor ${target} está en el índice ${i}`, + 3 + ) + ); + return trace; + } + } + + // Línea 6: return -1; // No encontrado + trace.push( + createTrace( + arr, + -1, + -2, + target, + `El valor ${target} no se encontró en el arreglo`, + 6 + ) + ); + return trace; +}; + +export const BusquedaLinealCode = () => { + return `public static int busquedaLineal(int[] arreglo, int objetivo) { + for (int i = 0; i < arreglo.length; i++) { + if (arreglo[i] == objetivo) { + return i; + } + } + return -1; +}`; +}; diff --git a/src/animations/linearSearch/components/LinearSearchChart.jsx b/src/animations/linearSearch/components/LinearSearchChart.jsx new file mode 100644 index 0000000..14a13cf --- /dev/null +++ b/src/animations/linearSearch/components/LinearSearchChart.jsx @@ -0,0 +1,155 @@ +import { useSelector } from 'react-redux'; +import { useState } from 'react'; + +export const LinearSearchChart = () => { + const { array, currentIndex, foundIndex, description } = useSelector( + state => state.linearSearch + ); + + // Calcular tamaño dinámico basado en cantidad de elementos + const getNodeSize = () => { + const count = array.length; + if (count <= 5) return { nodeWidth: 64, fontSize: 18, gap: 8 }; + if (count <= 8) return { nodeWidth: 52, fontSize: 14, gap: 6 }; + if (count <= 12) return { nodeWidth: 44, fontSize: 12, gap: 4 }; + if (count <= 16) return { nodeWidth: 36, fontSize: 11, gap: 3 }; + return { nodeWidth: 30, fontSize: 10, gap: 2 }; + }; + + const nodeStyles = getNodeSize(); + + if (array.length === 0) { + return ( +
+
+
+ 🔍 Arreglo vacío +
+

+ Ingresa valores separados por comas +

+
+
+ ); + } + + return ( +
+ {/* Descripción del paso actual */} +
+
+

+ {description} +

+
+
+ + {/* Visualización del arreglo */} +
+
+ {array.map((value, index) => ( +
+ {/* Indicador de posición actual */} +
+ {currentIndex === index && ( + + ↓ + + )} +
+ + {/* Nodo */} + + + {/* Índice */} + + [{index}] + +
+ ))} +
+
+ + {/* Leyenda */} +
+
+
+ Actual +
+
+
+ Encontrado +
+
+
+ No encontrado +
+
+
+ ); +}; + +const NodeComponent = ({ + value, + index, + currentIndex, + foundIndex, + nodeStyles, +}) => { + const getBackgroundColor = () => { + if (foundIndex === index) { + return 'bg-gradient-to-br from-green-400 to-green-600 border-2 border-green-300 shadow-2xl shadow-green-500/70 scale-110'; + } + if (foundIndex === -2) { + return 'bg-gradient-to-br from-red-400/70 to-red-600/70 border-2 border-red-400 shadow-lg shadow-red-500/50'; + } + if (currentIndex === index) { + return 'bg-gradient-to-br from-blue-400 to-blue-600 border-2 border-blue-300 shadow-2xl shadow-blue-500/70 scale-105'; + } + return 'bg-gradient-to-br from-indigo-500 to-indigo-700 border-2 border-indigo-400 shadow-lg shadow-indigo-500/50'; + }; + + return ( +
+ + {value} + +
+ ); +}; + +export default LinearSearchChart; diff --git a/src/animations/linearSearch/components/LinearSearchCode.jsx b/src/animations/linearSearch/components/LinearSearchCode.jsx new file mode 100644 index 0000000..4c4da2c --- /dev/null +++ b/src/animations/linearSearch/components/LinearSearchCode.jsx @@ -0,0 +1,151 @@ +import { useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { BusquedaLinealCode } from '../algorithms'; +import { SvgIconDropdown, svgAlgo } from '../../../assets/svg/SvgConstans'; +import { useTranslation } from 'react-i18next'; + +// Componente de código para renderizar +const BusquedaLinealCodeComponent = ({ codeRef }) => { + const code = BusquedaLinealCode(); + const lines = code.split('\n'); + + return ( +
+      {lines.map((line, index) => (
+        
+ {line} +
+ ))} +
+ ); +}; + +export const LinearSearchCode = () => { + const codeRef = useRef(null); + const [prevCurrentLine, setPrevCurrentLine] = useState(null); + const { history, stepHistory } = useSelector(state => state.linearSearch); + const [infoVisible, setInfoVisible] = useState(false); + const { t } = useTranslation(); + + useEffect(() => { + // Si no hay historial o estamos en -1, limpiar + if (history === -1 || !stepHistory || stepHistory.length === 0) { + if ( + prevCurrentLine !== null && + codeRef.current?.childNodes[prevCurrentLine] + ) { + codeRef.current.childNodes[prevCurrentLine].classList.remove( + 'bg-secondary' + ); + } + setPrevCurrentLine(null); + return; + } + + // Obtener el paso actual + const currentStep = stepHistory[history]; + if (!currentStep || currentStep.currentLine === undefined) return; + + const currentLine = currentStep.currentLine; + const currentElement = codeRef.current?.childNodes[currentLine]; + const prevElement = + prevCurrentLine !== null + ? codeRef.current?.childNodes[prevCurrentLine] + : null; + + // Quitar highlight de línea anterior + if (prevElement && prevCurrentLine !== currentLine) { + prevElement.classList.remove('bg-secondary'); + } + + // Agregar highlight a línea actual + if (currentElement && currentLine !== prevCurrentLine) { + currentElement.classList.add('bg-secondary'); + } + + setPrevCurrentLine(currentLine); + }, [history, stepHistory, prevCurrentLine]); + + return ( +
+ {/* Header del panel de código */} +
+
+ {svgAlgo} + {t('algorithm') || 'Algoritmo'} +
+ +
+ + {/* Contenido del código */} +
+
+ +
+
+ + {/* Panel de información */} + {infoVisible && ( +
+
    +
  • + Definición: + + Algoritmo de búsqueda secuencial que recorre el array elemento + por elemento + +
  • +
  • + Complejidad: +
      +
    • + Tiempo: O(n) - Lineal +
    • +
    • + Espacio: O(1) - + Constante +
    • +
    +
  • +
  • + Ventajas: +
      +
    • Funciona en arrays desordenados
    • +
    • Simple de implementar
    • +
    +
  • +
  • + Desventajas: +
      +
    • Lento para arrays grandes
    • +
    +
  • +
+
+ )} +
+ ); +}; + +export default LinearSearchCode; diff --git a/src/animations/linearSearch/components/LinearSearchControls.jsx b/src/animations/linearSearch/components/LinearSearchControls.jsx new file mode 100644 index 0000000..ea23144 --- /dev/null +++ b/src/animations/linearSearch/components/LinearSearchControls.jsx @@ -0,0 +1,458 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { + setArrayLinearSearch, + actionButtonLinearSearch, + updateVisualizationLinearSearch, + incrementHistoryLinearSearch, + decrementHistoryLinearSearch, + setPlayingLinearSearch, + setTimeIdLinearSearch, + restoreTimeIdLinearSearch, + restoreRepeatLinearSearch, + resetLinearSearch, +} from '../../../store'; +import { busquedaLineal } from '../algorithms'; +import { Alert, Button, Snackbar, TextField } from '@mui/material'; +import { useCallback, useState, useRef, useEffect } from 'react'; +import { BasicButton } from '../../../components'; +import { + svgPause, + svgBack, + svgPlay, + svgRepeat, + svgForward, +} from '../../../assets/svg/SvgConstans'; +import { registrarClick } from '../../../utils/clickRegistry'; + +export const LinearSearchControls = () => { + const linearSearchState = useSelector(state => state.linearSearch); + const dispatch = useDispatch(); + + const [inputArray, setInputArray] = useState('5, 12, 3, 8, 15, 7, 20, 1'); + const [searchValue, setSearchValue] = useState(''); + const [toast, setToast] = useState(''); + const [open, setOpen] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); + + const animationExecutedRef = useRef(false); + const currentActionRef = useRef(null); + + const handleArrayChange = useCallback(event => { + setInputArray(event.target.value); + }, []); + + const handleSearchValueChange = useCallback(event => { + const inputValue = event.target.value; + if (inputValue === '' || !isNaN(inputValue)) { + setSearchValue(inputValue); + } + }, []); + + const showError = useCallback(message => { + setToast(message); + setOpen(true); + }, []); + + const clearActiveTimeouts = useCallback(() => { + if (linearSearchState.timeIds && linearSearchState.timeIds.length > 0) { + linearSearchState.timeIds.forEach(timeoutId => clearTimeout(timeoutId)); + dispatch(restoreTimeIdLinearSearch()); + } + }, [linearSearchState.timeIds, dispatch]); + + const runAnimation = useCallback( + (stepHistory, startFromBeginning = false) => { + if (!stepHistory || stepHistory.length === 0) { + setIsAnimating(false); + return; + } + + clearActiveTimeouts(); + dispatch(setPlayingLinearSearch(true)); + setIsAnimating(true); + + const timeIds = []; + if (startFromBeginning) { + dispatch(updateVisualizationLinearSearch(stepHistory[0])); + dispatch(incrementHistoryLinearSearch()); + } + + const startIndex = startFromBeginning ? 1 : 0; + + for (let i = startIndex; i < stepHistory.length; i++) { + const step = stepHistory[i]; + const timeoutId = setTimeout( + () => { + dispatch(updateVisualizationLinearSearch(step)); + dispatch(incrementHistoryLinearSearch()); + }, + (i - startIndex) * 500 + ); + timeIds.push(timeoutId); + } + + const finalTimeoutId = setTimeout( + () => { + dispatch(setPlayingLinearSearch(false)); + dispatch(restoreTimeIdLinearSearch()); + setIsAnimating(false); + animationExecutedRef.current = false; + }, + (stepHistory.length - startIndex) * 500 + ); + + timeIds.push(finalTimeoutId); + dispatch(setTimeIdLinearSearch(timeIds)); + }, + [dispatch, clearActiveTimeouts] + ); + + const executeSearch = useCallback( + value => { + if (isAnimating) { + showError('Espera a que termine la animación actual'); + return; + } + + if (linearSearchState.array.length === 0) { + showError('Primero carga un arreglo'); + return; + } + + try { + clearActiveTimeouts(); + dispatch(setPlayingLinearSearch(false)); + + const arr = [...linearSearchState.array]; + const trace = busquedaLineal(arr, value); + + animationExecutedRef.current = false; + currentActionRef.current = 'busquedaLineal'; + + dispatch(actionButtonLinearSearch({ trace, value })); + } catch (error) { + console.error('Error executing search:', error); + showError('Error al ejecutar la búsqueda'); + setIsAnimating(false); + } + }, + [ + dispatch, + linearSearchState.array, + isAnimating, + showError, + clearActiveTimeouts, + ] + ); + + useEffect(() => { + if ( + linearSearchState.stepHistory && + linearSearchState.stepHistory.length > 0 && + linearSearchState.funAction === currentActionRef.current && + !linearSearchState.isPlaying && + !animationExecutedRef.current && + linearSearchState.history === -1 + ) { + animationExecutedRef.current = true; + runAnimation(linearSearchState.stepHistory, true); + } + }, [ + linearSearchState.stepHistory, + linearSearchState.funAction, + linearSearchState.isPlaying, + linearSearchState.history, + runAnimation, + ]); + + const handleLoadArray = () => { + const values = inputArray + .split(',') + .map(v => parseInt(v.trim(), 10)) + .filter(v => !isNaN(v)); + + if (values.length === 0) { + showError('Ingresa valores válidos separados por comas'); + return; + } + + if (values.length > 20) { + showError('Máximo 20 elementos permitidos'); + return; + } + + dispatch(setArrayLinearSearch(values)); + dispatch(resetLinearSearch()); + clearActiveTimeouts(); + registrarClick('Cargar arreglo', `Cargar arreglo: [${values.join(', ')}]`); + setToast('Arreglo cargado correctamente'); + setOpen(true); + }; + + const handleRandomArray = () => { + const length = Math.floor(Math.random() * 8) + 5; + const randomArray = Array.from( + { length }, + () => Math.floor(Math.random() * 50) + 1 + ); + setInputArray(randomArray.join(', ')); + dispatch(setArrayLinearSearch(randomArray)); + dispatch(resetLinearSearch()); + clearActiveTimeouts(); + registrarClick( + 'Arreglo aleatorio', + `Generar arreglo aleatorio: [${randomArray.join(', ')}]` + ); + setToast('Arreglo aleatorio generado'); + setOpen(true); + }; + + const handleBuscar = () => { + const numValue = searchValue === '' ? null : parseInt(searchValue); + if (numValue === null) { + showError('Ingresa un valor válido para buscar'); + return; + } + executeSearch(numValue); + registrarClick('Buscar', `Buscar ${numValue} en el arreglo`); + setSearchValue(''); + }; + + const onPlayPause = e => { + e.preventDefault(); + if (linearSearchState.isPlaying) { + pause(); + } else { + play(); + } + }; + + const play = () => { + if ( + linearSearchState.stepHistory && + linearSearchState.stepHistory.length > 0 + ) { + const currentHistory = linearSearchState.history || -1; + const remainingSteps = linearSearchState.stepHistory.slice( + Math.max(0, currentHistory + 1) + ); + + if (remainingSteps.length > 0) { + animationExecutedRef.current = true; + runAnimation(remainingSteps, false); + } else { + dispatch(restoreRepeatLinearSearch()); + animationExecutedRef.current = true; + runAnimation(linearSearchState.stepHistory, true); + } + } + }; + + const pause = () => { + dispatch(setPlayingLinearSearch(false)); + clearActiveTimeouts(); + setIsAnimating(false); + }; + + const goBackward = () => { + if (linearSearchState.isPlaying) pause(); + const currentHistory = linearSearchState.history || -1; + if (currentHistory > 0 && linearSearchState.stepHistory) { + const previousStep = linearSearchState.stepHistory[currentHistory - 1]; + dispatch(decrementHistoryLinearSearch()); + dispatch(updateVisualizationLinearSearch(previousStep)); + } + }; + + const goForward = () => { + if (linearSearchState.isPlaying) pause(); + const currentHistory = linearSearchState.history || -1; + if ( + linearSearchState.stepHistory && + currentHistory < linearSearchState.stepHistory.length - 1 + ) { + const nextStep = linearSearchState.stepHistory[currentHistory + 1]; + dispatch(incrementHistoryLinearSearch()); + dispatch(updateVisualizationLinearSearch(nextStep)); + } + }; + + const repeat = () => { + if ( + linearSearchState.stepHistory && + linearSearchState.stepHistory.length > 0 + ) { + clearActiveTimeouts(); + dispatch(restoreRepeatLinearSearch()); + animationExecutedRef.current = true; + runAnimation(linearSearchState.stepHistory, true); + } + }; + + useEffect(() => { + return () => clearActiveTimeouts(); + }, [clearActiveTimeouts]); + + const playPauseIcon = linearSearchState.isPlaying ? svgPause : svgPlay; + + return ( +
+ {/* Controles de reproducción */} +
+ + {svgBack} + + + {playPauseIcon} + + = + linearSearchState.stepHistory.length - 1 + } + title="Avanzar" + > + {svgForward} + + + {svgRepeat} + +
+ + {/* Input del arreglo con botones Cargar y Aleatorio en la misma línea */} +
+ + + +
+ + {/* Input de búsqueda */} +
+ + +
+ + {/* Indicador de progreso elegante */} + {linearSearchState.stepHistory && + linearSearchState.stepHistory.length > 0 && ( +
+
+ + Paso {Math.max(0, linearSearchState.history + 1)} de{' '} + {linearSearchState.stepHistory.length} + + + {linearSearchState.isPlaying + ? '▶️ En progreso...' + : '⏸️ Pausado'} + +
+
+
+
+
+ )} + + setOpen(false)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + setOpen(false)}> + {toast} + + +
+ ); +}; + +export default LinearSearchControls; diff --git a/src/animations/linearSearch/components/index.js b/src/animations/linearSearch/components/index.js new file mode 100644 index 0000000..e7a39a7 --- /dev/null +++ b/src/animations/linearSearch/components/index.js @@ -0,0 +1,3 @@ +export * from './LinearSearchChart'; +export * from './LinearSearchControls'; +export * from './LinearSearchCode'; diff --git a/src/animations/linearSearch/index.js b/src/animations/linearSearch/index.js new file mode 100644 index 0000000..fe7a2b4 --- /dev/null +++ b/src/animations/linearSearch/index.js @@ -0,0 +1,4 @@ +export { LinearSearchChart } from './components/LinearSearchChart'; +export { LinearSearchControls } from './components/LinearSearchControls'; +export { LinearSearchCode } from './components/LinearSearchCode'; +export { busquedaLineal, BusquedaLinealCode } from './algorithms'; diff --git a/src/animations/queue/algorithms/Nodo.js b/src/animations/queue/algorithms/Nodo.js new file mode 100644 index 0000000..0c54a48 --- /dev/null +++ b/src/animations/queue/algorithms/Nodo.js @@ -0,0 +1,32 @@ +class Nodo { + constructor(value) { + this.value = value; + this.next = null; + } + + getValue() { + return this.value; + } + + setValue(value) { + this.value = value; + } + + getNext() { + return this.next; + } + + setNext(nextNode) { + this.next = nextNode; + } + + hasNext() { + return this.next !== null; + } + + toString() { + return `Nodo(${this.value})`; + } +} + +export default Nodo; diff --git a/src/animations/queue/algorithms/QueueAlgorithm.jsx b/src/animations/queue/algorithms/QueueAlgorithm.jsx new file mode 100644 index 0000000..7ad10ba --- /dev/null +++ b/src/animations/queue/algorithms/QueueAlgorithm.jsx @@ -0,0 +1,441 @@ +import { newTraceQueue, moveInHistoryRecordQueue } from './helpers'; +import Nodo from './Nodo'; + +// Copiar la cola completa +const copiarCola = head => { + if (head === null) return null; + + let nuevaCabeza = new Nodo(head.getValue()); + let actualNuevo = nuevaCabeza; + let actualViejo = head.getNext(); + + while (actualViejo !== null) { + actualNuevo.setNext(new Nodo(actualViejo.getValue())); + actualNuevo = actualNuevo.getNext(); + actualViejo = actualViejo.getNext(); + } + + return nuevaCabeza; +}; + +const crearRegistro = ( + head, + primerConjunto = [], + segundoConjunto = [], + indiceActual = 0, + nodoAEliminar = null +) => { + const highlightedNode = + primerConjunto && primerConjunto.length > 0 ? primerConjunto[0] : null; + const pointer = + segundoConjunto && segundoConjunto.length > 0 ? segundoConjunto[0] : null; + + return { + head: copiarCola(head), + firstSet: [...primerConjunto], + secondSet: [...segundoConjunto], + isHead: head ? head.getValue() : -1, + currentIndex: indiceActual, + highlightedNode, + pointer, + deletingNode: nodoAEliminar, + }; +}; + +// Enqueue: añadir al final (tail) +export const Enqueue = (head, valor) => { + const historialPasos = []; + + // Línea 0: creación del nuevo nodo + historialPasos.push(crearRegistro(head, [], [], 0)); + + const nuevoNodo = new Nodo(valor); + historialPasos.push(crearRegistro(head, [valor], [], 0)); + + if (head === null) { + // Línea 1: head === null + head = nuevoNodo; + historialPasos.push(crearRegistro(head, [], [], 1)); + } else { + // Línea 2: else { + let actual = head; + historialPasos.push(crearRegistro(head, [], [actual.getValue()], 2)); + + // Línea 5: recorrido de la cola + while (actual.getNext() !== null) { + actual = actual.getNext(); + historialPasos.push(crearRegistro(head, [valor], [actual.getValue()], 5)); + } + + // Línea 6: enlazar al final + actual.setNext(nuevoNodo); + historialPasos.push(crearRegistro(head, [], [nuevoNodo.getValue()], 6)); + } + + // Línea 7: cierre + historialPasos.push(crearRegistro(head, [], [], 7)); + + return { historialPasos, nuevaCabeza: head }; +}; + +// Dequeue: eliminar del frente (head) +export const Dequeue = cabeza => { + const historialPasos = []; + + // Línea 0: if (head === null) + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + + if (cabeza === null) { + // Se mantiene en la línea 0 (retorno por vacío) + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Línea 1: valor = head.getValue() - resaltar el nodo en rojo + const valorEliminado = cabeza.getValue(); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 1)); + + // Mantener el resaltado en rojo (sin desaparecer aún) - varios pasos + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 1)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 1)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 1)); + + // AHORA SÍ: Marcar el nodo para animación de desaparición (sin el resaltado rojo pulsante) + historialPasos.push(crearRegistro(cabeza, [], [], 1, valorEliminado)); + + // Mantener la animación de desaparición + historialPasos.push(crearRegistro(cabeza, [], [], 1, valorEliminado)); + historialPasos.push(crearRegistro(cabeza, [], [], 1, valorEliminado)); + + // Línea 2: head = head.getNext() - ahora sí eliminar + cabeza = cabeza.getNext(); + historialPasos.push(crearRegistro(cabeza, [], [], 2)); + + // Última línea (fin): mantenerse en la 2 para mostrar estado final + historialPasos.push(crearRegistro(cabeza, [], [], 2)); + + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; +}; + +// EliminarFinal: eliminar el último nodo (tail) +export const EliminarFinal = cabeza => { + const historialPasos = []; + + // Línea 0: if (head === null) + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + + if (cabeza === null) { + // Cola vacía + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Línea 1: if (head.getNext() === null) - solo un elemento + if (cabeza.getNext() === null) { + const valorEliminado = cabeza.getValue(); + // Resaltar en rojo el único nodo + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 1)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 1)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 1)); + // Marcar para desaparecer + historialPasos.push(crearRegistro(cabeza, [], [], 1, valorEliminado)); + historialPasos.push(crearRegistro(cabeza, [], [], 1, valorEliminado)); + historialPasos.push(crearRegistro(cabeza, [], [], 1, valorEliminado)); + // Eliminar + cabeza = null; + historialPasos.push(crearRegistro(cabeza, [], [], 1)); + historialPasos.push(crearRegistro(cabeza, [], [], 4)); + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; + } + + // Línea 2: actual = head + let nodoActual = cabeza; + historialPasos.push(crearRegistro(cabeza, [], [nodoActual.getValue()], 2)); + + // Línea 3: recorrer hasta el penúltimo nodo + while (nodoActual.getNext() && nodoActual.getNext().getNext() !== null) { + nodoActual = nodoActual.getNext(); + historialPasos.push(crearRegistro(cabeza, [], [nodoActual.getValue()], 3)); + } + + // Encontrar el último nodo y resaltarlo en rojo + const valorEliminado = nodoActual.getNext().getValue(); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 3)); + + // Mantener el resaltado en rojo (sin desaparecer aún) - varios pasos + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 3)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 3)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 3)); + + // AHORA SÍ: Marcar el nodo para animación de desaparición + historialPasos.push(crearRegistro(cabeza, [], [], 3, valorEliminado)); + + // Mantener la animación de desaparición + historialPasos.push(crearRegistro(cabeza, [], [], 3, valorEliminado)); + historialPasos.push(crearRegistro(cabeza, [], [], 3, valorEliminado)); + + // Línea 4: actual.setNext(null) - eliminar el último nodo + nodoActual.setNext(null); + historialPasos.push(crearRegistro(cabeza, [], [], 4)); + + // Última línea (fin) + historialPasos.push(crearRegistro(cabeza, [], [], 4)); + + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; +}; + +// Insertar: insertar en posición (0 = frente) +export const Insertar = (cabeza, valor, posicion) => { + const historialPasos = []; + + // Línea 0: creación del nodo + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + + const nuevoNodo = new Nodo(valor); + historialPasos.push(crearRegistro(cabeza, [valor], [], 0)); + + if (cabeza === null) { + if (posicion === 0) { + // Línea 1: head === null && posicion === 0 + cabeza = nuevoNodo; + historialPasos.push(crearRegistro(cabeza, [], [], 1)); + historialPasos.push(crearRegistro(cabeza, [], [], 9)); + return { historialPasos, nuevaCabeza: cabeza }; + } else { + // Línea 1: condición fallida -> return error + historialPasos.push(crearRegistro(cabeza, [], [], 1)); + return { historialPasos, nuevaCabeza: cabeza }; + } + } + + // calcular tamaño + let tamano = 0; + let temp = cabeza; + while (temp !== null) { + tamano++; + temp = temp.getNext(); + } + + if (posicion >= tamano) { + // insertar al final (enqueue) + let actual = cabeza; + while (actual.getNext() !== null) actual = actual.getNext(); + actual.setNext(nuevoNodo); + historialPasos.push(crearRegistro(cabeza, [], [nuevoNodo.getValue()], 7)); + historialPasos.push(crearRegistro(cabeza, [], [], 9)); + return { historialPasos, nuevaCabeza: cabeza }; + } + + if (posicion === 0) { + // insertar al frente (dequeue-front insertion) + nuevoNodo.setNext(cabeza); + cabeza = nuevoNodo; + historialPasos.push(crearRegistro(cabeza, [], [], 2)); + historialPasos.push(crearRegistro(cabeza, [], [], 9)); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // insertar en posición intermedia + let indice = 0; + let nodoActual = cabeza; + + // Línea 4: actual = head + historialPasos.push(crearRegistro(cabeza, [], [nodoActual.getValue()], 4)); + + // Línea 5/6: for y avance + while (indice < posicion - 1 && nodoActual.getNext() !== null) { + nodoActual = nodoActual.getNext(); + indice++; + historialPasos.push(crearRegistro(cabeza, [], [nodoActual.getValue()], 6)); + } + + // Línea 7: nuevoNodo.setNext(actual.getNext()) + nuevoNodo.setNext(nodoActual.getNext()); + historialPasos.push(crearRegistro(cabeza, [], [nodoActual.getValue()], 7)); + + // Línea 8: actual.setNext(nuevoNodo) + nodoActual.setNext(nuevoNodo); + historialPasos.push(crearRegistro(cabeza, [], [nuevoNodo.getValue()], 8)); + + // Línea 9: cierre + historialPasos.push(crearRegistro(cabeza, [], [], 9)); + + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Extraer: eliminar en posición específica (índices alineados con ExtraerCode) +export const Extraer = (cabeza, posicion) => { + const historialPasos = []; + + // Línea 0: chequeo de cola vacía + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + + if (cabeza === null) { + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + return { historialPasos, nuevaCabeza: cabeza }; + } + + let tamano = 0; + let temp = cabeza; + while (temp !== null) { + tamano++; + temp = temp.getNext(); + } + + // Línea 1: posición fuera de rango + if (posicion >= tamano) { + historialPasos.push(crearRegistro(cabeza, [], [], 1)); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Eliminar al frente (línea 2) + if (posicion === 0) { + const valor = cabeza.getValue(); + // Resaltar en rojo el nodo a eliminar + historialPasos.push(crearRegistro(cabeza, [], [valor], 2)); + // Mantener el resaltado en rojo (sin desaparecer aún) - varios pasos + historialPasos.push(crearRegistro(cabeza, [], [valor], 2)); + historialPasos.push(crearRegistro(cabeza, [], [valor], 2)); + historialPasos.push(crearRegistro(cabeza, [], [valor], 2)); + // AHORA SÍ: Marcar para animación de desaparición (sin el resaltado rojo pulsante) + historialPasos.push(crearRegistro(cabeza, [], [], 2, valor)); + // Mantener la animación de desaparición + historialPasos.push(crearRegistro(cabeza, [], [], 2, valor)); + historialPasos.push(crearRegistro(cabeza, [], [], 2, valor)); + // Ahora sí eliminar el nodo + cabeza = cabeza.getNext(); + historialPasos.push(crearRegistro(cabeza, [], [], 2)); + historialPasos.push(crearRegistro(cabeza, [], [], 9)); + return { historialPasos, nuevaCabeza: cabeza, valorEliminado: valor }; + } + + // Eliminar intermedio + let indice = 0; + let nodoActual = cabeza; + + // Línea 4: inicializar "actual" - resaltar en rojo + historialPasos.push(crearRegistro(cabeza, [], [nodoActual.getValue()], 4)); + + // Línea 5: recorrer hasta el nodo previo - resaltar cada nodo en rojo + while (indice < posicion - 1) { + nodoActual = nodoActual.getNext(); + indice++; + historialPasos.push(crearRegistro(cabeza, [], [nodoActual.getValue()], 5)); + } + + // Línea 7: encontrar el nodo a eliminar y resaltarlo en rojo + const valorEliminado = nodoActual.getNext().getValue(); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 7)); + + // Mantener el resaltado en rojo (sin desaparecer aún) - varios pasos + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 7)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 7)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 7)); + + // AHORA SÍ: Marcar el nodo para animación de desaparición (el nodo aún existe en la cola) + // Ya NO lo ponemos en secondSet para que no tenga el efecto de pulso rojo + historialPasos.push(crearRegistro(cabeza, [], [], 7, valorEliminado)); + + // Mantener la animación de desaparición + historialPasos.push(crearRegistro(cabeza, [], [], 7, valorEliminado)); + historialPasos.push(crearRegistro(cabeza, [], [], 7, valorEliminado)); + + // Línea 8: reasignar puntero para eliminar - ahora sí eliminamos el nodo + nodoActual.setNext(nodoActual.getNext().getNext()); + historialPasos.push(crearRegistro(cabeza, [], [], 8)); + + // Línea 9: fin + historialPasos.push(crearRegistro(cabeza, [], [], 9)); + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; +}; + +export const EnqueueCode = ({ codeRef }) => { + return ( + +
{`public void enqueue(int valor) {`}
+
{`    Node nuevoNodo = new Node(valor);`}
+
{`    if (head == null) {`}
+
{`        head = nuevoNodo;`}
+
{`        return;`}
+
{`    }`}
+
{`    Node actual = head;`}
+
{`    while (actual.getNext() != null) {`}
+
{`        actual = actual.getNext();`}
+
{`    }`}
+
{`    actual.setNext(nuevoNodo);`}
+
{`}`}
+
+ ); +}; + +export const DequeueCode = ({ codeRef }) => { + return ( + +
{`public int dequeue() {`}
+
{`    if (head == null) return -1;`}
+
{`    int valor = head.getValue();`}
+
{`    head = head.getNext();`}
+
{`    return valor;`}
+
{`}`}
+
+ ); +}; + +export const EliminarFinalCode = ({ codeRef }) => { + return ( + +
{`public void eliminarFinal() {`}
+
{`    if (head == null) return;`}
+
{`    if (head.getNext() == null) {`}
+
{`        head = null;`}
+
{`        return;`}
+
{`    }`}
+
{`    Node actual = head;`}
+
{`    while (actual.getNext().getNext() != null) {`}
+
{`        actual = actual.getNext();`}
+
{`    }`}
+
{`    actual.setNext(null);`}
+
{`}`}
+
+ ); +}; + +export const InsertarCode = ({ codeRef }) => { + return ( + +
{`public void insertar(int valor, int posicion) {`}
+
{`    Node nuevoNodo = new Node(valor);`}
+
{`    if (head == null && posicion == 0) {`}
+
{`        head = nuevoNodo;`}
+
{`        return;`}
+
{`    }`}
+
{`    if (posicion == 0) {`}
+
{`        nuevoNodo.setNext(head);`}
+
{`        head = nuevoNodo;`}
+
{`    }`}
+
{`}`}
+
+ ); +}; + +export const ExtraerCode = ({ codeRef }) => { + return ( + +
{`public int extraer(int posicion) {`}
+
{`    if (head == null) return -1;`}
+
{`    if (posicion == 0) {`}
+
{`        int valor = head.getValue();`}
+
{`        head = head.getNext();`}
+
{`        return valor;`}
+
{`    }`}
+
{`    Node actual = head;`}
+
{`    for (int i = 0; i < posicion - 1; i++) {`}
+
{`        actual = actual.getNext();`}
+
{`    }`}
+
{`    int valor = actual.getNext().getValue();`}
+
{`    actual.setNext(actual.getNext().getNext());`}
+
{`    return valor;`}
+
{`}`}
+
+ ); +}; diff --git a/src/animations/queue/algorithms/helpers.jsx b/src/animations/queue/algorithms/helpers.jsx new file mode 100644 index 0000000..c663f1c --- /dev/null +++ b/src/animations/queue/algorithms/helpers.jsx @@ -0,0 +1,33 @@ +export const newTraceQueue = head => { + return [ + { + head: head, + firstSet: [], + secondSet: [], + isHead: head ? head.getValue() : -1, + currentIndex: null, + highlightedNode: null, + pointer: null, + }, + ]; +}; + +export const moveInHistoryRecordQueue = ( + stepHistory, + head, + firstSet = [], + secondSet = [], + currentIndex = null, + highlightedNode = null, + pointer = null +) => { + stepHistory.push({ + head: head, + firstSet: [...firstSet], + secondSet: [...secondSet], + isHead: head ? head.getValue() : -1, + currentIndex: currentIndex, + highlightedNode: highlightedNode, + pointer: pointer, + }); +}; diff --git a/src/animations/queue/algorithms/index.js b/src/animations/queue/algorithms/index.js new file mode 100644 index 0000000..171c068 --- /dev/null +++ b/src/animations/queue/algorithms/index.js @@ -0,0 +1,14 @@ +export * from './helpers'; +export * from './Nodo'; +import * as QA from './QueueAlgorithm'; + +export const Enqueue = QA.Enqueue; +export const Dequeue = QA.Dequeue; +export const EliminarFinal = QA.EliminarFinal; +export const InsertarQueue = QA.Insertar; +export const ExtraerQueue = QA.Extraer; +export const EnqueueCode = QA.EnqueueCode; +export const DequeueCode = QA.DequeueCode; +export const EliminarFinalCode = QA.EliminarFinalCode; +export const InsertarCode = QA.InsertarCode; +export const ExtraerCode = QA.ExtraerCode; diff --git a/src/animations/queue/components/QueueChart.jsx b/src/animations/queue/components/QueueChart.jsx new file mode 100644 index 0000000..22e0cf3 --- /dev/null +++ b/src/animations/queue/components/QueueChart.jsx @@ -0,0 +1,164 @@ +import { useSelector } from 'react-redux'; +import { useState, useEffect } from 'react'; + +// Convertir cola a array (head -> tail) +const queueToArray = (node, arr = []) => { + if (!node) return arr; + arr.push(node); + return queueToArray(node.next, arr); +}; + +export const QueueChart = () => { + const { + head, + isHead, + firstSet, + secondSet, + highlightedNode, + pointer, + deletingNode, + } = useSelector(state => state.queue); + + const nodes = queueToArray(head); + const nodeCount = nodes.length; + + // Calcular tamaño dinámico basado en ancho disponible + const containerWidth = 520; // valor aproximado + const padding = 32; + const availableWidth = containerWidth - padding; + + const getNodeStyles = () => { + // Mantener un tamaño de fuente legible y fijo para evitar que se haga demasiado pequeño + const fontSize = 14; + + if (nodeCount === 0) return { width: 80, gap: 12, fontSize, height: 40 }; + + // Caso especial: un solo elemento debe verse más grande y centrado + if (nodeCount === 1) return { width: 140, gap: 12, fontSize, height: 48 }; + + const maxNodeWidth = 120; + const minNodeWidth = 60; + const maxGap = 16; + const minGap = 6; + + const maxSpaceNeeded = nodeCount * maxNodeWidth + (nodeCount - 1) * maxGap; + if (maxSpaceNeeded <= availableWidth) { + return { width: maxNodeWidth, gap: maxGap, fontSize, height: 40 }; + } + + const ratio = availableWidth / maxSpaceNeeded; + const calculatedWidth = Math.max( + minNodeWidth, + Math.floor(maxNodeWidth * ratio) + ); + const calculatedGap = Math.max(minGap, Math.floor(maxGap * ratio)); + + // No reducir la fuente más abajo de fontSize fijo + return { width: calculatedWidth, gap: calculatedGap, fontSize, height: 40 }; + }; + + const { width, gap, fontSize, height } = getNodeStyles(); + + const getBackgroundColor = value => { + if (firstSet.includes(value)) return 'bg-success'; + if (secondSet.includes(value)) return 'bg-danger'; + return 'bg-secondary'; + }; + + const ArrowComponent = ({ arrowWidth = 32 }) => { + return ( +
+
+
+
+
+
+ ); + }; + + // Mostrar mensaje cuando la cola está vacía + if (!head || nodeCount === 0) { + return ( +
+
+ 🎯 Cola vacía +
+
+ ); + } + + /* NodeComponent (simplificado temporalmente) */ + + return ( +
+
+ {nodes.map((node, index) => { + const isSingle = nodeCount === 1; + const label = isSingle + ? 'front / rear' + : index === 0 + ? 'front' + : index === nodes.length - 1 + ? 'rear' + : null; + const isDangerNode = secondSet.includes(node.value); + const isDeleting = deletingNode === node.value; + const isNewNode = firstSet.includes(node.value); + const bgClass = firstSet.includes(node.value) + ? 'bg-success' + : isDangerNode + ? 'bg-danger' + : 'bg-secondary'; + + // Determinar qué animación aplicar + let animationClass = ''; + let animationDelay = '0ms'; + + if (isDeleting) { + animationClass = 'animate-fadeOutScale'; + } else if (isNewNode) { + animationClass = 'animate-fadeInScale'; + animationDelay = `${index * 150}ms`; + } + + return ( +
+
+
{node.value}
+ {label && ( +
+ {label} +
+ )} +
+
+ ); + })} +
+
+ ); +}; diff --git a/src/animations/queue/components/QueueCode.jsx b/src/animations/queue/components/QueueCode.jsx new file mode 100644 index 0000000..081e3bd --- /dev/null +++ b/src/animations/queue/components/QueueCode.jsx @@ -0,0 +1,208 @@ +import { useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + EnqueueCode, + DequeueCode, + EliminarFinalCode, + InsertarCode, + ExtraerCode, +} from '../algorithms/QueueAlgorithm'; +import { SvgIconDropdown, svgAlgo } from '../../../assets/svg/SvgConstans'; +import { useTranslation } from 'react-i18next'; + +export const QueueCode = () => { + const codeRef = useRef(null); + const [prevCurrentLine, setPrevCurrentLine] = useState(null); + const { funAction, history, stepHistory } = useSelector(state => state.queue); + const [infoVisible, setInfoVisible] = useState(false); + const { t } = useTranslation(); + + useEffect(() => { + if (history === -1 || !stepHistory || history >= stepHistory.length) { + if ( + prevCurrentLine !== null && + codeRef.current?.childNodes[prevCurrentLine] + ) { + codeRef.current.childNodes[prevCurrentLine].classList.remove( + 'bg-secondary' + ); + setPrevCurrentLine(null); + } + return; + } + + const currentStep = stepHistory[history]; + if (!currentStep) return; + + const currentLine = currentStep.currentIndex; + + const currentElement = codeRef.current?.childNodes[currentLine]; + const prevElement = + prevCurrentLine !== null + ? codeRef.current?.childNodes[prevCurrentLine] + : null; + + if (prevElement && prevCurrentLine !== currentLine) { + prevElement.classList.remove('bg-secondary'); + } + + if (currentElement && currentLine !== prevCurrentLine) { + currentElement.classList.add('bg-secondary'); + } + + setPrevCurrentLine(currentLine); + }, [history, stepHistory, funAction, prevCurrentLine]); + + useEffect(() => { + return () => { + if (codeRef.current && prevCurrentLine !== null) { + const element = codeRef.current.childNodes[prevCurrentLine]; + if (element) element.classList.remove('bg-secondary'); + } + }; + }, [prevCurrentLine]); + + const toggleInfoPanel = () => { + setInfoVisible(!infoVisible); + }; + + const renderCode = funAction => { + switch (funAction) { + case 'enqueue': + return ; + case 'dequeue': + return ; + case 'eliminarFinal': + return ; + case 'insertar': + return ; + case 'extraer': + return ; + default: + return ( +
+

+ {t('selectOperation') || + 'Selecciona una operación para ver su algoritmo'} +

+

+ {t('availableOperations') || + 'Enqueue, Dequeue, Eliminar Final, Insertar, Extraer'} +

+
+ ); + } + }; + + return ( +
+
+
+ {svgAlgo} + {t('algorithm') || 'Algoritmo'} +
+ +
+ +
+
+ +
+
{renderCode(funAction)}
+
+ + {infoVisible && ( +
+
    +
  • + + {t('definition') || 'Definición'}: + + + {t('queueDefinition') || + 'Una cola es una estructura FIFO (First In, First Out).'} + +
  • +
  • + + {t('principalOperations') || 'Operaciones principales'}: + +
      +
    • + Enqueue: + + Añade un elemento al final de la cola + +
    • +
    • + Dequeue: + + Elimina el elemento del frente de la cola + +
    • +
    • + + Eliminar Final: + + + Elimina el último elemento de la cola + +
    • +
    • + + Insertar: + + + Inserta un elemento en una posición específica + +
    • +
    • + + Extraer: + + + Elimina un elemento en una posición específica + +
    • +
    +
  • +
  • + + {t('utility') || 'Utilidad'}: + + + {t('queueUsage') || + 'Útil para manejar tareas en orden temporal, colas de mensajes y procesos.'} + +
  • +
  • + + {t('implementations') || 'Implementaciones'}: + + + {t('queueImplementations') || 'Arrays, listas enlazadas, etc.'} + +
  • +
+
+ )} +
+ ); +}; + +export default QueueCode; diff --git a/src/animations/queue/components/QueueControls.jsx b/src/animations/queue/components/QueueControls.jsx new file mode 100644 index 0000000..1f86bda --- /dev/null +++ b/src/animations/queue/components/QueueControls.jsx @@ -0,0 +1,547 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { + decrementHistoryQueue, + incrementHistoryQueue, + actionButtonQueue, + restoreRepeatQueue, + restoreTimeIdQueue, + setHeadQueue, + setPlayingQueue, + setTimeIdQueue, + updateVisualizationQueue, +} from '../../../store'; +import { Alert, Button, Snackbar, TextField } from '@mui/material'; +import { useEffect, useState, useCallback, useRef } from 'react'; +import { BasicButton } from '../../../components'; +import { registrarClick } from '../../../utils/clickRegistry'; +import { + svgPause, + svgBack, + svgPlay, + svgRepeat, + svgForward, +} from '../../../assets/svg/SvgConstans'; + +export const QueueControls = () => { + const queueState = useSelector(state => state.queue); + const dispatch = useDispatch(); + + const [value, setValue] = useState(''); + const [insertPosition, setInsertPosition] = useState(''); + const [extractPosition, setExtractPosition] = useState(''); + const [toast, setToast] = useState(''); + const [open, setOpen] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); + + const animationExecutedRef = useRef(false); + const currentFunActionRef = useRef(null); + + const handleValueChange = event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue !== null)) { + setValue(inputValue); + } + }; + + const handleInsertPositionChange = event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue >= 0)) { + setInsertPosition(inputValue); + } + }; + + const handleExtractPositionChange = event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue >= 0)) { + setExtractPosition(inputValue); + } + }; + + const showError = useCallback(message => { + setToast(message); + setOpen(true); + }, []); + + const runAnimation = useCallback( + (stepHistory, startFromBeginning = false) => { + if (!stepHistory || stepHistory.length === 0) { + setIsAnimating(false); + return; + } + + dispatch(restoreTimeIdQueue()); + dispatch(setPlayingQueue(true)); + setIsAnimating(true); + + const timeIds = []; + + if (startFromBeginning) { + dispatch(updateVisualizationQueue(stepHistory[0])); + dispatch(incrementHistoryQueue()); + } + + const startIndex = startFromBeginning ? 1 : 0; + + for (let i = startIndex; i < stepHistory.length; i++) { + const step = stepHistory[i]; + const timeoutId = setTimeout( + () => { + dispatch(updateVisualizationQueue(step)); + dispatch(incrementHistoryQueue()); + }, + (i - startIndex) * (250 / (queueState.timeStep || 1)) + ); + + timeIds.push(timeoutId); + } + + const finalTimeoutId = setTimeout( + () => { + const finalState = stepHistory[stepHistory.length - 1]; + if (finalState) { + dispatch(updateVisualizationQueue(finalState)); + } + + dispatch(setPlayingQueue(false)); + dispatch(restoreTimeIdQueue()); + setIsAnimating(false); + animationExecutedRef.current = false; + }, + (stepHistory.length - startIndex) * (250 / (queueState.timeStep || 1)) + ); + + timeIds.push(finalTimeoutId); + dispatch(setTimeIdQueue(timeIds)); + }, + [dispatch, queueState.timeStep] + ); + + const executeAnimation = useCallback( + (action, value, position = null) => { + if (isAnimating) { + showError('Espera a que termine la animación actual'); + return; + } + + try { + dispatch(restoreTimeIdQueue()); + dispatch(setPlayingQueue(false)); + + const actionPayload = { action, value }; + if (position !== null) actionPayload.position = position; + + dispatch(setHeadQueue(queueState.head)); + dispatch(actionButtonQueue(actionPayload)); + dispatch(restoreRepeatQueue()); + + animationExecutedRef.current = false; + currentFunActionRef.current = action; + } catch (error) { + console.error('Error executing animation:', error); + showError('Error al ejecutar la animación'); + setIsAnimating(false); + } + }, + [dispatch, queueState.head, isAnimating, showError] + ); + + useEffect(() => { + if ( + queueState.stepHistory && + queueState.stepHistory.length > 0 && + queueState.funAction && + queueState.funAction === currentFunActionRef.current && + !queueState.playing && + !animationExecutedRef.current + ) { + animationExecutedRef.current = true; + runAnimation(queueState.stepHistory, true); + } + }, [queueState.stepHistory, queueState.funAction, runAnimation]); + + useEffect(() => { + if (queueState.funAction !== currentFunActionRef.current) { + animationExecutedRef.current = false; + currentFunActionRef.current = queueState.funAction; + } + }, [queueState.funAction]); + + const handleEnqueue = () => { + const numValue = value === '' ? null : parseInt(value); + if (numValue === null) { + showError('Ingresa un valor válido'); + return; + } + + if (queueState.elementos && queueState.elementos.length >= 7) { + showError('La cola está llena (máximo 7 elementos)'); + return; + } + + if (queueState.elementos && queueState.elementos.includes(numValue)) { + showError('El elemento ya existe en la cola'); + return; + } + + executeAnimation('enqueue', numValue); + registrarClick('Enqueue', `Insertar ${numValue} en la cola`); + setValue(''); + }; + + const handleDequeue = () => { + if (!queueState.head) { + showError('La cola está vacía'); + return; + } + + const frontValue = queueState.head.getValue(); + executeAnimation('dequeue', frontValue); + registrarClick('Dequeue', `Eliminar ${frontValue} del frente de la cola`); + }; + + const handleEliminarFinal = () => { + if (!queueState.head) { + showError('La cola está vacía'); + return; + } + + executeAnimation('eliminarFinal', null); + registrarClick('Eliminar Final', 'Eliminar último elemento de la cola'); + }; + + const handleInsert = () => { + const numValue = value === '' ? null : parseInt(value); + const pos = insertPosition === '' ? null : parseInt(insertPosition); + + if (numValue === null) { + showError('Ingresa un valor válido para insertar'); + return; + } + + if (pos === null || pos < 0) { + showError('Ingresa una posición válida (≥ 0)'); + return; + } + + if (queueState.elementos && queueState.elementos.includes(numValue)) { + showError('El elemento ya existe en la cola'); + return; + } + + if (queueState.elementos && queueState.elementos.length >= 7) { + showError('La cola está llena (máximo 7 elementos)'); + return; + } + + const maxPosition = queueState.elementos ? queueState.elementos.length : 0; + if (pos > maxPosition) { + showError(`La posición máxima válida es ${maxPosition}`); + return; + } + + executeAnimation('insertar', numValue, pos); + registrarClick('Insertar', `Insertar ${numValue} en posición ${pos}`); + setValue(''); + setInsertPosition(''); + }; + + const handleExtraer = () => { + if (!queueState.head) { + showError('La cola está vacía'); + return; + } + + const pos = extractPosition === '' ? null : parseInt(extractPosition); + if (pos === null || pos < 0) { + showError('Ingresa una posición válida (≥ 0)'); + return; + } + + const maxPosition = queueState.elementos + ? queueState.elementos.length - 1 + : 0; + if (pos > maxPosition) { + showError(`La posición máxima válida es ${maxPosition}`); + return; + } + + executeAnimation('extraer', null, pos); + registrarClick('Extraer', `Extraer elemento en posición ${pos}`); + setExtractPosition(''); + }; + + const onPlayPause = e => { + e.preventDefault(); + if (queueState.playing) { + pause(); + } else { + play(); + } + }; + + const play = () => { + if (queueState.stepHistory && queueState.stepHistory.length > 0) { + const currentHistory = queueState.history || -1; + const remainingSteps = queueState.stepHistory.slice( + Math.max(0, currentHistory + 1) + ); + + if (remainingSteps.length > 0) { + animationExecutedRef.current = true; + runAnimation(remainingSteps, false); + } else { + dispatch(restoreRepeatQueue()); + animationExecutedRef.current = true; + runAnimation(queueState.stepHistory, true); + } + } + }; + + const pause = () => { + dispatch(setPlayingQueue(false)); + dispatch(restoreTimeIdQueue()); + setIsAnimating(false); + }; + + const goBackward = () => { + if (queueState.playing) pause(); + + const currentHistory = queueState.history || -1; + if (currentHistory > 0 && queueState.stepHistory) { + const previousStep = queueState.stepHistory[currentHistory - 1]; + dispatch(decrementHistoryQueue()); + dispatch(updateVisualizationQueue(previousStep)); + } + }; + + const goForward = () => { + if (queueState.playing) pause(); + + const currentHistory = queueState.history || -1; + if ( + queueState.stepHistory && + currentHistory < queueState.stepHistory.length - 1 + ) { + const nextStep = queueState.stepHistory[currentHistory + 1]; + dispatch(incrementHistoryQueue()); + dispatch(updateVisualizationQueue(nextStep)); + } + }; + + const repeat = () => { + if (queueState.stepHistory && queueState.stepHistory.length > 0) { + dispatch(restoreTimeIdQueue()); + dispatch(restoreRepeatQueue()); + animationExecutedRef.current = true; + runAnimation(queueState.stepHistory, true); + } + }; + + useEffect(() => { + return () => { + dispatch(restoreTimeIdQueue()); + }; + }, [dispatch]); + + const playPauseIcon = queueState.playing ? svgPause : svgPlay; + + return ( +
+ {/* Controles de reproducción */} +
+ + {svgBack} + + + + {playPauseIcon} + + + = + (queueState.stepHistory ? queueState.stepHistory.length - 1 : 0) + } + title="Avanzar" + > + {svgForward} + + + + {svgRepeat} + +
+ + {/* Controles Enqueue/Dequeue */} +
+ +
+ + +
+
+ + {/* Controles para Insertar */} +
+ + +
+ + {/* Controles para Extraer */} +
+ + +
+ + {/* Control Eliminar Final */} +
+ +
+ + {/* Notificaciones */} + setOpen(false)} + > + {toast} + +
+ ); +}; + +export default QueueControls; diff --git a/src/animations/queue/components/index.js b/src/animations/queue/components/index.js new file mode 100644 index 0000000..cb074c9 --- /dev/null +++ b/src/animations/queue/components/index.js @@ -0,0 +1,3 @@ +export * from './QueueChart'; +export * from './QueueControls'; +export * from './QueueCode'; diff --git a/src/animations/queue/index.js b/src/animations/queue/index.js new file mode 100644 index 0000000..44413ea --- /dev/null +++ b/src/animations/queue/index.js @@ -0,0 +1,2 @@ +export * from './algorithms'; +export * from './components'; diff --git a/src/animations/simpleList/algorithms/Nodo.js b/src/animations/simpleList/algorithms/Nodo.js new file mode 100644 index 0000000..0c54a48 --- /dev/null +++ b/src/animations/simpleList/algorithms/Nodo.js @@ -0,0 +1,32 @@ +class Nodo { + constructor(value) { + this.value = value; + this.next = null; + } + + getValue() { + return this.value; + } + + setValue(value) { + this.value = value; + } + + getNext() { + return this.next; + } + + setNext(nextNode) { + this.next = nextNode; + } + + hasNext() { + return this.next !== null; + } + + toString() { + return `Nodo(${this.value})`; + } +} + +export default Nodo; diff --git a/src/animations/simpleList/algorithms/SimpleListAlgorithm.jsx b/src/animations/simpleList/algorithms/SimpleListAlgorithm.jsx new file mode 100644 index 0000000..c135df6 --- /dev/null +++ b/src/animations/simpleList/algorithms/SimpleListAlgorithm.jsx @@ -0,0 +1,1321 @@ +import { newTraceSimpleList, moveInHistoryRecordSimpleList } from './helpers'; +import Nodo from './Nodo'; + +// Función para copiar lista completa +export const copiarLista = cabeza => { + if (cabeza === null) return null; + + let nuevaCabeza = new Nodo(cabeza.getValue()); + let actualNuevo = nuevaCabeza; + let actualViejo = cabeza.getNext(); + + while (actualViejo !== null) { + actualNuevo.setNext(new Nodo(actualViejo.getValue())); + actualNuevo = actualNuevo.getNext(); + actualViejo = actualViejo.getNext(); + } + + return nuevaCabeza; +}; + +// Función para crear registro de animación +export const crearRegistro = ( + cabeza, + primerConjunto = [], + segundoConjunto = [], + tercerConjunto = [], + indiceActual = 0, + nodoResaltado = null, + puntero = null, + descripcion = '', + codigoResaltado = [] +) => { + return { + head: copiarLista(cabeza), + firstSet: [...primerConjunto], + secondSet: [...segundoConjunto], + thirdSet: [...tercerConjunto], + currentIndex: indiceActual, + highlightedNode: nodoResaltado, + pointer: puntero, + description: descripcion, + highlightedCode: [...codigoResaltado], + }; +}; + +// Algoritmo: Insertar al inicio +export const InsertarAlInicio = (cabeza, valor) => { + const historialPasos = []; + + // Estado inicial + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + 'Estado inicial de la lista enlazada', + [0] + ) + ); + + // Crear nuevo nodo + const nuevoNodo = new Nodo(valor); + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [], + [], + 1, + null, + null, + `Creando nuevo nodo con valor ${valor}`, + [1] + ) + ); + + if (cabeza === null) { + // Caso lista vacía + cabeza = nuevoNodo; + historialPasos.push( + crearRegistro( + cabeza, + [], + [valor], + [], + 2, + null, + null, + 'Lista vacía: el nuevo nodo se convierte en la cabeza', + [2, 3] + ) + ); + } else { + // Conectar nuevo nodo + nuevoNodo.setNext(cabeza); + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [], + [], + 2, + null, + null, + 'Conectando nuevo nodo con la cabeza actual', + [2] + ) + ); + + // Actualizar cabeza + cabeza = nuevoNodo; + historialPasos.push( + crearRegistro( + cabeza, + [], + [valor], + [], + 3, + null, + null, + 'Actualizando cabeza al nuevo nodo', + [3] + ) + ); + } + + // Estado final + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 4, + null, + null, + 'Inserción al inicio completada exitosamente', + [4] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Algoritmo: Insertar al final +export const InsertarAlFinal = (cabeza, valor) => { + const historialPasos = []; + + // Estado inicial + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + 'Estado inicial de la lista enlazada', + [0] + ) + ); + + // Crear nuevo nodo + const nuevoNodo = new Nodo(valor); + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [], + [], + 1, + null, + null, + `Creando nuevo nodo con valor ${valor}`, + [1] + ) + ); + + if (cabeza === null) { + // Caso lista vacía + cabeza = nuevoNodo; + historialPasos.push( + crearRegistro( + cabeza, + [], + [valor], + [], + 2, + null, + null, + 'Lista vacía: el nuevo nodo se convierte en la cabeza', + [2] + ) + ); + } else { + // Buscar último nodo + let nodoActual = cabeza; + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [nodoActual.getValue()], + [], + 2, + nodoActual.getValue(), + null, + 'Iniciando búsqueda del último nodo', + [3, 4] + ) + ); + + while (nodoActual.getNext() !== null) { + nodoActual = nodoActual.getNext(); + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [nodoActual.getValue()], + [], + 3, + nodoActual.getValue(), + null, + `Recorriendo lista, nodo actual: ${nodoActual.getValue()}`, + [5, 6] + ) + ); + } + + // Conectar nuevo nodo + nodoActual.setNext(nuevoNodo); + historialPasos.push( + crearRegistro( + cabeza, + [], + [nuevoNodo.getValue()], + [], + 4, + null, + null, + 'Conectando último nodo con el nuevo nodo', + [7] + ) + ); + } + + // Estado final + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 5, + null, + null, + 'Inserción al final completada exitosamente', + [8] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Algoritmo: Insertar en posición +export const InsertarEnPosicion = (cabeza, valor, posicion) => { + const historialPasos = []; + + // Estado inicial + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + `Insertando valor ${valor} en posición ${posicion}`, + [0] + ) + ); + + // Validar posición + if (posicion < 0) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Error: Posición inválida (menor que 0)', + [1] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Caso especial: insertar en posición 0 + if (posicion === 0) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Posición 0 detectada, redirigiendo a insertar al inicio', + [1] + ) + ); + return InsertarAlInicio(cabeza, valor); + } + + // Crear nuevo nodo + const nuevoNodo = new Nodo(valor); + historialPasos.push( + crearRegistro( + cabeza, + [valor], + [], + [], + 1, + null, + null, + `Creando nuevo nodo con valor ${valor}`, + [2] + ) + ); + + // Navegar hasta la posición anterior + let actual = cabeza; + let contador = 0; + + if (actual === null) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 2, + null, + null, + 'Error: No se puede insertar en posición mayor que 0 en lista vacía', + [3] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Navegar hasta la posición anterior + while (contador < posicion - 1 && actual !== null) { + historialPasos.push( + crearRegistro( + cabeza, + [actual.getValue()], + [], + [], + 2, + actual.getValue(), + null, + `Navegando a posición ${contador}, nodo actual: ${actual.getValue()}`, + [4, 5] + ) + ); + actual = actual.getNext(); + contador++; + } + + // Verificar posición válida + if (actual === null) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 3, + null, + null, + `Error: Posición ${posicion} fuera de rango`, + [6] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Realizar inserción + historialPasos.push( + crearRegistro( + cabeza, + [actual.getValue()], + [], + [], + 3, + actual.getValue(), + null, + `Posicionado en nodo anterior (posición ${contador})`, + [6] + ) + ); + + nuevoNodo.setNext(actual.getNext()); + actual.setNext(nuevoNodo); + + historialPasos.push( + crearRegistro( + cabeza, + [], + [valor], + [], + 4, + nuevoNodo.getValue(), + null, + `Insertando nodo en posición ${posicion}`, + [7, 8] + ) + ); + + // Estado final + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 5, + null, + null, + 'Inserción en posición completada exitosamente', + [9] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Algoritmo: Eliminar del inicio +export const EliminarDelInicio = cabeza => { + const historialPasos = []; + + // Estado inicial + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + 'Eliminando nodo del inicio de la lista', + [0] + ) + ); + + if (cabeza === null) { + // Caso lista vacía + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Error: No se puede eliminar de una lista vacía', + [1] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Identificar nodo a eliminar + const valorEliminado = cabeza.getValue(); + historialPasos.push( + crearRegistro( + cabeza, + [], + [valorEliminado], + [], + 2, + cabeza.getValue(), + null, + `Identificando nodo a eliminar: ${valorEliminado}`, + [2] + ) + ); + + // Actualizar cabeza + cabeza = cabeza.getNext(); + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 3, + null, + null, + 'Actualizando cabeza al siguiente nodo', + [3] + ) + ); + + // Estado final + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 4, + null, + null, + 'Eliminación del inicio completada exitosamente', + [4] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; +}; + +// Algoritmo: Eliminar del final +export const EliminarDelFinal = cabeza => { + const historialPasos = []; + + // Estado inicial + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + 'Eliminando nodo del final de la lista', + [0] + ) + ); + + if (cabeza === null) { + // Caso lista vacía + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Error: No se puede eliminar de una lista vacía', + [1] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Caso especial: solo un elemento + if (cabeza.getNext() === null) { + const valorEliminado = cabeza.getValue(); + historialPasos.push( + crearRegistro( + cabeza, + [], + [valorEliminado], + [], + 1, + cabeza.getValue(), + null, + `Único elemento en la lista: ${valorEliminado}`, + [2, 3] + ) + ); + + cabeza = null; + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 2, + null, + null, + 'Lista ahora vacía después de eliminar único elemento', + [4] + ) + ); + + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 3, + null, + null, + 'Eliminación del final completada exitosamente', + [8] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; + } + + // Buscar penúltimo nodo + let actual = cabeza; + historialPasos.push( + crearRegistro( + cabeza, + [actual.getValue()], + [], + [], + 1, + actual.getValue(), + null, + 'Buscando penúltimo nodo de la lista', + [5] + ) + ); + + while (actual.getNext().getNext() !== null) { + actual = actual.getNext(); + historialPasos.push( + crearRegistro( + cabeza, + [actual.getValue()], + [], + [], + 1, + actual.getValue(), + null, + `Continuando búsqueda, nodo actual: ${actual.getValue()}`, + [6] + ) + ); + } + + // Identificar último nodo + const valorEliminado = actual.getNext().getValue(); + historialPasos.push( + crearRegistro( + cabeza, + [actual.getValue()], + [valorEliminado], + [], + 2, + actual.getValue(), + null, + `Penúltimo nodo encontrado: ${actual.getValue()}, último nodo: ${valorEliminado}`, + [6] + ) + ); + + // Eliminar último nodo + actual.setNext(null); + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 3, + null, + null, + 'Desconectando último nodo de la lista', + [7] + ) + ); + + // Estado final + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 4, + null, + null, + 'Eliminación del final completada exitosamente', + [8] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; +}; + +// Algoritmo: Eliminar en posición +export const EliminarEnPosicion = (cabeza, posicion) => { + const historialPasos = []; + + // Estado inicial + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + `Eliminando nodo en posición ${posicion}`, + [0] + ) + ); + + if (cabeza === null) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Error: No se puede eliminar de una lista vacía', + [1] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + if (posicion < 0) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Error: Posición inválida (menor que 0)', + [1] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Caso especial: eliminar en posición 0 + if (posicion === 0) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Posición 0 detectada, redirigiendo a eliminar del inicio', + [1] + ) + ); + return EliminarDelInicio(cabeza); + } + + // Navegar hasta nodo anterior + let actual = cabeza; + let contador = 0; + + historialPasos.push( + crearRegistro( + cabeza, + [actual.getValue()], + [], + [], + 1, + actual.getValue(), + null, + `Navegando a posición ${posicion - 1}`, + [2] + ) + ); + + while (contador < posicion - 1 && actual !== null) { + actual = actual.getNext(); + contador++; + + if (actual !== null) { + historialPasos.push( + crearRegistro( + cabeza, + [actual.getValue()], + [], + [], + 1, + actual.getValue(), + null, + `Posición ${contador}, nodo actual: ${actual.getValue()}`, + [3, 4] + ) + ); + } + } + + // Verificar posición válida + if (actual === null || actual.getNext() === null) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 2, + null, + null, + `Error: Posición ${posicion} fuera de rango`, + [5] + ) + ); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Identificar nodo a eliminar + const valorEliminado = actual.getNext().getValue(); + historialPasos.push( + crearRegistro( + cabeza, + [actual.getValue()], + [valorEliminado], + [], + 2, + actual.getValue(), + null, + `Nodo anterior: ${actual.getValue()}, nodo a eliminar: ${valorEliminado}`, + [5] + ) + ); + + // Realizar eliminación + actual.setNext(actual.getNext().getNext()); + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 3, + null, + null, + 'Reconectando nodos para omitir el nodo eliminado', + [6] + ) + ); + + // Estado final + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 4, + null, + null, + 'Eliminación en posición completada exitosamente', + [7] + ) + ); + + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; +}; + +// Algoritmo: Buscar elemento +export const Buscar = (cabeza, valor) => { + const historialPasos = []; + + // Estado inicial + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 0, + null, + null, + `Iniciando búsqueda del valor ${valor}`, + [0] + ) + ); + + if (cabeza === null) { + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 1, + null, + null, + 'Lista vacía: valor no encontrado', + [1] + ) + ); + return { historialPasos, encontrado: false, posicion: -1 }; + } + + let nodoActual = cabeza; + let posicion = 0; + historialPasos.push( + crearRegistro( + cabeza, + [nodoActual.getValue()], + [], + [], + 2, + nodoActual.getValue(), + null, + `Iniciando búsqueda desde la cabeza`, + [2, 3] + ) + ); + + while (nodoActual !== null) { + historialPasos.push( + crearRegistro( + cabeza, + [nodoActual.getValue()], + [], + [], + 2, + nodoActual.getValue(), + null, + `Comparando: ${nodoActual.getValue()} con ${valor} en posición ${posicion}`, + [4, 5] + ) + ); + + if (nodoActual.getValue() === valor) { + // Elemento encontrado + historialPasos.push( + crearRegistro( + cabeza, + [], + [nodoActual.getValue()], + [], + 3, + nodoActual.getValue(), + null, + `¡Valor ${valor} encontrado en posición ${posicion}!`, + [6, 7] + ) + ); + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 4, + null, + null, + 'Búsqueda completada exitosamente', + [10] + ) + ); + return { + historialPasos, + encontrado: true, + posicion, + }; + } + + nodoActual = nodoActual.getNext(); + posicion++; + + if (nodoActual !== null) { + historialPasos.push( + crearRegistro( + cabeza, + [nodoActual.getValue()], + [], + [], + 2, + nodoActual.getValue(), + null, + `Avanzando al siguiente nodo: ${nodoActual.getValue()}`, + [8, 9] + ) + ); + } + } + + // Elemento no encontrado + historialPasos.push( + crearRegistro( + cabeza, + [], + [], + [], + 4, + null, + null, + `Valor ${valor} no encontrado en la lista`, + [10] + ) + ); + return { historialPasos, encontrado: false, posicion: -1 }; +}; + +// Componente para dibujar lista enlazada +export const DrawLinkedList = ({ record, cabeza }) => { + const currentRecord = record || { head: cabeza }; + + if (!currentRecord.head) { + return ( +
+
+
∅
+
Lista vacía
+
+
+ ); + } + + const nodes = []; + let current = currentRecord.head; + let index = 0; + + while (current !== null) { + const value = current.getValue(); + const isHighlighted = currentRecord.highlightedNode === value; + const isInFirstSet = currentRecord.firstSet.includes(value); + const isInSecondSet = currentRecord.secondSet.includes(value); + const isInThirdSet = currentRecord.thirdSet.includes(value); + + // Estilos del nodo + let nodeClass = + 'relative flex items-center justify-center w-20 h-16 border-3 rounded-lg font-bold text-lg transition-all duration-700 shadow-lg'; + + if (isInFirstSet) { + nodeClass += + ' bg-orange-100 border-orange-500 text-orange-900 shadow-orange-200'; + } else if (isInSecondSet) { + nodeClass += + ' bg-green-100 border-green-500 text-green-900 shadow-green-200'; + } else if (isInThirdSet) { + nodeClass += ' bg-red-100 border-red-500 text-red-900 shadow-red-200'; + } else if (isHighlighted) { + nodeClass += + ' bg-blue-100 border-blue-500 text-blue-900 scale-110 shadow-blue-200'; + } else { + nodeClass += ' bg-white border-gray-400 text-gray-800 shadow-gray-200'; + } + + // Crear el nodo visual + const nodeElement = ( +
+
+ {/* Indicador HEAD */} + {index === 0 && ( +
+
+ HEAD +
+
+
+ )} + + {/* Nodo principal */} +
+
+
{value}
+
+
+ {current.getNext() !== null ? '•' : '∅'} +
+
+
+
+ + {/* Flecha conectora */} + {current.getNext() !== null && ( +
+
+
+
+
+ )} + + {/* Indicador NULL al final */} + {current.getNext() === null && ( +
+
+
+
+
+ ∅ +
+
+ )} +
+ ); + + nodes.push(nodeElement); + current = current.getNext(); + index++; + } + + return ( +
+
+ {nodes} +
+
+ ); +}; + +// Funciones de visualización de código +export const InsertarAlInicioCode = ({ codeRef, highlightedLines = [] }) => { + const lines = [ + 'Node nuevoNodo = new Node(valor);', + 'nuevoNodo.setNext(cabeza);', + 'cabeza = nuevoNodo;', + ]; + + return ( +
+ + {lines.map((line, index) => ( +
+            {line}
+          
+ ))} +
+
+ ); +}; + +export const InsertarAlFinalCode = ({ codeRef, highlightedLines = [] }) => { + const lines = [ + 'Node nuevoNodo = new Node(valor);', + 'if (cabeza == null) { cabeza = nuevoNodo; return; }', + 'Node actual = cabeza;', + 'while (actual.getNext() != null)', + ' actual = actual.getNext();', + 'actual.setNext(nuevoNodo);', + ]; + + return ( +
+ + {lines.map((line, index) => ( +
+            {line}
+          
+ ))} +
+
+ ); +}; + +export const InsertarEnPosicionCode = ({ codeRef, highlightedLines = [] }) => { + const lines = [ + 'if (posicion == 0) return insertarAlInicio(valor);', + 'Node nuevoNodo = new Node(valor);', + 'if (cabeza == null) return;', + 'Node actual = cabeza;', + 'for (int i = 0; i < posicion - 1; i++) {', + ' if (actual == null) return;', + ' actual = actual.getNext();', + '}', + 'nuevoNodo.setNext(actual.getNext());', + 'actual.setNext(nuevoNodo);', + ]; + + return ( +
+ + {lines.map((line, index) => ( +
+            {line}
+          
+ ))} +
+
+ ); +}; + +export const EliminarDelInicioCode = ({ codeRef, highlightedLines = [] }) => { + const lines = [ + 'if (cabeza == null) return -1;', + 'int valor = cabeza.getValue();', + 'cabeza = cabeza.getNext();', + 'return valor;', + ]; + + return ( +
+ + {lines.map((line, index) => ( +
+            {line}
+          
+ ))} +
+
+ ); +}; + +export const EliminarDelFinalCode = ({ codeRef, highlightedLines = [] }) => { + const lines = [ + 'if (cabeza == null) return -1;', + 'if (cabeza.getNext() == null) {', + ' int valor = cabeza.getValue();', + ' cabeza = null;', + ' return valor;', + '}', + 'Node actual = cabeza;', + 'while (actual.getNext().getNext() != null)', + ' actual = actual.getNext();', + 'int valor = actual.getNext().getValue();', + 'actual.setNext(null);', + 'return valor;', + ]; + + return ( +
+ + {lines.map((line, index) => ( +
+            {line}
+          
+ ))} +
+
+ ); +}; + +export const EliminarEnPosicionCode = ({ codeRef, highlightedLines = [] }) => { + const lines = [ + 'if (posicion == 0) return eliminarDelInicio();', + 'Node actual = cabeza;', + 'for (int i = 0; i < posicion - 1; i++)', + ' actual = actual.getNext();', + 'if (actual == null || actual.getNext() == null) return -1;', + 'int valor = actual.getNext().getValue();', + 'actual.setNext(actual.getNext().getNext());', + 'return valor;', + ]; + + return ( +
+ + {lines.map((line, index) => ( +
+            {line}
+          
+ ))} +
+
+ ); +}; + +export const BuscarCode = ({ codeRef, highlightedLines = [] }) => { + const lines = [ + 'if (cabeza == null) return -1;', + 'Node actual = cabeza;', + 'int posicion = 0;', + 'while (actual != null) {', + ' if (actual.getValue() == valor)', + ' return posicion;', + ' actual = actual.getNext();', + ' posicion++;', + '}', + 'return -1;', + ]; + + return ( +
+ + {lines.map((line, index) => ( +
+            {line}
+          
+ ))} +
+
+ ); +}; diff --git a/src/animations/simpleList/algorithms/helpers.jsx b/src/animations/simpleList/algorithms/helpers.jsx new file mode 100644 index 0000000..6b6fc94 --- /dev/null +++ b/src/animations/simpleList/algorithms/helpers.jsx @@ -0,0 +1,36 @@ +export const newTraceSimpleList = head => { + return [ + { + head: head, + firstSet: [], + secondSet: [], + thirdSet: [], + isHead: head ? head.getValue() : -1, + currentIndex: null, + highlightedNode: null, + pointer: null, + }, + ]; +}; + +export const moveInHistoryRecordSimpleList = ( + stepHistory, + head, + firstSet = [], + secondSet = [], + thirdSet = [], + currentIndex = null, + highlightedNode = null, + pointer = null +) => { + stepHistory.push({ + head: head, + firstSet: [...firstSet], + secondSet: [...secondSet], + thirdSet: [...thirdSet], + isHead: head ? head.getValue() : -1, + currentIndex: currentIndex, + highlightedNode: highlightedNode, + pointer: pointer, + }); +}; diff --git a/src/animations/simpleList/algorithms/index.js b/src/animations/simpleList/algorithms/index.js new file mode 100644 index 0000000..2c63468 --- /dev/null +++ b/src/animations/simpleList/algorithms/index.js @@ -0,0 +1,3 @@ +export * from './helpers'; +export * from './Nodo'; +export * from './SimpleListAlgorithm'; diff --git a/src/animations/simpleList/components/SimpleListChart.jsx b/src/animations/simpleList/components/SimpleListChart.jsx new file mode 100644 index 0000000..685237f --- /dev/null +++ b/src/animations/simpleList/components/SimpleListChart.jsx @@ -0,0 +1,234 @@ +import { useSelector } from 'react-redux'; +import { useState, useEffect, useMemo } from 'react'; + +// Función para contar nodos +const countNodes = node => { + if (!node) return 0; + return 1 + countNodes(node.next); +}; + +// Función para convertir lista a array +const listToArray = (node, arr = []) => { + if (!node) return arr; + arr.push(node); + return listToArray(node.next, arr); +}; + +export const SimpleListChart = () => { + const { + head, + isHead, + firstSet, + secondSet, + thirdSet, + highlightedNode, + pointer, + } = useSelector(state => state.simpleList); + + const nodes = useMemo(() => listToArray(head), [head]); + const nodeCount = nodes.length; + + // Calcular tamaño dinámico basado en cantidad de nodos + const getNodeSize = () => { + if (nodeCount <= 4) + return { + size: 'normal', + nodeWidth: 64, + fontSize: 18, + arrowWidth: 32, + padding: 16, + }; + if (nodeCount <= 6) + return { + size: 'medium', + nodeWidth: 52, + fontSize: 14, + arrowWidth: 24, + padding: 12, + }; + if (nodeCount <= 8) + return { + size: 'small', + nodeWidth: 44, + fontSize: 12, + arrowWidth: 16, + padding: 8, + }; + return { + size: 'tiny', + nodeWidth: 36, + fontSize: 10, + arrowWidth: 12, + padding: 6, + }; + }; + + const nodeStyles = getNodeSize(); + + if (!head) { + return ( +
+
+
+ 📋 Lista vacía +
+
+
+ ); + } + + return ( +
+
+
+ {nodes.map((node, index) => ( +
+ {/* Nodo */} + + + {/* Flecha conectora */} + {node.getNext() && ( + + )} +
+ ))} +
+
+
+ ); +}; + +const NodeComponent = ({ + value, + firstSet, + secondSet, + thirdSet, + isHighlighted, + isPointed, + nodeStyles, + isHead = false, +}) => { + const [isAnimating, setIsAnimating] = useState(false); + + const getBackgroundColor = () => { + if (firstSet.includes(value)) + return 'bg-gradient-to-br from-green-400 to-green-600'; + if (secondSet.includes(value)) + return 'bg-gradient-to-br from-red-400 to-red-600'; + if (thirdSet.includes(value)) + return 'bg-gradient-to-br from-yellow-400 to-yellow-600'; + return 'bg-gradient-to-br from-blue-400 to-blue-600'; + }; + + useEffect(() => { + if (isHighlighted) { + setIsAnimating(true); + const timer = setTimeout(() => setIsAnimating(false), 1000); + return () => clearTimeout(timer); + } + }, [isHighlighted]); + + const isDangerNode = secondSet.includes(value); + + return ( +
+ {/* Indicador HEAD - solo para el primer nodo */} + {isHead && ( +
+
+ HEAD +
+
+ )} + + {/* Puntero */} + {isPointed && ( +
+
+ 🎯 Puntero +
+
+
+ )} + + {/* Nodo principal */} +
+ {/* Efecto de brillo */} +
+ + {/* Valor del nodo */} +
+ {value} +
+ + {/* Efecto de resaltado */} + {isHighlighted && ( +
+ )} +
+
+ ); +}; + +const ArrowComponent = ({ arrowWidth }) => { + return ( +
+
+ {/* Línea de la flecha */} +
+ + {/* Punta de la flecha */} +
+
+
+
+
+ ); +}; diff --git a/src/animations/simpleList/components/SimpleListCode.jsx b/src/animations/simpleList/components/SimpleListCode.jsx new file mode 100644 index 0000000..2ec2a91 --- /dev/null +++ b/src/animations/simpleList/components/SimpleListCode.jsx @@ -0,0 +1,152 @@ +import { useEffect, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + InsertarAlInicioCode, + InsertarAlFinalCode, + InsertarEnPosicionCode, + EliminarDelInicioCode, + EliminarDelFinalCode, + EliminarEnPosicionCode, + BuscarCode, +} from '../algorithms/SimpleListAlgorithm'; +import { SvgIconDropdown, svgAlgo } from '../../../assets/svg/SvgConstans'; +import { useTranslation } from 'react-i18next'; + +export const SimpleListCode = () => { + const codeRef = useRef(null); + const [prevCurrentLine, setPrevCurrentLine] = useState(null); + const { funAction, history, stepHistory } = useSelector( + state => state.simpleList + ); + const [infoVisible, setInfoVisible] = useState(false); + const { t } = useTranslation(); + + // Efecto para resaltar línea de código actual + useEffect(() => { + if (history === -1 || !stepHistory || history >= stepHistory.length) { + if ( + prevCurrentLine !== null && + codeRef.current?.childNodes[prevCurrentLine] + ) { + codeRef.current.childNodes[prevCurrentLine].classList.remove( + 'bg-secondary' + ); + setPrevCurrentLine(null); + } + return; + } + + const currentStep = stepHistory[history]; + if (!currentStep) return; + + const currentLine = currentStep.currentIndex; + const currentElement = codeRef.current?.childNodes[currentLine]; + const prevElement = + prevCurrentLine !== null + ? codeRef.current?.childNodes[prevCurrentLine] + : null; + + if (prevElement && prevCurrentLine !== currentLine) { + prevElement.classList.remove('bg-secondary'); + } + + if (currentElement && currentLine !== prevCurrentLine) { + currentElement.classList.add('bg-secondary'); + } + + setPrevCurrentLine(currentLine); + }, [history, stepHistory, funAction, prevCurrentLine]); + + // Renderizar código según la operación + const renderCode = funAction => { + switch (funAction) { + case 'insertarAlInicio': + return ; + case 'insertarAlFinal': + return ; + case 'insertarEnPosicion': + return ; + case 'eliminarDelInicio': + return ; + case 'eliminarDelFinal': + return ; + case 'eliminarEnPosicion': + return ; + case 'buscar': + return ; + default: + return ( +
+

+ {t('selectOperation') || + 'Selecciona una operación para ver su algoritmo'} +

+
+ ); + } + }; + + return ( +
+ {/* Header del panel de código */} +
+
+ {svgAlgo} + {t('algorithm') || 'Algoritmo'} +
+ +
+ + {/* Contenido del código */} +
+
{renderCode(funAction)}
+
+ + {/* Panel de información */} + {infoVisible && ( +
+
    +
  • + + {t('definition') || 'Definición'}: + + + Estructura lineal donde cada nodo contiene datos y un puntero al + siguiente nodo + +
  • +
  • + Operaciones: +
      +
    • + Inserción: Al inicio, + final o posición específica +
    • +
    • + Eliminación: Del inicio, + final o posición específica +
    • +
    • + Búsqueda: Recorrido + secuencial +
    • +
    +
  • +
+
+ )} +
+ ); +}; diff --git a/src/animations/simpleList/components/SimpleListControls.jsx b/src/animations/simpleList/components/SimpleListControls.jsx new file mode 100644 index 0000000..2111b68 --- /dev/null +++ b/src/animations/simpleList/components/SimpleListControls.jsx @@ -0,0 +1,721 @@ +import { useDispatch, useSelector } from 'react-redux'; +import { + decrementHistorySimpleList, + incrementHistorySimpleList, + actionButtonSimpleList, + restoreRepeatSimpleList, + restoreTimeIdSimpleList, + setHeadSimpleList, + setPlayingSimpleList, + setTimeIdSimpleList, + updateVisualizationSimpleList, +} from '../../../store'; +import { Alert, Button, Snackbar, TextField } from '@mui/material'; +import { useCallback, useState, useRef, useEffect } from 'react'; +import { BasicButton } from '../../../components'; +import { + svgPause, + svgBack, + svgPlay, + svgRepeat, + svgForward, +} from '../../../assets/svg/SvgConstans'; +import { registrarClick } from '../../../utils/clickRegistry'; + +export const SimpleListControls = () => { + const simpleListState = useSelector(state => state.simpleList); + const dispatch = useDispatch(); + + // Estados para los inputs + const [value, setValue] = useState(''); + const [position, setPosition] = useState(''); + const [searchValue, setSearchValue] = useState(''); + const [deletePosition, setDeletePosition] = useState(''); + + // Estados para el toast/notificaciones + const [toast, setToast] = useState(''); + const [open, setOpen] = useState(false); + + // Estado para controlar las animaciones + const [isAnimating, setIsAnimating] = useState(false); + + // Referencias para controlar la ejecución de animaciones + const animationExecutedRef = useRef(false); + const currentFunActionRef = useRef(null); + + // Función para obtener el tamaño actual de la lista + const getListSize = useCallback(() => { + if (!simpleListState.head) return 0; + let count = 0; + let current = simpleListState.head; + while (current !== null) { + count++; + current = current.getNext(); + } + return count; + }, [simpleListState.head]); + + // Función para verificar si un elemento ya existe en la lista + const elementExists = useCallback( + value => { + if (!simpleListState.head) return false; + let current = simpleListState.head; + while (current !== null) { + if (current.getValue() === value) return true; + current = current.getNext(); + } + return false; + }, + [simpleListState.head] + ); + + // Manejadores de cambio para los inputs + const handleValueChange = useCallback(event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue !== null)) { + setValue(inputValue); + } + }, []); + + const handlePositionChange = useCallback(event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue >= 0)) { + setPosition(inputValue); + } + }, []); + + const handleSearchValueChange = useCallback(event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue !== null)) { + setSearchValue(inputValue); + } + }, []); + + const handleDeletePositionChange = useCallback(event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue >= 0)) { + setDeletePosition(inputValue); + } + }, []); + + // Función para mostrar mensajes de error + const showError = useCallback(message => { + setToast(message); + setOpen(true); + }, []); + + // Función para limpiar timeouts activos + const clearActiveTimeouts = useCallback(() => { + if (simpleListState.timeIds && simpleListState.timeIds.length > 0) { + simpleListState.timeIds.forEach(timeoutId => { + clearTimeout(timeoutId); + }); + dispatch(restoreTimeIdSimpleList()); + } + }, [simpleListState.timeIds, dispatch]); + + // Función para ejecutar las animaciones paso a paso + const runAnimation = useCallback( + (stepHistory, startFromBeginning = false) => { + if (!stepHistory || stepHistory.length === 0) { + setIsAnimating(false); + return; + } + + clearActiveTimeouts(); + dispatch(setPlayingSimpleList(true)); + setIsAnimating(true); + + const timeIds = []; + + if (startFromBeginning) { + dispatch(updateVisualizationSimpleList(stepHistory[0])); + dispatch(incrementHistorySimpleList()); + } + + const startIndex = startFromBeginning ? 1 : 0; + + for (let i = startIndex; i < stepHistory.length; i++) { + const step = stepHistory[i]; + const timeoutId = setTimeout( + () => { + dispatch(updateVisualizationSimpleList(step)); + dispatch(incrementHistorySimpleList()); + }, + (i - startIndex) * (250 / (simpleListState.timeStep || 1)) + ); + + timeIds.push(timeoutId); + } + + const finalTimeoutId = setTimeout( + () => { + const finalState = stepHistory[stepHistory.length - 1]; + if (finalState) { + dispatch(updateVisualizationSimpleList(finalState)); + } + + dispatch(setPlayingSimpleList(false)); + dispatch(restoreTimeIdSimpleList()); + setIsAnimating(false); + animationExecutedRef.current = false; + }, + (stepHistory.length - startIndex) * + (250 / (simpleListState.timeStep || 1)) + ); + + timeIds.push(finalTimeoutId); + dispatch(setTimeIdSimpleList(timeIds)); + }, + [dispatch, simpleListState.timeStep, clearActiveTimeouts] + ); + + // Función para ejecutar animaciones de manera robusta + const executeAnimation = useCallback( + (action, value, position = null) => { + if (isAnimating) { + showError('Espera a que termine la animación actual'); + return; + } + + try { + clearActiveTimeouts(); + dispatch(setPlayingSimpleList(false)); + + const actionPayload = { action, value }; + if (position !== null) { + actionPayload.position = position; + } + + // Marcar que se va a ejecutar una nueva animación + animationExecutedRef.current = false; + currentFunActionRef.current = action; + + dispatch(setHeadSimpleList(simpleListState.head)); + dispatch(actionButtonSimpleList(actionPayload)); + // NO llamar a restoreRepeatSimpleList aquí, causa el bug de doble animación + } catch (error) { + console.error('Error executing animation:', error); + showError('Error al ejecutar la animación'); + setIsAnimating(false); + } + }, + [ + dispatch, + simpleListState.head, + isAnimating, + showError, + clearActiveTimeouts, + ] + ); + + // Efecto para manejar la ejecución automática de animaciones + useEffect(() => { + // Solo ejecutar si hay un stepHistory nuevo y válido + if ( + simpleListState.stepHistory && + simpleListState.stepHistory.length > 0 && + simpleListState.funAction && + simpleListState.funAction === currentFunActionRef.current && + !simpleListState.playing && + !animationExecutedRef.current && + simpleListState.history === -1 // Solo si no ha empezado la animación + ) { + animationExecutedRef.current = true; + runAnimation(simpleListState.stepHistory, true); + } + }, [ + simpleListState.stepHistory, + simpleListState.funAction, + simpleListState.playing, + simpleListState.history, + runAnimation, + ]); + + // Limpiar refs cuando cambia la acción + useEffect(() => { + if (simpleListState.funAction !== currentFunActionRef.current) { + animationExecutedRef.current = false; + currentFunActionRef.current = simpleListState.funAction; + } + }, [simpleListState.funAction]); + + // Handlers para operaciones de lista + const handleInsertarAlInicio = () => { + const numValue = value === '' ? null : parseInt(value); + + if (numValue === null) { + showError('Ingresa un valor válido'); + return; + } + + if (elementExists(numValue)) { + showError('El elemento ya existe en la lista'); + return; + } + + executeAnimation('insertarAlInicio', numValue); + registrarClick( + 'Insertar al inicio', + `Insertar ${numValue} al inicio de la lista` + ); + setValue(''); + }; + + const handleInsertarAlFinal = () => { + const numValue = value === '' ? null : parseInt(value); + + if (numValue === null) { + showError('Ingresa un valor válido'); + return; + } + + if (elementExists(numValue)) { + showError('El elemento ya existe en la lista'); + return; + } + + executeAnimation('insertarAlFinal', numValue); + registrarClick( + 'Insertar al final', + `Insertar ${numValue} al final de la lista` + ); + setValue(''); + }; + + const handleInsertarEnPosicion = () => { + const numValue = value === '' ? null : parseInt(value); + const pos = position === '' ? null : parseInt(position); + + if (numValue === null) { + showError('Ingresa un valor válido para insertar'); + return; + } + + if (elementExists(numValue)) { + showError('El elemento ya existe en la lista'); + return; + } + + if (pos === null || pos < 0) { + showError('Ingresa una posición válida (≥ 0)'); + return; + } + + const maxPosition = getListSize(); + if (pos > maxPosition) { + showError(`La posición máxima válida es ${maxPosition}`); + return; + } + + executeAnimation('insertarEnPosicion', numValue, pos); + registrarClick( + 'Insertar en posición', + `Insertar ${numValue} en posición ${pos}` + ); + setValue(''); + setPosition(''); + }; + + const handleEliminarDelInicio = () => { + if (!simpleListState.head) { + showError('La lista está vacía'); + return; + } + + executeAnimation('eliminarDelInicio', null); + registrarClick( + 'Eliminar del inicio', + 'Eliminar primer elemento de la lista' + ); + }; + + const handleEliminarDelFinal = () => { + if (!simpleListState.head) { + showError('La lista está vacía'); + return; + } + + executeAnimation('eliminarDelFinal', null); + registrarClick( + 'Eliminar del final', + 'Eliminar último elemento de la lista' + ); + }; + + const handleEliminarEnPosicion = () => { + if (!simpleListState.head) { + showError('La lista está vacía'); + return; + } + + const pos = deletePosition === '' ? null : parseInt(deletePosition); + + if (pos === null || pos < 0) { + showError('Ingresa una posición válida (≥ 0)'); + return; + } + + const maxPosition = getListSize() - 1; + if (pos > maxPosition) { + showError(`La posición máxima válida es ${maxPosition}`); + return; + } + + executeAnimation('eliminarEnPosicion', null, pos); + registrarClick( + 'Eliminar en posición', + `Eliminar elemento en posición ${pos}` + ); + setDeletePosition(''); + }; + + const handleBuscar = () => { + if (!simpleListState.head) { + showError('La lista está vacía'); + return; + } + + const numValue = searchValue === '' ? null : parseInt(searchValue); + + if (numValue === null) { + showError('Ingresa un valor válido para buscar'); + return; + } + + executeAnimation('buscar', numValue); + registrarClick('Buscar', `Buscar ${numValue} en la lista`); + setSearchValue(''); + }; + + // Controles de reproducción manual + const onPlayPause = e => { + e.preventDefault(); + if (simpleListState.playing) { + pause(); + } else { + play(); + } + }; + + const play = () => { + if (simpleListState.stepHistory && simpleListState.stepHistory.length > 0) { + const currentHistory = simpleListState.history || -1; + const remainingSteps = simpleListState.stepHistory.slice( + Math.max(0, currentHistory + 1) + ); + + if (remainingSteps.length > 0) { + animationExecutedRef.current = true; + runAnimation(remainingSteps, false); + } else { + dispatch(restoreRepeatSimpleList()); + animationExecutedRef.current = true; + runAnimation(simpleListState.stepHistory, true); + } + } + }; + + const pause = () => { + dispatch(setPlayingSimpleList(false)); + clearActiveTimeouts(); + setIsAnimating(false); + }; + + const goBackward = () => { + if (simpleListState.playing) { + pause(); + } + + const currentHistory = simpleListState.history || -1; + if (currentHistory > 0 && simpleListState.stepHistory) { + const previousStep = simpleListState.stepHistory[currentHistory - 1]; + dispatch(decrementHistorySimpleList()); + dispatch(updateVisualizationSimpleList(previousStep)); + } + }; + + const goForward = () => { + if (simpleListState.playing) { + pause(); + } + + const currentHistory = simpleListState.history || -1; + if ( + simpleListState.stepHistory && + currentHistory < simpleListState.stepHistory.length - 1 + ) { + const nextStep = simpleListState.stepHistory[currentHistory + 1]; + dispatch(incrementHistorySimpleList()); + dispatch(updateVisualizationSimpleList(nextStep)); + } + }; + + const repeat = () => { + if (simpleListState.stepHistory && simpleListState.stepHistory.length > 0) { + clearActiveTimeouts(); + dispatch(restoreRepeatSimpleList()); + animationExecutedRef.current = true; + runAnimation(simpleListState.stepHistory, true); + } + }; + + // Limpiar timeouts al desmontar el componente + useEffect(() => { + return () => { + clearActiveTimeouts(); + }; + }, [clearActiveTimeouts]); + + // Determinar el icono de play/pause + const playPauseIcon = simpleListState.playing ? svgPause : svgPlay; + + return ( +
+ {/* Controles de reproducción */} +
+ + {svgBack} + + + + {playPauseIcon} + + + = + simpleListState.stepHistory.length - 1 + } + title="Avanzar" + > + {svgForward} + + + + {svgRepeat} + +
+ + {/* Controles Insertar Inicio/Final */} +
+ + + +
+ + {/* Controles para Insertar en Posición */} +
+ + +
+ + {/* Controles para Eliminar en Posición */} +
+ 0 ? getListSize() - 1 : 0, + }} + /> + +
+ + {/* Control Eliminar Inicio/Final */} +
+ + +
+ + {/* Control Buscar */} +
+ + +
+ + {/* Notificaciones */} + setOpen(false)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + > + setOpen(false)} + severity="error" + sx={{ width: '100%' }} + > + {toast} + + +
+ ); +}; diff --git a/src/animations/simpleList/components/index.js b/src/animations/simpleList/components/index.js new file mode 100644 index 0000000..cc3b251 --- /dev/null +++ b/src/animations/simpleList/components/index.js @@ -0,0 +1,3 @@ +export * from './SimpleListChart'; +export * from './SimpleListControls'; +export * from './SimpleListCode'; diff --git a/src/animations/simpleList/index.js b/src/animations/simpleList/index.js new file mode 100644 index 0000000..44413ea --- /dev/null +++ b/src/animations/simpleList/index.js @@ -0,0 +1,2 @@ +export * from './algorithms'; +export * from './components'; diff --git a/src/animations/sorting/algorithms/BubbleSort.jsx b/src/animations/sorting/algorithms/BubbleSort.jsx index 5e16f08..30f1e2a 100644 --- a/src/animations/sorting/algorithms/BubbleSort.jsx +++ b/src/animations/sorting/algorithms/BubbleSort.jsx @@ -59,13 +59,18 @@ export const BubbleSortLegend = createKey('Comparing', 'Swapping'); export const BubbleSortCode = ({ codeRef }) => { return ( -
{`do`}
-
{`  swapped = falso
-  for i = 1 to lastUnsortedIndex - 1`}
-
{`    if left > right`}
-
{`      swap(left, right)
-      swapped = true; swapCounter++`}
-
{`while swapped`}
+
{`public static void bubbleSort(int[] arr) {`}
+
{`    int n = arr.length;`}
+
{`    for (int i = 0; i < n - 1; i++) {`}
+
{`        for (int j = 0; j < n - i - 1; j++) {`}
+
{`            if (arr[j] > arr[j + 1]) {`}
+
{`                int temp = arr[j];`}
+
{`                arr[j] = arr[j + 1];`}
+
{`                arr[j + 1] = temp;`}
+
{`            }`}
+
{`        }`}
+
{`    }`}
+
{`}`}
); }; diff --git a/src/animations/sorting/algorithms/HeapSort.jsx b/src/animations/sorting/algorithms/HeapSort.jsx index 6ac1b8c..4067dbb 100644 --- a/src/animations/sorting/algorithms/HeapSort.jsx +++ b/src/animations/sorting/algorithms/HeapSort.jsx @@ -231,12 +231,17 @@ export const HeapSortLegend = createKey('Comparing', 'Swapping', null, 'Heap'); export const HeapSortCode = ({ codeRef }) => { return ( -
{`BuildMaxHeap(arr)`}
-
{`n = length(arr)`}
-
{`// Extract elements from the heap one by one
-for i from n - 1 down to 1:`}
-
{`	Swap(arr[0], arr[i])`}
-
{`	MaxHeapify(arr, 0, i)`}
+
{`public static void heapSort(int[] arr) {`}
+
{`    int n = arr.length;`}
+
{`    for (int i = n / 2 - 1; i >= 0; i--)`}
+
{`        maxHeapify(arr, n, i);`}
+
{`    for (int i = n - 1; i > 0; i--) {`}
+
{`        int temp = arr[0];`}
+
{`        arr[0] = arr[i];`}
+
{`        arr[i] = temp;`}
+
{`        maxHeapify(arr, i, 0);`}
+
{`    }`}
+
{`}`}
); }; diff --git a/src/animations/sorting/algorithms/InsertionSort.jsx b/src/animations/sorting/algorithms/InsertionSort.jsx index 2b41752..a8dbf7c 100644 --- a/src/animations/sorting/algorithms/InsertionSort.jsx +++ b/src/animations/sorting/algorithms/InsertionSort.jsx @@ -55,14 +55,17 @@ export const InsertionSortLegend = createKey( export const InsertionSortCode = ({ codeRef }) => { return ( -
{`mark first element as sorted`}
-
{`for each unsorted element X
-	 'extract' the element X`}
-
{`	for j = lastSortedIndex down to 0`}
-
{`		if current element j > X
-			move sorted element 
-			to the right by 1`}
-
{`		break loop and insert X here`}
+
{`public static void insertionSort(int[] arr) {`}
+
{`    for (int i = 1; i < arr.length; i++) {`}
+
{`        int key = arr[i];`}
+
{`        int j = i - 1;`}
+
{`        while (j >= 0 && arr[j] > key) {`}
+
{`            arr[j + 1] = arr[j];`}
+
{`            j--;`}
+
{`        }`}
+
{`        arr[j + 1] = key;`}
+
{`    }`}
+
{`}`}
); }; diff --git a/src/animations/sorting/algorithms/QuickSort.jsx b/src/animations/sorting/algorithms/QuickSort.jsx index aba8199..fd289fa 100644 --- a/src/animations/sorting/algorithms/QuickSort.jsx +++ b/src/animations/sorting/algorithms/QuickSort.jsx @@ -117,13 +117,13 @@ export const QuickSortLegend = createKey( export const QuickSortCode = ({ codeRef }) => { return ( -
{`for each (unsorted) partition
-set random element as pivot
-	storeIndex = pivotIndex+1`}
-
{`	for i = pivotIndex+1 to rightmostIndex
-		if (a[i] < a[pivot])`}
-
{`			swap(i, storeIndex); storeIndex++`}
-
{`	swap(pivot, storeIndex-1)`}
+
{`public static void quickSort(int[] arr, int low, int high) {`}
+
{`    if (low < high) {`}
+
{`        int pi = partition(arr, low, high);`}
+
{`        quickSort(arr, low, pi - 1);`}
+
{`        quickSort(arr, pi + 1, high);`}
+
{`    }`}
+
{`}`}
); }; diff --git a/src/animations/sorting/algorithms/SelectionSort.jsx b/src/animations/sorting/algorithms/SelectionSort.jsx index 67ca5be..c545955 100644 --- a/src/animations/sorting/algorithms/SelectionSort.jsx +++ b/src/animations/sorting/algorithms/SelectionSort.jsx @@ -108,12 +108,18 @@ export const SelectionSortLegend = createKey('Comparing', 'Swapping'); export const SelectionSortCode = ({ codeRef }) => { return ( -
{`repeat (numOfElements - 1) times
-	set the first unsorted element as the minimum
-	for each of the unsorted elements`}
-
{`		if element < currentMinimum`}
-
{`			set element as new minimum`}
-
{`	swap minimum with first unsorted position`}
+
{`public static void selectionSort(int[] arr) {`}
+
{`    int n = arr.length;`}
+
{`    for (int i = 0; i < n - 1; i++) {`}
+
{`        int minIdx = i;`}
+
{`        for (int j = i + 1; j < n; j++) {`}
+
{`            if (arr[j] < arr[minIdx]) minIdx = j;`}
+
{`        }`}
+
{`        int temp = arr[i];`}
+
{`        arr[i] = arr[minIdx];`}
+
{`        arr[minIdx] = temp;`}
+
{`    }`}
+
{`}`}
); }; diff --git a/src/animations/sorting/algorithms/ShellSort.jsx b/src/animations/sorting/algorithms/ShellSort.jsx index fe69e0e..6447898 100644 --- a/src/animations/sorting/algorithms/ShellSort.jsx +++ b/src/animations/sorting/algorithms/ShellSort.jsx @@ -58,16 +58,20 @@ export const ShellSortLegend = createKey('Comparing', 'Swapping'); export const ShellSortCode = ({ codeRef }) => { return ( -
{`for (i = num/2; i>0; i = i/2) {
-	for (j = i; j < num; j++) {
-    	for(k = j-i; k >= 0; k = k-i) {`}
-
{`			if (arr[k+i] < arr[k])`}
-
{`				tmp = arr[k];
-                arr[k] = arr[k+i];
-                arr[k+i] = tmp;`}
-
{`			else {
-                    break;
-                }`}
+
{`public static void shellSort(int[] arr) {`}
+
{`    int n = arr.length;`}
+
{`    for (int gap = n / 2; gap > 0; gap /= 2) {`}
+
{`        for (int i = gap; i < n; i++) {`}
+
{`            int temp = arr[i];`}
+
{`            int j = i;`}
+
{`            while (j >= gap && arr[j - gap] > temp) {`}
+
{`                arr[j] = arr[j - gap];`}
+
{`                j -= gap;`}
+
{`            }`}
+
{`            arr[j] = temp;`}
+
{`        }`}
+
{`    }`}
+
{`}`}
); }; diff --git a/src/animations/sorting/components/SortCode.jsx b/src/animations/sorting/components/SortCode.jsx index b539946..1393447 100644 --- a/src/animations/sorting/components/SortCode.jsx +++ b/src/animations/sorting/components/SortCode.jsx @@ -87,37 +87,34 @@ export const SortCode = () => { return (
-
- {/* Panel de información */} - {infoVisible && ( -
+
+ {svgAlgo} + {t('algorithm')} +
+
+ - {Info(algorithm)} -
- )} - - {/* Div header */} -
-
- {svgAlgo} - {t('algorithm')} -
-
- - {t('explanation')} - - -
+ {t('explanation')} + +
{/* Div del algoritmo */}
{Code(algorithm)}
+ + {/* Panel de información */} + {infoVisible && ( +
+
+ {Info(algorithm)} +
+
+ )}
); }; diff --git a/src/animations/stack/algorithms/Nodo.js b/src/animations/stack/algorithms/Nodo.js index 1637c91..968ba0b 100644 --- a/src/animations/stack/algorithms/Nodo.js +++ b/src/animations/stack/algorithms/Nodo.js @@ -1,18 +1,38 @@ -export default class Nodo { - constructor(value, next = null) { +class Nodo { + constructor(value) { this.value = value; - this.next = next; + this.next = null; } - setNext(next) { - this.next = next; + // Getter para obtener el valor del nodo + getValue() { + return this.value; + } + + // Setter para establecer el valor del nodo + setValue(value) { + this.value = value; } + // Getter para obtener la referencia al siguiente nodo getNext() { return this.next; } - getValue() { - return this.value; + // Setter para establecer la referencia al siguiente nodo + setNext(nextNode) { + this.next = nextNode; + } + + // Método para verificar si el nodo tiene un siguiente + hasNext() { + return this.next !== null; + } + + // Método toString para representación en string del nodo + toString() { + return `Nodo(${this.value})`; } } + +export default Nodo; diff --git a/src/animations/stack/algorithms/StackAlgorithm.jsx b/src/animations/stack/algorithms/StackAlgorithm.jsx index 5038fdd..711c91a 100644 --- a/src/animations/stack/algorithms/StackAlgorithm.jsx +++ b/src/animations/stack/algorithms/StackAlgorithm.jsx @@ -1,77 +1,367 @@ import { newTraceStack, moveInHistoryRecordStack } from './helpers'; import Nodo from './Nodo'; -export const Push = (head, value, isHead) => { - const stepHistory = newTraceStack(head, isHead); - - const nodo = new Nodo(value); - nodo.setNext(head); - head = nodo; - - moveInHistoryRecordStack( - stepHistory, - head, - [head.getValue()], - [], - head.getNext() === null ? -1 : head.getNext().getValue(), - 0 - ); - moveInHistoryRecordStack( - stepHistory, - head, - [], - [head.getValue()], - head.getValue(), - 1 - ); +// Función mejorada para copiar la pila completa +const copiarPila = cabeza => { + if (cabeza === null) return null; - return stepHistory; + let nuevaCabeza = new Nodo(cabeza.getValue()); + let actualNuevo = nuevaCabeza; + let actualViejo = cabeza.getNext(); + + while (actualViejo !== null) { + actualNuevo.setNext(new Nodo(actualViejo.getValue())); + actualNuevo = actualNuevo.getNext(); + actualViejo = actualViejo.getNext(); + } + + return nuevaCabeza; }; -export const Pop = (head, value, isHead) => { - const stepHistory = newTraceStack(head, isHead); - - if (head.getNext() !== null) { - const temp = head; - moveInHistoryRecordStack( - stepHistory, - head, - [head.getValue()], - [], - head.getValue(), - 1 - ); - moveInHistoryRecordStack( - stepHistory, - head, - [head.getValue()], - [head.getNext() === null ? -1 : head.getNext().getValue()], - head.getNext() === null ? -1 : head.getNext().getValue(), - 2 +// Función mejorada para crear un registro de estado para la animación +const crearRegistro = ( + cabeza, + primerConjunto = [], + segundoConjunto = [], + indiceActual = 0, + nodoAEliminar = null +) => { + return { + head: copiarPila(cabeza), + firstSet: [...primerConjunto], + secondSet: [...segundoConjunto], + isHead: cabeza ? cabeza.getValue() : -1, + currentIndex: indiceActual, + deletingNode: nodoAEliminar, + }; +}; + +// Animación Push: Añadir un elemento al tope de la pila +export const Push = (cabeza, valor) => { + const historialPasos = []; + + // Paso 1: Visualizar el estado inicial + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + + // Paso 2: Crear un nuevo nodo + const nuevoNodo = new Nodo(valor); + historialPasos.push(crearRegistro(cabeza, [valor], [], 1)); + + // Paso 3: Si la pila está vacía, el nuevo nodo es la cabeza + if (cabeza === null) { + cabeza = nuevoNodo; + // Mostrar el nuevo nodo como la cabeza + historialPasos.push(crearRegistro(cabeza, [], [], 3)); + } + // Paso 4: Si no, conectar el nuevo nodo al tope actual + else { + nuevoNodo.setNext(cabeza); + // Mostrar el nuevo nodo y su conexión + historialPasos.push(crearRegistro(cabeza, [valor], [], 2)); + + // Paso 5: El nuevo nodo se convierte en el tope + cabeza = nuevoNodo; + // Actualizar cabeza + historialPasos.push(crearRegistro(cabeza, [], [], 3)); + } + + // Paso 6: Mostrar estado final + historialPasos.push(crearRegistro(cabeza, [], [], 4)); + + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Animación Pop: Eliminar un elemento del tope de la pila +export const Pop = cabeza => { + const historialPasos = []; + + // Paso 1: Visualizar el estado inicial + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + + // Paso 2: Verificar si la pila está vacía + if (cabeza === null) { + historialPasos.push(crearRegistro(cabeza, [], [], 1)); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Paso 3: Guardar el valor a eliminar + const valorEliminado = cabeza.getValue(); + + // Resaltar en rojo (secondSet) varios pasos como en la cola + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 2)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 2)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 2)); + + // Preparar la animación de desaparición (deletingNode) + historialPasos.push(crearRegistro(cabeza, [], [], 2, valorEliminado)); + historialPasos.push(crearRegistro(cabeza, [], [], 2, valorEliminado)); + + // Paso 4: Actualizar el tope para que apunte al siguiente elemento + cabeza = cabeza.getNext(); + historialPasos.push(crearRegistro(cabeza, [], [], 3)); + + // Paso 5: Mostrar estado final + historialPasos.push(crearRegistro(cabeza, [], [], 4)); + + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; +}; + +// Animación Sumergir: Mover el elemento del tope al fondo de la pila +export const Sumergir = cabeza => { + const historialPasos = []; + + // Paso 1: Visualizar el estado inicial + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + + // Paso 2: Verificar si la pila está vacía o tiene un solo elemento + if (cabeza === null || cabeza.getNext() === null) { + historialPasos.push(crearRegistro(cabeza, [], [], 1)); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Paso 3: Guardar el nodo del tope y su valor + const nodoTope = cabeza; + const valorTope = nodoTope.getValue(); + historialPasos.push(crearRegistro(cabeza, [valorTope], [], 2, valorTope)); + + // Paso 4: Actualizar el tope para que apunte al segundo elemento + cabeza = cabeza.getNext(); + historialPasos.push(crearRegistro(cabeza, [valorTope], [], 3, valorTope)); + + // Paso 5: Buscar el último nodo de la pila + let nodoActual = cabeza; + while (nodoActual.getNext() !== null) { + nodoActual = nodoActual.getNext(); + historialPasos.push( + crearRegistro(cabeza, [valorTope], [nodoActual.getValue()], 4, valorTope) ); - head = temp.getNext(); - moveInHistoryRecordStack( - stepHistory, - head, - [], - [head.getValue()], - head.getValue(), - 3 + } + + // Paso 6: El nodo tope ahora apunta a null y se coloca al final + nodoTope.setNext(null); + nodoActual.setNext(nodoTope); + historialPasos.push( + crearRegistro(cabeza, [valorTope], [nodoActual.getValue()], 5) + ); + + // Paso 7: Mostrar estado final + historialPasos.push(crearRegistro(cabeza, [], [], 5)); + + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Animación Insertar: Insertar un elemento en una posición específica +export const Insertar = (cabeza, valor, posicion) => { + const historialPasos = []; + + // Paso 1: Visualizar el estado inicial + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + + // Paso 2: Crear un nuevo nodo + const nuevoNodo = new Nodo(valor); + historialPasos.push(crearRegistro(cabeza, [valor], [], 1)); + + // Paso 3: Si la pila está vacía, solo se puede insertar en posición 0 + if (cabeza === null) { + if (posicion === 0) { + cabeza = nuevoNodo; + historialPasos.push(crearRegistro(cabeza, [], [], 2)); + historialPasos.push(crearRegistro(cabeza, [], [], 5)); + return { historialPasos, nuevaCabeza: cabeza }; + } else { + historialPasos.push(crearRegistro(cabeza, [], [], 1)); + return { historialPasos, nuevaCabeza: cabeza }; + } + } + + // Paso 4: Contar el tamaño actual de la pila + let tamanoPila = 0; + let temp = cabeza; + while (temp !== null) { + tamanoPila++; + temp = temp.getNext(); + } + + // Paso 5: Si posición <= 0, insertar en el tope (cima) + if (posicion <= 0) { + nuevoNodo.setNext(cabeza); + cabeza = nuevoNodo; + historialPasos.push(crearRegistro(cabeza, [], [], 2)); + historialPasos.push(crearRegistro(cabeza, [], [], 5)); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Paso 6: Si posición >= tamaño, insertar al fondo + if (posicion >= tamanoPila) { + // Encontrar el último nodo + let nodoActual = cabeza; + historialPasos.push(crearRegistro(cabeza, [valor], [], 3)); + + while (nodoActual.getNext() !== null) { + nodoActual = nodoActual.getNext(); + historialPasos.push( + crearRegistro(cabeza, [valor], [nodoActual.getValue()], 3) + ); + } + + // Insertar al final + nodoActual.setNext(nuevoNodo); + historialPasos.push(crearRegistro(cabeza, [], [nuevoNodo.getValue()], 4)); + historialPasos.push(crearRegistro(cabeza, [], [], 5)); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Paso 7: Insertar en posición intermedia (contando desde el tope) + let nodoActual = cabeza; + let contador = 0; + historialPasos.push(crearRegistro(cabeza, [valor], [], 3)); + + // Navegar hasta la posición anterior + while (contador < posicion - 1) { + nodoActual = nodoActual.getNext(); + contador++; + historialPasos.push( + crearRegistro(cabeza, [valor], [nodoActual.getValue()], 3) ); - } else { - head = null; - moveInHistoryRecordStack(stepHistory, head, [], [], -1, -1); } - return stepHistory; + // Insertar el nuevo nodo + nuevoNodo.setNext(nodoActual.getNext()); + historialPasos.push( + crearRegistro(cabeza, [valor], [nodoActual.getValue()], 4) + ); + + nodoActual.setNext(nuevoNodo); + historialPasos.push(crearRegistro(cabeza, [], [nuevoNodo.getValue()], 4)); + + // Paso 8: Mostrar estado final + historialPasos.push(crearRegistro(cabeza, [], [], 5)); + + return { historialPasos, nuevaCabeza: cabeza }; }; +// Animación Extraer: Eliminar un elemento en una posición específica +export const Extraer = (cabeza, posicion) => { + const historialPasos = []; + + // Paso 1: Visualizar el estado inicial + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + + // Paso 2: Verificar si la pila está vacía + if (cabeza === null) { + historialPasos.push(crearRegistro(cabeza, [], [], 1)); + return { historialPasos, nuevaCabeza: cabeza }; + } + + // Paso 3: Contar el tamaño de la pila + let tamanoPila = 0; + let temp = cabeza; + while (temp !== null) { + tamanoPila++; + temp = temp.getNext(); + } + + // Paso 4: Verificar que la posición sea válida (0 <= posición < tamanoPila) + if (posicion < 0 || posicion >= tamanoPila) { + historialPasos.push(crearRegistro(cabeza, [], [], 1)); + return { historialPasos, nuevaCabeza: cabeza }; + } + + let valorEliminado; + + // Caso 1: Eliminar en posición 0 (tope -> pop-like) + if (posicion === 0) { + valorEliminado = cabeza.getValue(); + + // Resaltar en rojo varios pasos + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 2)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 2)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 2)); + + // Preparar desaparición + historialPasos.push(crearRegistro(cabeza, [], [], 2, valorEliminado)); + historialPasos.push(crearRegistro(cabeza, [], [], 2, valorEliminado)); + + cabeza = cabeza.getNext(); + historialPasos.push(crearRegistro(cabeza, [], [], 3)); + historialPasos.push(crearRegistro(cabeza, [], [], 4)); + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; + } + + // Caso 2: Eliminar en la última posición (fondo) + if (posicion === tamanoPila - 1) { + // Encontrar el penúltimo nodo + let nodoActual = cabeza; + while (nodoActual.getNext().getNext() !== null) { + nodoActual = nodoActual.getNext(); + historialPasos.push( + crearRegistro(cabeza, [], [nodoActual.getValue()], 2) + ); + } + + // Valor a eliminar (el último) + valorEliminado = nodoActual.getNext().getValue(); + + // Resaltar en rojo antes de desaparición + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 3)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 3)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 3)); + + // Preparar desaparición + historialPasos.push(crearRegistro(cabeza, [], [], 3, valorEliminado)); + historialPasos.push(crearRegistro(cabeza, [], [], 3, valorEliminado)); + + // Eliminar el último nodo + nodoActual.setNext(null); + historialPasos.push(crearRegistro(cabeza, [], [], 4)); + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; + } + + // Caso 3: Eliminar en posición intermedia (contando desde el tope) + // Navegar hasta el nodo anterior al que se va a eliminar + let nodoActual = cabeza; + let contador = 0; + while (contador < posicion - 1) { + nodoActual = nodoActual.getNext(); + contador++; + historialPasos.push(crearRegistro(cabeza, [], [nodoActual.getValue()], 2)); + } + + // Guardar el valor a eliminar + valorEliminado = nodoActual.getNext().getValue(); + + // Resaltar en rojo antes de desaparición + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 3)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 3)); + historialPasos.push(crearRegistro(cabeza, [], [valorEliminado], 3)); + + // Preparar desaparición + historialPasos.push(crearRegistro(cabeza, [], [], 3, valorEliminado)); + historialPasos.push(crearRegistro(cabeza, [], [], 3, valorEliminado)); + + // Eliminar el nodo de la cadena + nodoActual.setNext(nodoActual.getNext().getNext()); + historialPasos.push(crearRegistro(cabeza, [], [], 4)); + + // Paso 7: Mostrar estado final + historialPasos.push(crearRegistro(cabeza, [], [], 4)); + + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; +}; + +// Componentes de código con explicaciones detalladas para mejor visualización export const PushCode = ({ codeRef }) => { return ( -
{`Nodo nodo = new Nodo(int)
-nodo.next = head`}
-
{`head = nodo`}
+
{`public void push(int valor) {`}
+
{`    Node nuevoNodo = new Node(valor);`}
+
{`    if (cabeza == null) {`}
+
{`        cabeza = nuevoNodo;`}
+
{`    } else {`}
+
{`        nuevoNodo.setNext(cabeza);`}
+
{`        cabeza = nuevoNodo;`}
+
{`    }`}
+
{`}`}
); }; @@ -79,10 +369,66 @@ nodo.next = head`} export const PopCode = ({ codeRef }) => { return ( -
{`if empty, do nothing`}
-
{`tmp = head`}
-
{`head = tmp.next`}
-
{`delete tmp`}
+
{`public int pop() {`}
+
{`    if (cabeza == null) return -1;`}
+
{`    int valor = cabeza.getValue();`}
+
{`    cabeza = cabeza.getNext();`}
+
{`    return valor;`}
+
{`}`}
+
+ ); +}; + +export const SumergirCode = ({ codeRef }) => { + return ( + +
{`public void sumergir() {`}
+
{`    if (cabeza == null || cabeza.getNext() == null) return;`}
+
{`    Node nodo = cabeza;`}
+
{`    cabeza = cabeza.getNext();`}
+
{`    Node siguiente = cabeza.getNext();`}
+
{`    while (siguiente != null) {`}
+
{`        siguiente = siguiente.getNext();`}
+
{`    }`}
+
{`    siguiente.setNext(nodo);`}
+
{`}`}
+
+ ); +}; + +export const InsertarCode = ({ codeRef }) => { + return ( + +
{`public void insertar(int valor, int posicion) {`}
+
{`    Node nuevoNodo = new Node(valor);`}
+
{`    if (cabeza == null && posicion == 0) {`}
+
{`        cabeza = nuevoNodo;`}
+
{`        return;`}
+
{`    }`}
+
{`    int tamanoPila = 0;`}
+
{`    Node temp = cabeza;`}
+
{`    while (temp != null) {`}
+
{`        tamanoPila++;`}
+
{`        temp = temp.getNext();`}
+
{`    }`}
+
{`}`}
+
+ ); +}; + +export const ExtraerCode = ({ codeRef }) => { + return ( + +
{`public int extraer(int posicion) {`}
+
{`    if (cabeza == null) return -1;`}
+
{`    int tamanoPila = 0;`}
+
{`    Node temp = cabeza;`}
+
{`    while (temp != null) {`}
+
{`        tamanoPila++;`}
+
{`        temp = temp.getNext();`}
+
{`    }`}
+
{`    if (posicion >= tamanoPila) return -1;`}
+
{`}`}
); }; diff --git a/src/animations/stack/algorithms/helpers.jsx b/src/animations/stack/algorithms/helpers.jsx index b89cd12..90b83f9 100644 --- a/src/animations/stack/algorithms/helpers.jsx +++ b/src/animations/stack/algorithms/helpers.jsx @@ -1,10 +1,10 @@ -export const newTraceStack = (head, isHead) => { +export const newTraceStack = head => { return [ { head: head, firstSet: [], secondSet: [], - isHead: isHead, + isHead: head ? head.getValue() : -1, currentIndex: null, }, ]; @@ -15,14 +15,13 @@ export const moveInHistoryRecordStack = ( head, firstSet = [], secondSet = [], - isHead = -1, currentIndex = null ) => { stepHistory.push({ head: head, firstSet: [...firstSet], secondSet: [...secondSet], - isHead: isHead, + isHead: head ? head.getValue() : -1, currentIndex: currentIndex, }); }; diff --git a/src/animations/stack/components/StackChart.jsx b/src/animations/stack/components/StackChart.jsx index 701215f..4efbe2b 100644 --- a/src/animations/stack/components/StackChart.jsx +++ b/src/animations/stack/components/StackChart.jsx @@ -1,46 +1,155 @@ import { useSelector } from 'react-redux'; +import { useState, useEffect } from 'react'; + +// Función para contar nodos en la pila +const countNodes = node => { + if (!node) return 0; + return 1 + countNodes(node.next); +}; + +// Función para convertir la pila a array +const stackToArray = (node, arr = []) => { + if (!node) return arr; + arr.push(node); + return stackToArray(node.next, arr); +}; export const StackChart = () => { - const { head, isHead, firstSet, secondSet } = useSelector( + const { head, isHead, firstSet, secondSet, deletingNode } = useSelector( state => state.stack ); + const nodeCount = countNodes(head); + const nodes = stackToArray(head); + + // Calcular tamaño dinámico basado en cantidad de elementos + // Altura del contenedor: ~224px (h-56), con padding ~8px + // Espacio disponible: ~216px + const containerHeight = 224; + const padding = 16; + const availableHeight = containerHeight - padding; + + // Calcular altura y gap óptimos + const getNodeStyles = () => { + if (nodeCount === 0) return { height: 32, gap: 8, fontSize: 14 }; + + // Altura mínima por nodo: 20px, máxima: 32px + // Gap mínimo: 2px, máximo: 8px + const maxNodeHeight = 32; + const minNodeHeight = 20; + const maxGap = 8; + const minGap = 2; + + // Espacio necesario con tamaño máximo + const maxSpaceNeeded = nodeCount * maxNodeHeight + (nodeCount - 1) * maxGap; + + if (maxSpaceNeeded <= availableHeight) { + return { height: maxNodeHeight, gap: maxGap, fontSize: 14 }; + } + + // Calcular proporcionalmente + const ratio = availableHeight / maxSpaceNeeded; + const calculatedHeight = Math.max( + minNodeHeight, + Math.floor(maxNodeHeight * ratio) + ); + const calculatedGap = Math.max(minGap, Math.floor(maxGap * ratio)); + const calculatedFontSize = Math.max(10, Math.floor(14 * ratio)); + + return { + height: calculatedHeight, + gap: calculatedGap, + fontSize: calculatedFontSize, + }; + }; + + const { height, gap, fontSize } = getNodeStyles(); + return ( -
-
- {renderStack(head, isHead, firstSet, secondSet)} +
+
+ {nodes.map((node, index) => ( + + ))}
); }; -const renderStack = (node, isHead, firstSet, secondSet) => { - if (!node) { - return null; - } - +const StackNode = ({ + node, + isHead, + firstSet, + secondSet, + deletingNode, + height, + fontSize, +}) => { const getBackgroundColor = () => { if (firstSet.includes(node.value)) return 'bg-success'; if (secondSet.includes(node.value)) return 'bg-danger'; return 'bg-secondary'; }; - const nodoClassName = `w-full h-8 mt-2 flex justify-center items-center - relative text-black font-bold transition duration-200 ease-in-out rounded-md shadow-md ${getBackgroundColor()}`; + const isDangerNode = + secondSet.includes(node.value) || deletingNode === node.value; + const isDeleting = deletingNode === node.value; + const isNewNode = firstSet.includes(node.value); + const bgClass = firstSet.includes(node.value) + ? 'bg-success' + : isDangerNode + ? 'bg-danger' + : 'bg-secondary'; + + // Determinar qué animación aplicar + let animationClass = ''; + + if (isDeleting) { + animationClass = 'animate-fadeOutScale'; + } else if (isNewNode) { + animationClass = 'animate-fadeInScale'; + } return ( - <> -
+
+
{node.value === isHead && ( -
- ⬅️ - head +
+ + head
)} {node.value}
- - {renderStack(node.next, isHead, firstSet, secondSet)} - +
); }; diff --git a/src/animations/stack/components/StackCode.jsx b/src/animations/stack/components/StackCode.jsx index 3354a05..13565dc 100644 --- a/src/animations/stack/components/StackCode.jsx +++ b/src/animations/stack/components/StackCode.jsx @@ -1,6 +1,12 @@ import { useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; -import { PopCode, PushCode } from '../algorithms'; +import { + PopCode, + PushCode, + SumergirCode, + InsertarCode, + ExtraerCode, +} from '../algorithms'; import { SvgIconDropdown, svgAlgo } from '../../../assets/svg/SvgConstans'; import { useTranslation } from 'react-i18next'; @@ -11,95 +17,206 @@ export const StackCode = () => { const [infoVisible, setInfoVisible] = useState(false); const { t } = useTranslation(); + // Efecto para actualizar la línea destacada en el código useEffect(() => { - if (history === -1 || history >= stepHistory.length) return; + // Validar que tenemos elementos válidos + if (history === -1 || !stepHistory || history >= stepHistory.length) { + // Limpiar cualquier línea previamente destacada + if ( + prevCurrentLine !== null && + codeRef.current?.childNodes[prevCurrentLine] + ) { + codeRef.current.childNodes[prevCurrentLine].classList.remove( + 'bg-secondary' + ); + setPrevCurrentLine(null); + } + return; + } + + const currentStep = stepHistory[history]; + if (!currentStep) return; - const currentLine = stepHistory[history].currentIndex; + const currentLine = currentStep.currentIndex; - const currentElement = codeRef.current.childNodes[currentLine]; - const prevElement = codeRef.current.childNodes[prevCurrentLine]; + // Obtener elementos del DOM + const currentElement = codeRef.current?.childNodes[currentLine]; + const prevElement = + prevCurrentLine !== null + ? codeRef.current?.childNodes[prevCurrentLine] + : null; - if (prevElement) { + // Remover clase de la línea anterior si existe + if (prevElement && prevCurrentLine !== currentLine) { prevElement.classList.remove('bg-secondary'); } - if (currentElement) { + // Añadir clase a la línea actual si existe y es diferente a la anterior + if (currentElement && currentLine !== prevCurrentLine) { currentElement.classList.add('bg-secondary'); } + // Actualizar la línea anterior setPrevCurrentLine(currentLine); - }, [history, stepHistory, funAction]); + }, [history, stepHistory, funAction, prevCurrentLine]); - const Code = funAction => { + // Función para renderizar el código específico de cada operación + const renderCode = funAction => { switch (funAction) { case 'push': return ; case 'pop': return ; + case 'sumergir': + return ; + case 'insertar': + return ; + case 'extraer': + return ; default: - null; + return ( +
+

+ {t('selectOperation') || + 'Selecciona una operación para ver su algoritmo'} +

+

+ {t('availableOperations') || + 'Push, Pop, Insertar, Extraer o Sumergir'} +

+
+ ); } }; + // Función para alternar la visibilidad del panel de información const toggleInfoPanel = () => { setInfoVisible(!infoVisible); }; + // Limpiar las clases CSS cuando el componente se desmonta + useEffect(() => { + return () => { + if (codeRef.current && prevCurrentLine !== null) { + const element = codeRef.current.childNodes[prevCurrentLine]; + if (element) { + element.classList.remove('bg-secondary'); + } + } + }; + }, [prevCurrentLine]); + return ( -
-
- {/* Panel de información */} - {infoVisible && ( -
+ {/* Header del panel de código */} +
+
+ {svgAlgo} + {t('algorithm') || 'Algoritmo'} +
+
+
- )} - - {/* Div header */} -
-
- {svgAlgo} - {t('algorithm')} -
-
- - {t('explanation')} - - -
+ {t('explanation') || 'Explicación'} + +
-
- {Code(funAction)} + + {/* Contenido del código */} +
+
{renderCode(funAction)}
+ + {/* Panel de información */} + {infoVisible && ( +
+
    +
  • + + {t('definition') || 'Definición'}: + + + {t('stackDefinition') || + 'Una pila es una estructura de datos LIFO (Last In, First Out)'} + +
  • +
  • + + {t('principalOperations') || 'Operaciones principales'}: + +
      +
    • + Push: + + {t('pushExplanation') || + 'Añade un elemento al tope de la pila'} + +
    • +
    • + Pop: + + {t('popExplanation') || + 'Elimina el elemento del tope de la pila'} + +
    • +
    • + + Sumergir: + + + Mueve el elemento del tope de la pila al final + +
    • +
    • + + Insertar: + + + Agrega un elemento en una posición específica de la pila + +
    • +
    • + + Extraer: + + + Elimina un elemento en una posición específica de la pila + +
    • +
    +
  • +
  • + + {t('utility') || 'Utilidad'}: + + + {t('stackUsageExplanation') || + 'Útil para operaciones de deshacer, navegación, etc.'} + +
  • +
  • + + {t('implementations') || 'Implementaciones'}: + + + {t('stackImplementations') || 'Arrays, listas enlazadas, etc.'} + +
  • +
+
+ )}
); }; diff --git a/src/animations/stack/components/StackControls.jsx b/src/animations/stack/components/StackControls.jsx index 1a9fa1e..530f5a6 100644 --- a/src/animations/stack/components/StackControls.jsx +++ b/src/animations/stack/components/StackControls.jsx @@ -11,8 +11,9 @@ import { updateVisualizationStack, } from '../../../store'; import { Alert, Button, Snackbar, TextField } from '@mui/material'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback, useRef } from 'react'; import { BasicButton } from '../../../components'; +import { registrarClick } from '../../../utils/clickRegistry'; import { svgPause, svgBack, @@ -25,49 +26,286 @@ export const StackControls = () => { const stackState = useSelector(state => state.stack); const dispatch = useDispatch(); + // Estados locales const [value, setValue] = useState(''); + const [insertPosition, setInsertPosition] = useState(''); + const [extractPosition, setExtractPosition] = useState(''); const [toast, setToast] = useState(''); const [open, setOpen] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); - const handleNumeroChange = event => { - setValue(parseInt(event.target.value)); + // Ref para controlar si ya se ejecutó la animación automática + const animationExecutedRef = useRef(false); + const currentFunActionRef = useRef(null); - dispatch(setHead(stackState.head)); + // Handlers para cambios en inputs + const handleValueChange = event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue !== null)) { + setValue(inputValue); + } + }; - if (stackState.playing) { - pause(); + const handleInsertPositionChange = event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue >= 0)) { + setInsertPosition(inputValue); } + }; - if (stackState.elementos.length > 6) { - setToast('La pila está llena'); - setOpen(true); - return; + const handleExtractPositionChange = event => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue >= 0)) { + setExtractPosition(inputValue); } + }; + + // Función para mostrar mensajes de error + const showError = useCallback(message => { + setToast(message); + setOpen(true); + }, []); + + // Función para ejecutar la animación paso a paso - ARREGLADA + const runAnimation = useCallback( + (stepHistory, startFromBeginning = false) => { + if (!stepHistory || stepHistory.length === 0) { + setIsAnimating(false); + return; + } + + // Limpiar timeouts previos + dispatch(restoreTimeId()); + dispatch(setPlayingStack(true)); + setIsAnimating(true); - if (!stackState.elementos.includes(parseInt(event.target.value))) { - dispatch( - actionButton({ - action: 'push', - value: parseInt(event.target.value), - }) + const timeIds = []; + + // Si empezamos desde el principio, mostrar el primer paso inmediatamente + if (startFromBeginning) { + dispatch(updateVisualizationStack(stepHistory[0])); + dispatch(incrementHistoryStack()); // Ahora incrementamos DESPUÉS de mostrar + } + + // Ejecutar cada paso restante con retraso + const startIndex = startFromBeginning ? 1 : 0; + + for (let i = startIndex; i < stepHistory.length; i++) { + const step = stepHistory[i]; + const timeoutId = setTimeout( + () => { + dispatch(updateVisualizationStack(step)); // Primero mostramos + dispatch(incrementHistoryStack()); // Luego incrementamos + }, + (i - startIndex) * (250 / (stackState.timeStep || 1)) + ); + + timeIds.push(timeoutId); + } + + // Finalizar animación + const finalTimeoutId = setTimeout( + () => { + const finalState = stepHistory[stepHistory.length - 1]; + if (finalState) { + dispatch(updateVisualizationStack(finalState)); + } + + dispatch(setPlayingStack(false)); + dispatch(restoreTimeId()); + setIsAnimating(false); + animationExecutedRef.current = false; // Reset del flag + }, + (stepHistory.length - startIndex) * (250 / (stackState.timeStep || 1)) ); - } else { - setValue(''); - setToast('El elemento ya existe'); - setOpen(true); + + timeIds.push(finalTimeoutId); + dispatch(setTimeIdStack(timeIds)); + }, + [dispatch, stackState.timeStep] + ); + + // Función para ejecutar animaciones de manera robusta + const executeAnimation = useCallback( + (action, value, position = null) => { + if (isAnimating) { + showError('Espera a que termine la animación actual'); + return; + } + + try { + // Cancelar cualquier animación en curso + dispatch(restoreTimeId()); + dispatch(setPlayingStack(false)); + + // Preparar el payload de la acción + const actionPayload = { action, value }; + if (position !== null) { + actionPayload.position = position; + } + + // Guardar estado actual y despachar acción + dispatch(setHead(stackState.head)); + dispatch(actionButton(actionPayload)); + dispatch(restoreRepeatStack()); + + // Marcar que se va a ejecutar una animación automática + animationExecutedRef.current = false; + currentFunActionRef.current = action; + } catch (error) { + console.error('Error executing animation:', error); + showError('Error al ejecutar la animación'); + setIsAnimating(false); + } + }, + [dispatch, stackState.head, isAnimating, showError] + ); + + // Efecto para manejar la ejecución automática de animaciones - ARREGLADO + useEffect(() => { + // Solo ejecutar si: + // 1. Hay un stepHistory válido + // 2. Hay una acción activa + // 3. No se está reproduciendo manualmente + // 4. No se ha ejecutado ya la animación para esta acción + // 5. La acción actual coincide con la que se acaba de crear + if ( + stackState.stepHistory && + stackState.stepHistory.length > 0 && + stackState.funAction && + stackState.funAction === currentFunActionRef.current && + !stackState.playing && + !animationExecutedRef.current + ) { + // Marcar que se va a ejecutar la animación + animationExecutedRef.current = true; + // Ejecutar desde el principio con el flag correspondiente + runAnimation(stackState.stepHistory, true); } - }; + }, [stackState.stepHistory, stackState.funAction, runAnimation]); + // Limpiar refs cuando cambia la acción + useEffect(() => { + if (stackState.funAction !== currentFunActionRef.current) { + animationExecutedRef.current = false; + currentFunActionRef.current = stackState.funAction; + } + }, [stackState.funAction]); + + // Handlers para operaciones de pila const handlePushButton = () => { - run(stackState.stepHistory); + const numValue = value === '' ? null : parseInt(value); + + if (numValue === null) { + showError('Ingresa un valor válido'); + return; + } + + if (stackState.elementos && stackState.elementos.length >= 7) { + showError('La pila está llena (máximo 7 elementos)'); + return; + } + + if (stackState.elementos && stackState.elementos.includes(numValue)) { + showError('El elemento ya existe en la pila'); + return; + } + + executeAnimation('push', numValue); + registrarClick('Push', `Insertar ${numValue} en la pila`); setValue(''); }; - const keepPlaying = () => { - const stepHistory = stackState.stepHistory.slice(stackState.history); - return run(stepHistory); + const handlePopButton = () => { + if (!stackState.head) { + showError('La pila está vacía'); + return; + } + + const topValue = stackState.head.getValue(); + executeAnimation('pop', topValue); + registrarClick('Pop', `Eliminar ${topValue} de la pila`); + }; + + const handleSumergirButton = () => { + if (!stackState.head) { + showError('La pila está vacía'); + return; + } + + if (!stackState.head.getNext()) { + showError('La pila necesita al menos 2 elementos para sumergir'); + return; + } + + const topValue = stackState.head.getValue(); + executeAnimation('sumergir', topValue); + registrarClick('Sumergir', `Sumergir ${topValue} en la pila`); + }; + + const handleInsertarButton = () => { + const numValue = value === '' ? null : parseInt(value); + const position = insertPosition === '' ? null : parseInt(insertPosition); + + if (numValue === null) { + showError('Ingresa un valor válido para insertar'); + return; + } + + if (position === null || position < 0) { + showError('Ingresa una posición válida (≥ 0)'); + return; + } + + if (stackState.elementos && stackState.elementos.includes(numValue)) { + showError('El elemento ya existe en la pila'); + return; + } + + if (stackState.elementos && stackState.elementos.length >= 7) { + showError('La pila está llena (máximo 7 elementos)'); + return; + } + + const maxPosition = stackState.elementos ? stackState.elementos.length : 0; + if (position > maxPosition) { + showError(`La posición máxima válida es ${maxPosition}`); + return; + } + + executeAnimation('insertar', numValue, position); + registrarClick('Insertar', `Insertar ${numValue} en posición ${position}`); + setValue(''); + setInsertPosition(''); + }; + + const handleExtraerButton = () => { + if (!stackState.head) { + showError('La pila está vacía'); + return; + } + + const position = extractPosition === '' ? null : parseInt(extractPosition); + + if (position === null || position < 0) { + showError('Ingresa una posición válida (≥ 0)'); + return; + } + + const maxPosition = stackState.elementos + ? stackState.elementos.length - 1 + : 0; + if (position > maxPosition) { + showError(`La posición máxima válida es ${maxPosition}`); + return; + } + + executeAnimation('extraer', null, position); + registrarClick('Extraer', `Extraer elemento en posición ${position}`); + setExtractPosition(''); }; + // Controles de reproducción manual - ARREGLADO const onPlayPause = e => { e.preventDefault(); if (stackState.playing) { @@ -78,42 +316,29 @@ export const StackControls = () => { }; const play = () => { - dispatch(setPlayingStack(true)); - if (stackState.history === -1) { - run(stackState.stepHistory); - } else { - keepPlaying(); + if (stackState.stepHistory && stackState.stepHistory.length > 0) { + const currentHistory = stackState.history || -1; + const remainingSteps = stackState.stepHistory.slice( + Math.max(0, currentHistory + 1) + ); + + if (remainingSteps.length > 0) { + // Marcar que esta es una reproducción manual + animationExecutedRef.current = true; + runAnimation(remainingSteps, false); // No empezar desde el principio + } else { + // Reiniciar desde el principio + dispatch(restoreRepeatStack()); + animationExecutedRef.current = true; + runAnimation(stackState.stepHistory, true); // Empezar desde el principio + } } }; const pause = () => { dispatch(setPlayingStack(false)); dispatch(restoreTimeId()); - }; - - const run = stepHistory => { - const timeId = []; - stepHistory.forEach((item, i) => { - let timeoutId = setTimeout( - () => { - dispatch(incrementHistoryStack()); - dispatch(updateVisualizationStack(item)); - }, - i * (250 / stackState.timeStep) - ); - - timeId.push(timeoutId); - }); - - let timeoutId = setTimeout( - () => { - dispatch(restoreTimeId()); - }, - stepHistory.length * (250 / stackState.timeStep) - ); - - timeId.push(timeoutId); - dispatch(setTimeIdStack(timeId)); + setIsAnimating(false); }; const goBackward = () => { @@ -121,12 +346,11 @@ export const StackControls = () => { pause(); } - const stepHistory = stackState.stepHistory; - const current = stackState.history; - if (current > 0) { - const item = stepHistory[current - 1]; + const currentHistory = stackState.history || -1; + if (currentHistory > 0 && stackState.stepHistory) { + const previousStep = stackState.stepHistory[currentHistory - 1]; dispatch(decrementHistoryStack()); - dispatch(updateVisualizationStack(item)); + dispatch(updateVisualizationStack(previousStep)); } }; @@ -135,117 +359,229 @@ export const StackControls = () => { pause(); } - const stepHistory = stackState.stepHistory; - const current = stackState.history; - if (current < stepHistory.length - 1) { - const item = stepHistory[current + 1]; + const currentHistory = stackState.history || -1; + if ( + stackState.stepHistory && + currentHistory < stackState.stepHistory.length - 1 + ) { + const nextStep = stackState.stepHistory[currentHistory + 1]; dispatch(incrementHistoryStack()); - dispatch(updateVisualizationStack(item)); + dispatch(updateVisualizationStack(nextStep)); } }; const repeat = () => { - dispatch(restoreTimeId()); - dispatch(restoreRepeatStack()); - dispatch(setPlayingStack(true)); - run(stackState.stepHistory); - }; - - const handlePopButton = () => { - if (stackState.playing) { - pause(); - } - - dispatch(setHead(stackState.head)); - - if (stackState.head) { - dispatch( - actionButton({ - action: 'pop', - value: stackState.head.getValue(), - }) - ); - } else { - setToast('La pila está vacía'); - setOpen(true); + if (stackState.stepHistory && stackState.stepHistory.length > 0) { + dispatch(restoreTimeId()); + dispatch(restoreRepeatStack()); + // Marcar que esta es una reproducción manual + animationExecutedRef.current = true; + runAnimation(stackState.stepHistory, true); // Empezar desde el principio } }; - const [svg, setSvg] = useState(svgPlay); - + // Limpiar timeouts al desmontar el componente useEffect(() => { - if (stackState.playing) { - setSvg(svgPause); - } else { - setSvg(svgPlay); - } - }, [stackState.playing]); + return () => { + dispatch(restoreTimeId()); + }; + }, [dispatch]); + + // Determinar el icono de play/pause + const playPauseIcon = stackState.playing ? svgPause : svgPlay; return ( -
-
- - {svgBack} +
+ {/* Controles de reproducción */} +
+ + {svgBack} - onPlayPause(e)}> - {svg} + + {playPauseIcon} - - {svgForward} + = stackState.stepHistory.length - 1 + } + title="Avanzar" + > + {svgForward} - - {svgRepeat} + + {svgRepeat}
-
+ + {/* Controles Push/Pop */} +
+ {/* Controles para Insertar */} +
+ + +
+ + {/* Controles para Extraer */} +
+ 0 + ? stackState.elementos.length - 1 + : 0, + }} + /> + +
+ + {/* Control Sumergir */} +
+ +
+ + {/* Notificaciones */} { - setOpen(false); - }} + autoHideDuration={4000} + onClose={() => setOpen(false)} anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} > { - setOpen(false); - }} + onClose={() => setOpen(false)} severity="error" sx={{ width: '100%' }} > diff --git a/src/animations/views/AnimationView.jsx b/src/animations/views/AnimationView.jsx index 1853129..7a7a1e1 100644 --- a/src/animations/views/AnimationView.jsx +++ b/src/animations/views/AnimationView.jsx @@ -22,23 +22,47 @@ import { } from '@mui/material'; import { BasicButton } from '../../components'; import { StackChart, StackCode, StackControls } from '../stack'; +import { QueueChart, QueueCode, QueueControls } from '../queue'; +import { + SimpleListChart, + SimpleListControls, + SimpleListCode, +} from '../simpleList/components'; +import { + DoubleListChart, + DoubleListControls, + DoubleListCode, +} from '../doubleList/components'; +import { + LinearSearchChart, + LinearSearchControls, + LinearSearchCode, +} from '../linearSearch'; +import { + BinarySearchChart, + BinarySearchControls, + BinarySearchCode, +} from '../binarySearch'; import { useTranslation } from 'react-i18next'; +import usePageActivity from '../../hooks/usePageActivity'; export const AnimationView = () => { const { t } = useTranslation(); const { animacion } = useParams(); const dispatch = useDispatch(); const navigate = useNavigate(); + usePageActivity('tema_abierto', animacion); + + // Determina si es un algoritmo de ordenamiento const algorithm = animacion.charAt(0).toUpperCase() + animacion.slice(1) + ' Sort'; + const sort = getSortingAlgorithm(algorithm); const { generatedArray, currentQuestion } = useSelector(state => state.quiz); const sortState = useSelector(state => state.sorts); const [modalIsOpen, setIsOpen] = useState(false); - const sort = getSortingAlgorithm(algorithm); - useEffect(() => { if (sort) { dispatch(setearAlgoritmo(algorithm)); @@ -47,7 +71,6 @@ export const AnimationView = () => { array: generatedArray, history: currentQuestion.stepToHistory, }; - dispatch(setGeneratedArray(payload)); setIsOpen(true); } else { @@ -57,41 +80,100 @@ export const AnimationView = () => { } }, [animacion]); + // Determinar la clase para el contenedor principal const classNameChart = - 'flex-grow mb-4 w-full items-center justify-center ' + - (animacion === 'stack' ? 'flex' : ''); + 'flex-grow mb-4 w-full max-w-full overflow-hidden items-center justify-center ' + + ([ + 'stack', + 'queue', + 'simpleList', + 'doubleList', + 'linearSearch', + 'binarySearch', + ].includes(animacion) + ? 'flex' + : ''); + + // Obtener el título según el tipo de animación + const getTitulo = () => { + if (sort) return algorithm; + switch (animacion) { + case 'stack': + return 'Pilas'; + case 'queue': + return 'Colas'; + case 'simpleList': + return 'Listas Simples'; + case 'doubleList': + return 'Listas Dobles'; + case 'linearSearch': + return 'Búsqueda Lineal'; + case 'binarySearch': + return 'Búsqueda Binaria'; + default: + return animacion; + } + }; + + // Mapeo de animación a nombre de quiz + const getQuizNameFromAnimation = () => { + const quizMapping = { + stack: 'stacks', + queue: 'queues', + binarySearch: 'binarySearch', + bubble: 'bubble', + quick: 'quick', + shell: 'shell', + insertion: 'insertion', + }; + return quizMapping[animacion] || animacion; + }; + return ( -
+
- {animacion === 'stack' ? : ''} - {sort ? : ''} + {animacion === 'stack' && }{' '} + {animacion === 'queue' && }{' '} + {animacion === 'simpleList' && } + {animacion === 'doubleList' && } + {animacion === 'linearSearch' && } + {animacion === 'binarySearch' && } + {sort && }
-
-
- {animacion === 'stack' ? : ''} - {sort ? : ''} +
+
+ {animacion === 'stack' && }{' '} + {animacion === 'queue' && }{' '} + {animacion === 'simpleList' && } + {animacion === 'doubleList' && } + {animacion === 'linearSearch' && } + {animacion === 'binarySearch' && } + {sort && }
-
- {animacion === 'stack' ? : ''} - {sort ? : ''} +
+ {animacion === 'stack' && }{' '} + {animacion === 'queue' && }{' '} + {animacion === 'simpleList' && } + {animacion === 'doubleList' && } + {animacion === 'linearSearch' && } + {animacion === 'binarySearch' && } + {sort && }
setIsOpen(false)}> {'Feedback'} - {currentQuestion.feedback} - setIsOpen(false)}> {t('close')} diff --git a/src/assets/svg/SvgConstans.jsx b/src/assets/svg/SvgConstans.jsx index 21a0b1e..571ad26 100644 --- a/src/assets/svg/SvgConstans.jsx +++ b/src/assets/svg/SvgConstans.jsx @@ -166,10 +166,24 @@ export const svgLinkedList = ( ); +export const svgLinkedListDataStructure = ( + + + +); + export const svgBinaryTrees = ( @@ -183,8 +197,8 @@ export const svgBinaryTrees = ( export const svgPiles = ( @@ -217,8 +231,8 @@ export const svgPilesHome = ( export const svgQueue = ( @@ -635,3 +649,112 @@ export const svgMenu = ( ); + +export const svgSimpleList = ( + + + {/* Nodos */} + + + + + {/* Flechas */} + + + + + + +); + +export const svgDoubleList = ( + + + {/* Nodos */} + + + + + {/* Flechas hacia adelante (arriba) */} + + + + + + {/* Flechas hacia atrás (abajo) */} + + + + + + +); + +export const svgLinearSearch = ( + + + {/* Arreglo de cajas */} + + + + + + {/* Flecha de búsqueda */} + + + + {/* Puntos indicando recorrido */} + + + + + +); + +export const svgBinarySearch = ( + + + {/* Arreglo de cajas */} + + + + + + + {/* Indicadores L, M, R */} + + L + + + M + + + R + + + {/* Flecha en el medio */} + + + + +); diff --git a/src/components/Auth/LoginButton.jsx b/src/components/Auth/LoginButton.jsx new file mode 100644 index 0000000..1532f39 --- /dev/null +++ b/src/components/Auth/LoginButton.jsx @@ -0,0 +1,83 @@ +// LoginButton — triggers Google sign-in popup +import { useState } from 'react'; +import { Button, CircularProgress } from '@mui/material'; +import { loginWithGoogle } from '../../services/authService'; +import { saveUserData } from '../../services/firestoreService'; + +const LoginButton = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleLogin = async () => { + setLoading(true); + setError(null); + try { + const user = await loginWithGoogle(); + await saveUserData(user); + } catch (err) { + // Ignore popup-closed-by-user errors silently + if (err.code !== 'auth/popup-closed-by-user') { + setError('No se pudo iniciar sesión. Intenta de nuevo.'); + console.error('Login error:', err); + } + } finally { + setLoading(false); + } + }; + + return ( +
+ + + {error && ( + + {error} + + )} +
+ ); +}; + +export default LoginButton; diff --git a/src/components/Auth/UserProfile.jsx b/src/components/Auth/UserProfile.jsx new file mode 100644 index 0000000..7e10f7a --- /dev/null +++ b/src/components/Auth/UserProfile.jsx @@ -0,0 +1,115 @@ +// UserProfile — displays avatar + name and a logout button +import { useState } from 'react'; +import { + Avatar, + Box, + Typography, + IconButton, + Tooltip, + Menu, + MenuItem, + ListItemIcon, + Divider, + CircularProgress, +} from '@mui/material'; +import LogoutIcon from '@mui/icons-material/Logout'; +import { logout } from '../../services/authService'; +import PropTypes from 'prop-types'; + +const UserProfile = ({ user }) => { + const [anchorEl, setAnchorEl] = useState(null); + const [loggingOut, setLoggingOut] = useState(false); + + const open = Boolean(anchorEl); + + const handleAvatarClick = e => setAnchorEl(e.currentTarget); + const handleClose = () => setAnchorEl(null); + + const handleLogout = async () => { + setLoggingOut(true); + try { + await logout(); + } catch (err) { + console.error('Logout error:', err); + } finally { + setLoggingOut(false); + handleClose(); + } + }; + + // Show only first name to save navbar space + const firstName = user.displayName?.split(' ')[0] ?? 'Usuario'; + + return ( + + + {firstName} + + + + + + + + + + + + {user.displayName} + + + {user.email} + + + + + + + + {loggingOut ? ( + + ) : ( + + )} + + Cerrar sesión + + + + ); +}; + +UserProfile.propTypes = { + user: PropTypes.shape({ + displayName: PropTypes.string, + email: PropTypes.string, + photoURL: PropTypes.string, + }).isRequired, +}; + +export default UserProfile; diff --git a/src/components/BasicButton.jsx b/src/components/BasicButton.jsx index 148afd7..b5505c6 100644 --- a/src/components/BasicButton.jsx +++ b/src/components/BasicButton.jsx @@ -4,24 +4,32 @@ import PropTypes from 'prop-types'; export const BasicButton = ({ children, onClick = () => {}, - backgroundColor = 'gray.main', + backgroundColor = null, disabled = false, ariaLabel = '', + size = 'medium', + className = '', + sx = {}, }) => { + const isSmall = size === 'small'; + return ( + {/* Auth area — desktop */} +
+ {loading ? ( + + ) : user ? ( + + ) : ( + + )} +
); }; diff --git a/src/quiz/components/AnswersGrid.jsx b/src/quiz/components/AnswersGrid.jsx index 827fb3f..9bc3055 100644 --- a/src/quiz/components/AnswersGrid.jsx +++ b/src/quiz/components/AnswersGrid.jsx @@ -1,11 +1,7 @@ import PropTypes from 'prop-types'; export const AnswersGrid = ({ children }) => { - return ( -
- {children} -
- ); + return
{children}
; }; AnswersGrid.propTypes = { diff --git a/src/quiz/components/CorrectAnswerDialog.jsx b/src/quiz/components/CorrectAnswerDialog.jsx index d5f5c00..103f7ab 100644 --- a/src/quiz/components/CorrectAnswerDialog.jsx +++ b/src/quiz/components/CorrectAnswerDialog.jsx @@ -3,8 +3,15 @@ import { useTranslation } from 'react-i18next'; export const CorrectAnswerDialog = () => { const { t } = useTranslation(); return ( -
- {t('correctAnswer')} +
+
+
+ ✓ +
+
+
{t('correctAnswer')}
+
+
); }; diff --git a/src/quiz/components/QuizQuestion.jsx b/src/quiz/components/QuizQuestion.jsx index 61566c6..8b0792b 100644 --- a/src/quiz/components/QuizQuestion.jsx +++ b/src/quiz/components/QuizQuestion.jsx @@ -1,7 +1,13 @@ import PropTypes from 'prop-types'; export const QuizQuestion = ({ question }) => { - return {question}; + return ( +
+

+ {question} +

+
+ ); }; QuizQuestion.propTypes = { diff --git a/src/quiz/components/QuizStatement.jsx b/src/quiz/components/QuizStatement.jsx index 333d295..387019c 100644 --- a/src/quiz/components/QuizStatement.jsx +++ b/src/quiz/components/QuizStatement.jsx @@ -1,7 +1,13 @@ import PropTypes from 'prop-types'; export const QuizStatement = ({ statement }) => { - return {statement}; + return ( +
+

+ {statement} +

+
+ ); }; QuizStatement.propTypes = { diff --git a/src/quiz/components/WrongAnswerDialog.jsx b/src/quiz/components/WrongAnswerDialog.jsx index 4d02ad4..106eda3 100644 --- a/src/quiz/components/WrongAnswerDialog.jsx +++ b/src/quiz/components/WrongAnswerDialog.jsx @@ -1,10 +1,63 @@ import { useTranslation } from 'react-i18next'; +import PropTypes from 'prop-types'; -export const WrongAnswerDialog = () => { +export const WrongAnswerDialog = ({ + correctAnswer, + correctOption, + feedback, +}) => { const { t } = useTranslation(); + return ( -
- {t('wrongAnswer')} +
+ {/* Header - Respuesta Incorrecta */} +
+
+
+ ✕ +
+
+
{t('wrongAnswer')}
+
+
+
+ + {/* Respuesta Correcta */} + {correctAnswer && correctOption && ( +
+
+
+ {t('correctOption')} +
+
+ Opción {correctAnswer} +
+
+

+ {correctOption} +

+
+ )} + + {/* Explicación */} + {feedback && ( +
+
+
+ {t('explanation')} +
+
+

+ {feedback} +

+
+ )}
); }; + +WrongAnswerDialog.propTypes = { + correctAnswer: PropTypes.string, + correctOption: PropTypes.string, + feedback: PropTypes.string, +}; diff --git a/src/quiz/data/quizzes.js b/src/quiz/data/quizzes.js index 6c1c92b..9db303c 100644 --- a/src/quiz/data/quizzes.js +++ b/src/quiz/data/quizzes.js @@ -882,144 +882,268 @@ export const sortingMethodsQuiz = { ], }; -export const searchMethodsQuiz = { - name: 'Métodos de Búsqueda', +export const linearSearchQuiz = { + name: 'Búsqueda Lineal', questions: [ { - statement: - '¿Cuál de los siguientes métodos de búsqueda es más eficiente en un conjunto de datos ordenado?', - question: - 'En un conjunto de datos ordenado, ¿cuál método de búsqueda es más eficiente?', + statement: '¿Cuál es la definición correcta de búsqueda lineal?', + question: '¿Qué es la búsqueda lineal?', options: { - A: 'Búsqueda lineal', - B: 'Búsqueda binaria', - C: 'Búsqueda en árbol', - D: 'Búsqueda hash', + A: 'Un algoritmo que divide el conjunto de datos a la mitad en cada paso.', + B: 'Un algoritmo que recorre secuencialmente cada elemento hasta encontrar el objetivo.', + C: 'Un algoritmo que utiliza una función hash para encontrar elementos.', + D: 'Un algoritmo que busca en un árbol binario.', }, answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'La búsqueda binaria es más eficiente en conjuntos de datos ordenados, ya que reduce el espacio de búsqueda a la mitad en cada iteración.', + 'La búsqueda lineal es un algoritmo que recorre secuencialmente cada elemento del conjunto de datos hasta encontrar el objetivo o llegar al final.', }, { statement: - '¿Cuál de los siguientes métodos de búsqueda es más adecuado para conjuntos de datos pequeños y no ordenados?', + '¿Cuál es la complejidad temporal de la búsqueda lineal en el peor caso?', question: - 'En conjuntos de datos pequeños y no ordenados, ¿cuál método de búsqueda es más adecuado?', + '¿Cuál es la complejidad de tiempo de búsqueda lineal en el peor caso?', options: { - A: 'Búsqueda lineal', - B: 'Búsqueda binaria', - C: 'Búsqueda en árbol', - D: 'Búsqueda hash', + A: 'O(1)', + B: 'O(log n)', + C: 'O(n)', + D: 'O(n²)', }, - answer: 'A', + answer: 'C', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'La búsqueda lineal es más adecuada para conjuntos de datos pequeños y no ordenados, ya que implica revisar cada elemento uno por uno.', + 'En el peor caso, la búsqueda lineal tiene una complejidad temporal de O(n), ya que puede necesitar revisar todos los elementos del conjunto de datos.', }, { statement: - '¿Qué método de búsqueda se beneficia más de conjuntos de datos ordenados en términos de tiempo?', - question: - 'Entre estos métodos de búsqueda, ¿cuál se beneficia más de conjuntos de datos ordenados en términos de tiempo?', + '¿En qué tipo de conjunto de datos es más eficiente usar búsqueda lineal?', + question: '¿Cuándo es más apropiado usar búsqueda lineal?', + options: { + A: 'En conjuntos de datos grandes y ordenados.', + B: 'En conjuntos de datos pequeños o no ordenados.', + C: 'En conjuntos de datos con millones de elementos.', + D: 'Solo en estructuras de árbol binario.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'La búsqueda lineal es más eficiente para conjuntos de datos pequeños o cuando los datos no están ordenados, ya que no podemos hacer suposiciones sobre la disposición de los elementos.', + }, + { + statement: '¿Cuál es la principal ventaja de la búsqueda lineal?', + question: '¿Cuál es la ventaja principal de la búsqueda lineal?', options: { - A: 'Búsqueda lineal', - B: 'Búsqueda binaria', - C: 'Búsqueda en árbol', - D: 'Búsqueda hash', + A: 'Es muy rápida.', + B: 'Funciona en conjuntos de datos no ordenados.', + C: 'Usa menos memoria que otros métodos.', + D: 'Siempre tiene complejidad O(1).', }, answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'La búsqueda binaria se beneficia enormemente de conjuntos de datos ordenados, ya que reduce la cantidad de elementos a considerar en cada paso.', + 'La principal ventaja de la búsqueda lineal es que funciona en cualquier conjunto de datos, ordenado o no ordenado, sin necesidad de preprocesamiento.', + }, + { + statement: '¿Cuál es la principal desventaja de la búsqueda lineal?', + question: '¿Cuál es la desventaja principal de la búsqueda lineal?', + options: { + A: 'No es estable.', + B: 'Usa mucha memoria.', + C: 'Es lenta en conjuntos de datos grandes.', + D: 'No funciona en arrays.', + }, + answer: 'C', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'La principal desventaja de la búsqueda lineal es su lentitud en conjuntos de datos grandes, ya que puede requerir revisar todos los elementos en el peor caso.', }, { statement: - '¿Cuál de los siguientes métodos de búsqueda es más eficiente en términos de tiempo en conjuntos de datos grandes?', - question: - 'En conjuntos de datos grandes, ¿cuál método de búsqueda es más eficiente en términos de tiempo?', + '¿Es necesario que los datos estén ordenados para usar búsqueda lineal?', + question: '¿Los datos deben estar ordenados para búsqueda lineal?', options: { - A: 'Búsqueda lineal', - B: 'Búsqueda binaria', - C: 'Búsqueda en árbol', - D: 'Búsqueda hash', + A: 'Sí, siempre.', + B: 'No, nunca.', + C: 'Solo si hay más de 1000 elementos.', + D: 'Depende del lenguaje de programación.', }, - answer: 'D', + answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'La búsqueda hash es más eficiente en términos de tiempo en conjuntos de datos grandes, ya que permite el acceso directo a los elementos utilizando una función de hash.', + 'No es necesario que los datos estén ordenados para usar búsqueda lineal. El algoritmo funciona en cualquier conjunto de datos, ordenado o no.', }, { statement: - '¿En qué tipo de estructura de datos es común realizar la búsqueda en profundidad (DFS) y la búsqueda en amplitud (BFS)?', + '¿Cuál es el mejor caso de complejidad temporal para la búsqueda lineal?', question: - '¿En qué tipo de estructura de datos es común realizar tanto la búsqueda en profundidad (DFS) como la búsqueda en amplitud (BFS)?', + '¿Cuál es el mejor caso de complejidad temporal para búsqueda lineal?', options: { - A: 'Listas enlazadas', - B: 'Árboles binarios', - C: 'Colas', - D: 'Grafos', + A: 'O(1)', + B: 'O(log n)', + C: 'O(n)', + D: 'O(n²)', }, - answer: 'D', + answer: 'A', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'La búsqueda en profundidad (DFS) y la búsqueda en amplitud (BFS) son comunes en la exploración de grafos para encontrar caminos y conexiones entre nodos.', + 'El mejor caso de complejidad temporal para la búsqueda lineal es O(1), que ocurre cuando el elemento buscado es el primero del conjunto de datos.', }, { statement: - '¿En qué tipo de estructura de datos es útil implementar una tabla hash para la búsqueda eficiente?', + '¿Cuántas comparaciones se necesitan en promedio para encontrar un elemento usando búsqueda lineal?', + question: '¿Cuántas comparaciones en promedio para búsqueda lineal?', + options: { + A: '1 comparación', + B: 'n/2 comparaciones', + C: 'log n comparaciones', + D: 'n comparaciones', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'En promedio, la búsqueda lineal requiere n/2 comparaciones, ya que en el caso promedio el elemento se encuentra aproximadamente en la mitad del conjunto de datos.', + }, + ], +}; + +export const binarySearchQuiz = { + name: 'Búsqueda Binaria', + questions: [ + { + statement: '¿Cuál es la definición correcta de búsqueda binaria?', + question: '¿Qué es la búsqueda binaria?', + options: { + A: 'Un algoritmo que recorre secuencialmente cada elemento.', + B: 'Un algoritmo que divide el conjunto ordenado a la mitad en cada paso.', + C: 'Un algoritmo que utiliza una función hash.', + D: 'Un algoritmo que busca en un árbol binario.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'La búsqueda binaria es un algoritmo eficiente que divide el conjunto de datos ordenado a la mitad en cada paso, eliminando la mitad donde el objetivo no puede estar.', + }, + { + statement: + '¿Cuál es la complejidad temporal de la búsqueda binaria en el peor caso?', question: - '¿En qué tipo de estructura de datos es útil implementar una tabla hash para la búsqueda eficiente?', + '¿Cuál es la complejidad de tiempo de búsqueda binaria en el peor caso?', options: { - A: 'Listas enlazadas', - B: 'Árboles binarios', - C: 'Colas', - D: 'Arrays', + A: 'O(1)', + B: 'O(log n)', + C: 'O(n)', + D: 'O(n²)', }, - answer: 'D', + answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'Las tablas hash son particularmente útiles para la búsqueda eficiente en arrays o arreglos, ya que permiten acceso directo a través de una función de hash.', + 'La complejidad temporal de la búsqueda binaria en el peor caso es O(log n), ya que en cada iteración divide el espacio de búsqueda por la mitad.', + }, + { + statement: '¿Cuál es el requisito principal para usar búsqueda binaria?', + question: '¿Cuál es el requisito previo para búsqueda binaria?', + options: { + A: 'Los datos deben estar en un array.', + B: 'Los datos deben estar ordenados.', + C: 'Los datos deben ser números.', + D: 'Los datos deben ser únicos.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'El requisito principal para usar búsqueda binaria es que los datos deben estar ordenados, de lo contrario el algoritmo no funcionará correctamente.', }, { statement: '¿Cuál es la principal ventaja de la búsqueda binaria sobre la búsqueda lineal?', question: - '¿Cuál es la principal ventaja de la búsqueda binaria en comparación con la búsqueda lineal?', + '¿Cuál es la ventaja de búsqueda binaria frente a búsqueda lineal?', options: { A: 'Es más simple de implementar.', - B: 'Es más rápida en todo tipo de conjuntos de datos.', - C: 'Es más precisa.', - D: 'Requiere menos memoria.', + B: 'Es significativamente más rápida en conjuntos grandes.', + C: 'Funciona con datos no ordenados.', + D: 'Usa menos memoria.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'La principal ventaja de la búsqueda binaria es que es significativamente más rápida en conjuntos de datos grandes, con una complejidad de O(log n) frente a O(n) de la búsqueda lineal.', + }, + { + statement: '¿Cuál es la desventaja principal de la búsqueda binaria?', + question: '¿Cuál es la desventaja de la búsqueda binaria?', + options: { + A: 'Es lenta.', + B: 'Requiere que los datos estén ordenados.', + C: 'No funciona con arrays.', + D: 'Usa mucha memoria.', }, answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'La principal ventaja de la búsqueda binaria es su velocidad, ya que reduce significativamente el número de comparaciones necesarias, especialmente en conjuntos de datos grandes.', + 'La principal desventaja es que requiere que los datos estén previamente ordenados, lo cual puede ser costoso computacionalmente si hay que ordenar primero.', }, { statement: - '¿Cuál de los siguientes métodos de búsqueda es especialmente eficiente para conjuntos de datos dispersos?', - question: - '¿Cuál de estos métodos de búsqueda es especialmente eficiente para conjuntos de datos dispersos?', + '¿Cómo se llama el punto central en el algoritmo de búsqueda binaria?', + question: '¿Cómo se llama el punto central en búsqueda binaria?', options: { - A: 'Búsqueda lineal', - B: 'Búsqueda binaria', - C: 'Búsqueda en árbol', - D: 'Búsqueda hash', + A: 'Pivot', + B: 'Middle', + C: 'Centro', + D: 'Mitad', }, - answer: 'D', + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'En la búsqueda binaria, el punto central se suele llamar "middle" (del inglés) o punto medio, que es el elemento usado para dividir el conjunto de datos.', + }, + { + statement: + '¿Cuántas veces máximo se divide el conjunto de datos en una búsqueda binaria de 1 millón de elementos?', + question: '¿Cuántas divisiones máximo para 1 millón de elementos?', + options: { + A: 'Aproximadamente 10 divisiones', + B: 'Aproximadamente 20 divisiones', + C: 'Aproximadamente 100 divisiones', + D: 'Aproximadamente 1000 divisiones', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'Para 1 millón de elementos, la búsqueda binaria requiere un máximo de aproximadamente 20 divisiones (log₂(1,000,000) ≈ 20), demostrando su eficiencia.', + }, + { + statement: + '¿Es la búsqueda binaria siempre mejor que la búsqueda lineal?', + question: '¿Es búsqueda binaria siempre mejor?', + options: { + A: 'Sí, en todos los casos.', + B: 'No, en conjuntos pequeños la búsqueda lineal puede ser comparativa.', + C: 'No, la búsqueda lineal es siempre mejor.', + D: 'Depende del lenguaje de programación.', + }, + answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'La búsqueda hash es especialmente eficiente para conjuntos de datos dispersos o con muchos valores faltantes, ya que puede mapear rápidamente claves a valores.', + 'No es siempre mejor. En conjuntos de datos muy pequeños, la búsqueda lineal puede ser tan rápida o más debido al overhead de la búsqueda binaria, y además requiere datos ordenados.', }, ], }; @@ -1422,3 +1546,145 @@ export const queueQuiz = { }, ], }; + +export const simpleListQuiz = { + name: 'Listas Simples', + questions: [ + { + statement: + '¿Cuál es la definición correcta de una lista simple enlazada?', + question: + 'En el contexto de estructuras de datos, ¿qué es una lista simple?', + options: { + A: 'Una estructura lineal de datos donde cada nodo almacena un valor y una referencia al siguiente nodo.', + B: 'Una estructura que solo puede almacenar números enteros.', + C: 'Una estructura de datos que almacena elementos en orden aleatorio.', + D: 'Una estructura de árbol binario con múltiples ramificaciones.', + }, + answer: 'A', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'Una lista simple enlazada es una estructura lineal donde cada nodo contiene un valor y una referencia (puntero) al siguiente nodo en la secuencia.', + }, + { + statement: + '¿Cuál es la principal diferencia entre una lista simple y una lista doblemente enlazada?', + question: + '¿Qué diferencia existe entre una lista simple y una lista doblemente enlazada?', + options: { + A: 'La lista simple es más rápida.', + B: 'La lista simple tiene referencias solo al siguiente nodo, mientras que la doblemente enlazada tiene referencias al anterior y siguiente.', + C: 'La lista doblemente enlazada ocupa menos memoria.', + D: 'Ambas son exactamente iguales en estructura.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'La diferencia principal es que una lista simple solo tiene una referencia al siguiente nodo, mientras que una lista doblemente enlazada tiene referencias tanto al nodo anterior como al siguiente.', + }, + { + statement: + '¿Cuál es la complejidad temporal para insertar un elemento al inicio de una lista simple enlazada?', + question: + '¿Cuál es la complejidad temporal para insertar un elemento al inicio de una lista simple?', + options: { + A: 'O(1)', + B: 'O(n)', + C: 'O(log n)', + D: 'O(n log n)', + }, + answer: 'A', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'La inserción al inicio de una lista simple es O(1) porque solo requiere ajustar el puntero del nuevo nodo al nodo que era la cabeza y actualizar la cabeza.', + }, + { + statement: + '¿Cuál es la complejidad temporal para buscar un elemento en una lista simple enlazada?', + question: + '¿Cuál es la complejidad temporal promedio para buscar en una lista simple enlazada?', + options: { + A: 'O(1)', + B: 'O(n)', + C: 'O(log n)', + D: 'O(n^2)', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'La búsqueda en una lista simple enlazada es O(n) en el caso promedio, porque potencialmente tienes que recorrer toda la lista desde el inicio hasta encontrar el elemento.', + }, + { + statement: + '¿Qué operación es más eficiente en una lista simple comparada con un array?', + question: + '¿En qué operación es más eficiente una lista simple respecto a un array?', + options: { + A: 'En la inserción al inicio.', + B: 'En el acceso aleatorio a elementos.', + C: 'En la búsqueda binaria.', + D: 'En el almacenamiento de datos contiguos.', + }, + answer: 'A', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'La inserción al inicio de una lista simple es mucho más eficiente que en un array. En un array, insertar al inicio requiere desplazar todos los elementos (O(n)), mientras que en una lista simple es O(1).', + }, + { + statement: + '¿Cuál es la ventaja principal de usar una lista simple sobre un array en programación?', + question: + '¿Cuál es la principal ventaja de una lista simple sobre un array?', + options: { + A: 'Acceso más rápido a elementos aleatorios.', + B: 'Mayor flexibilidad en la inserción y eliminación de elementos.', + C: 'Menor uso de memoria en general.', + D: 'Mejor rendimiento en búsquedas.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'La principal ventaja de una lista simple es su flexibilidad para insertar y eliminar elementos en cualquier posición, especialmente en el inicio, lo que es O(1) en comparación con un array.', + }, + { + statement: + '¿Qué sucede cuando intentas acceder a un nodo después de la cola de una lista simple?', + question: + '¿Qué sucede si intentas acceder más allá del último nodo de una lista simple?', + options: { + A: 'Regresa al primer nodo.', + B: 'Accedes a un valor nulo o NULL.', + C: 'Se crea automáticamente un nuevo nodo.', + D: 'Se genera un error de desbordamiento.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'El último nodo de una lista simple tiene una referencia nula (NULL) al siguiente, lo que indica el final de la lista. Intentar acceder más allá resultaría en acceder a NULL.', + }, + { + statement: + '¿Cuál es la mejor estrategia para eliminar un nodo específico de una lista simple?', + question: + '¿Cuál es la estrategia correcta para eliminar un nodo específico de una lista simple?', + options: { + A: 'Buscar el nodo anterior al que se desea eliminar y redirigir su referencia.', + B: 'Simplemente establecer el nodo a NULL.', + C: 'Recrear toda la lista sin ese nodo.', + D: 'Utilizar un algoritmo de búsqueda binaria.', + }, + answer: 'A', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'Para eliminar un nodo de una lista simple, primero debes encontrar el nodo anterior al que deseas eliminar, y luego redirigir su referencia al nodo siguiente del nodo a eliminar.', + }, + ], +}; diff --git a/src/quiz/data/quizzesEn.js b/src/quiz/data/quizzesEn.js index 3f0def8..404128b 100644 --- a/src/quiz/data/quizzesEn.js +++ b/src/quiz/data/quizzesEn.js @@ -865,143 +865,262 @@ export const sortingMethodsQuizEn = { ], }; -export const searchMethodsQuizEn = { - name: 'Search Methods', +export const linearSearchQuizEn = { + name: 'Linear Search', questions: [ { - statement: - 'Which of the following search methods is more efficient on a sorted dataset?', - question: 'On a sorted dataset, which search method is more efficient?', + statement: 'What is the correct definition of linear search?', + question: 'What is linear search?', options: { - A: 'Linear Search', - B: 'Binary Search', - C: 'Tree Search', - D: 'Hash Search', + A: 'An algorithm that halves the dataset at each step.', + B: 'An algorithm that sequentially traverses each element until finding the target.', + C: 'An algorithm that uses a hash function to find elements.', + D: 'An algorithm that searches in a binary tree.', }, answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'Binary Search is more efficient on sorted datasets because it halves the search space with each iteration.', + 'Linear search is an algorithm that sequentially traverses each element of the dataset until finding the target or reaching the end.', }, { statement: - 'Which of the following search methods is more suitable for small and unsorted datasets?', + 'What is the time complexity of linear search in the worst case?', question: - 'On small and unsorted datasets, which search method is more suitable?', + 'What is the time complexity of linear search in the worst case?', + options: { + A: 'O(1)', + B: 'O(log n)', + C: 'O(n)', + D: 'O(n²)', + }, + answer: 'C', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'In the worst case, linear search has a time complexity of O(n) since it may need to check all elements in the dataset.', + }, + { + statement: 'On what type of dataset is linear search most efficient?', + question: 'When is it most appropriate to use linear search?', + options: { + A: 'On large and sorted datasets.', + B: 'On small or unsorted datasets.', + C: 'On datasets with millions of elements.', + D: 'Only on binary tree structures.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'Linear search is most efficient for small datasets or when data is not sorted, as we cannot make assumptions about element arrangement.', + }, + { + statement: 'What is the main advantage of linear search?', + question: 'What is the main advantage of linear search?', + options: { + A: 'It is very fast.', + B: 'It works on unsorted datasets.', + C: 'It uses less memory than other methods.', + D: 'It always has O(1) complexity.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'The main advantage of linear search is that it works on any dataset, sorted or unsorted, without requiring preprocessing.', + }, + { + statement: 'What is the main disadvantage of linear search?', + question: 'What is the main disadvantage of linear search?', options: { - A: 'Linear Search', - B: 'Binary Search', - C: 'Tree Search', - D: 'Hash Search', + A: 'It is not stable.', + B: 'It uses a lot of memory.', + C: 'It is slow on large datasets.', + D: 'It does not work on arrays.', + }, + answer: 'C', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'The main disadvantage of linear search is its slowness on large datasets, as it may require checking all elements in the worst case.', + }, + { + statement: 'Is it necessary for data to be sorted to use linear search?', + question: 'Must data be sorted for linear search?', + options: { + A: 'Yes, always.', + B: 'No, never.', + C: 'Only if there are more than 1000 elements.', + D: 'It depends on the programming language.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'It is not necessary for data to be sorted for linear search. The algorithm works on any dataset, sorted or unsorted.', + }, + { + statement: 'What is the best case time complexity for linear search?', + question: 'What is the best case time complexity for linear search?', + options: { + A: 'O(1)', + B: 'O(log n)', + C: 'O(n)', + D: 'O(n²)', }, answer: 'A', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'Linear Search is more suitable for small and unsorted datasets as it involves checking each element one by one.', + 'The best case time complexity for linear search is O(1), which occurs when the searched element is the first in the dataset.', }, { statement: - 'Which search method benefits the most from sorted datasets in terms of time?', - question: - 'Among these search methods, which benefits the most from sorted datasets in terms of time?', + 'On average, how many comparisons are needed to find an element using linear search?', + question: 'On average, how many comparisons for linear search?', options: { - A: 'Linear Search', - B: 'Binary Search', - C: 'Tree Search', - D: 'Hash Search', + A: '1 comparison', + B: 'n/2 comparisons', + C: 'log n comparisons', + D: 'n comparisons', }, answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'Binary Search greatly benefits from sorted datasets as it reduces the number of elements to consider at each step.', + 'On average, linear search requires n/2 comparisons, as the element is typically found approximately halfway through the dataset.', }, + ], +}; + +export const binarySearchQuizEn = { + name: 'Binary Search', + questions: [ { - statement: - 'Which of the following search methods is more efficient in terms of time on large datasets?', - question: - 'On large datasets, which search method is more efficient in terms of time?', + statement: 'What is the correct definition of binary search?', + question: 'What is binary search?', options: { - A: 'Linear Search', - B: 'Binary Search', - C: 'Tree Search', - D: 'Hash Search', + A: 'An algorithm that sequentially traverses each element.', + B: 'An algorithm that halves the sorted dataset at each step.', + C: 'An algorithm that uses a hash function.', + D: 'An algorithm that searches in a binary tree.', }, - answer: 'D', + answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'Hash Search is more efficient in terms of time on large datasets, as it allows direct access to elements using a hash function.', + 'Binary search is an efficient algorithm that halves the sorted dataset at each step, eliminating the half where the target cannot be.', }, { statement: - 'In what type of data structure is it common to perform Depth-First Search (DFS) and Breadth-First Search (BFS)?', + 'What is the time complexity of binary search in the worst case?', question: - 'In what type of data structure is it common to perform both Depth-First Search (DFS) and Breadth-First Search (BFS)?', + 'What is the time complexity of binary search in the worst case?', options: { - A: 'Linked Lists', - B: 'Binary Trees', - C: 'Queues', - D: 'Graphs', + A: 'O(1)', + B: 'O(log n)', + C: 'O(n)', + D: 'O(n²)', }, - answer: 'D', + answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'Depth-First Search (DFS) and Breadth-First Search (BFS) are common in exploring graphs to find paths and connections between nodes.', + 'The time complexity of binary search in the worst case is O(log n), as it halves the search space at each iteration.', }, { - statement: - 'In what type of data structure is it useful to implement a hash table for efficient searching?', - question: - 'In what type of data structure is it useful to implement a hash table for efficient searching?', + statement: 'What is the main requirement to use binary search?', + question: 'What is the main requirement for binary search?', options: { - A: 'Linked Lists', - B: 'Binary Trees', - C: 'Queues', - D: 'Arrays', + A: 'Data must be in an array.', + B: 'Data must be sorted.', + C: 'Data must be numbers.', + D: 'Data must be unique.', }, - answer: 'D', + answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'Hash tables are particularly useful for efficient searching in arrays, as they allow direct access through a hash function.', + 'The main requirement for binary search is that data must be sorted; otherwise the algorithm will not work correctly.', }, { statement: 'What is the main advantage of binary search over linear search?', - question: - 'What is the main advantage of binary search compared to linear search?', + question: 'What is the advantage of binary search over linear search?', options: { A: 'It is simpler to implement.', - B: 'It is faster across all types of datasets.', - C: 'It is more precise.', - D: 'It requires less memory.', + B: 'It is significantly faster on large datasets.', + C: 'It works with unsorted data.', + D: 'It uses less memory.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'The main advantage of binary search is that it is significantly faster on large datasets, with O(log n) complexity versus O(n) for linear search.', + }, + { + statement: 'What is the main disadvantage of binary search?', + question: 'What is the disadvantage of binary search?', + options: { + A: 'It is slow.', + B: 'It requires data to be sorted.', + C: 'It does not work on arrays.', + D: 'It uses a lot of memory.', }, answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'The main advantage of binary search is its speed, as it significantly reduces the number of comparisons needed, especially on large datasets.', + 'The main disadvantage is that it requires data to be pre-sorted, which can be computationally expensive if sorting is needed first.', }, { statement: - 'Which of the following search methods is particularly efficient for sparse datasets?', - question: - 'Which of these search methods is particularly efficient for sparse datasets?', + 'What is the central point called in the binary search algorithm?', + question: 'What is the central point called in binary search?', options: { - A: 'Linear Search', - B: 'Binary Search', - C: 'Tree Search', - D: 'Hash Search', + A: 'Pivot', + B: 'Middle', + C: 'Center', + D: 'Midpoint', }, - answer: 'D', + answer: 'B', selectedAnswer: null, state: 'UNANSWERED', feedback: - 'Hash Search is especially efficient for sparse datasets or datasets with many missing values, as it can quickly map keys to values.', + 'In binary search, the central point is typically called "middle", which is the element used to divide the dataset.', + }, + { + statement: + 'At most, how many times is the dataset divided in a binary search of 1 million elements?', + question: 'At most, how many divisions for 1 million elements?', + options: { + A: 'Approximately 10 divisions', + B: 'Approximately 20 divisions', + C: 'Approximately 100 divisions', + D: 'Approximately 1000 divisions', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'For 1 million elements, binary search requires at most approximately 20 divisions (log₂(1,000,000) ≈ 20), demonstrating its efficiency.', + }, + { + statement: 'Is binary search always better than linear search?', + question: 'Is binary search always better?', + options: { + A: 'Yes, in all cases.', + B: 'No, on small datasets linear search can be comparable.', + C: 'No, linear search is always better.', + D: 'It depends on the programming language.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'It is not always better. On very small datasets, linear search can be as fast or faster due to binary search overhead, and binary search requires sorted data.', }, ], }; @@ -1397,3 +1516,143 @@ export const queueQuizEn = { }, ], }; + +export const simpleListQuizEn = { + name: 'Simple Lists', + questions: [ + { + statement: + 'What is the correct definition of a simple singly linked list?', + question: 'In the context of data structures, what is a simple list?', + options: { + A: 'A linear data structure where each node stores a value and a reference to the next node.', + B: 'A structure that can only store integers.', + C: 'A data structure that stores elements in random order.', + D: 'A binary tree structure with multiple branches.', + }, + answer: 'A', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'A simple singly linked list is a linear structure where each node contains a value and a reference (pointer) to the next node in the sequence.', + }, + { + statement: + 'What is the main difference between a simple list and a doubly linked list?', + question: + 'What difference exists between a simple list and a doubly linked list?', + options: { + A: 'The simple list is faster.', + B: 'The simple list has references only to the next node, while the doubly linked list has references to both previous and next.', + C: 'The doubly linked list uses less memory.', + D: 'Both are exactly the same in structure.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'The main difference is that a simple list has only one reference to the next node, while a doubly linked list has references to both the previous and next nodes.', + }, + { + statement: + 'What is the time complexity for inserting an element at the beginning of a simple singly linked list?', + question: + 'What is the time complexity for inserting an element at the beginning of a simple list?', + options: { + A: 'O(1)', + B: 'O(n)', + C: 'O(log n)', + D: 'O(n log n)', + }, + answer: 'A', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + "Insertion at the beginning of a simple list is O(1) because it only requires adjusting the new node's pointer to the current head node and updating the head.", + }, + { + statement: + 'What is the time complexity for searching an element in a simple singly linked list?', + question: + 'What is the average time complexity for searching in a simple singly linked list?', + options: { + A: 'O(1)', + B: 'O(n)', + C: 'O(log n)', + D: 'O(n^2)', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'Searching in a simple singly linked list is O(n) on average, because you potentially have to traverse the entire list from the beginning to find the element.', + }, + { + statement: + 'What operation is more efficient in a simple list compared to an array?', + question: + 'In which operation is a simple list more efficient compared to an array?', + options: { + A: 'In insertion at the beginning.', + B: 'In random access to elements.', + C: 'In binary search.', + D: 'In contiguous data storage.', + }, + answer: 'A', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'Insertion at the beginning of a simple list is much more efficient than in an array. In an array, inserting at the beginning requires shifting all elements (O(n)), while in a simple list it is O(1).', + }, + { + statement: + 'What is the main advantage of using a simple list over an array in programming?', + question: 'What is the main advantage of a simple list over an array?', + options: { + A: 'Faster access to random elements.', + B: 'Greater flexibility in inserting and deleting elements.', + C: 'Lower overall memory usage.', + D: 'Better performance in searches.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'The main advantage of a simple list is its flexibility for inserting and deleting elements at any position, especially at the beginning, which is O(1) compared to an array.', + }, + { + statement: + 'What happens when you try to access a node after the tail of a simple list?', + question: + 'What happens if you try to access beyond the last node of a simple list?', + options: { + A: 'It returns to the first node.', + B: 'You access a null or NULL value.', + C: 'A new node is automatically created.', + D: 'An overflow error is generated.', + }, + answer: 'B', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'The last node of a simple list has a null (NULL) reference to the next node, indicating the end of the list. Attempting to access beyond it would result in accessing NULL.', + }, + { + statement: + 'What is the best strategy for deleting a specific node from a simple list?', + question: + 'What is the correct strategy for deleting a specific node from a simple list?', + options: { + A: 'Find the node before the one you want to delete and redirect its reference.', + B: 'Simply set the node to NULL.', + C: 'Recreate the entire list without that node.', + D: 'Use a binary search algorithm.', + }, + answer: 'A', + selectedAnswer: null, + state: 'UNANSWERED', + feedback: + 'To delete a node from a simple list, you must first find the node before the one you want to delete, and then redirect its reference to the next node of the node to be deleted.', + }, + ], +}; diff --git a/src/quiz/helpers/getAnimationNameByQuizName.js b/src/quiz/helpers/getAnimationNameByQuizName.js index 0142d30..4093329 100644 --- a/src/quiz/helpers/getAnimationNameByQuizName.js +++ b/src/quiz/helpers/getAnimationNameByQuizName.js @@ -4,6 +4,11 @@ export const getAnimationNameByQuizName = quizName => { Quicksort: 'quick', ShellSort: 'shell', 'Insertion Sort': 'insertion', + 'Listas Simples': 'simpleList', + Pilas: 'stack', + Colas: 'queue', + 'Búsqueda Lineal': 'linearSearch', + 'Búsqueda Binaria': 'binarySearch', }; return animationNames[quizName]; diff --git a/src/quiz/helpers/getQuizByName.js b/src/quiz/helpers/getQuizByName.js index d8f2b3b..4e276e6 100644 --- a/src/quiz/helpers/getQuizByName.js +++ b/src/quiz/helpers/getQuizByName.js @@ -1,21 +1,25 @@ import { defaultLanguage } from '../../main'; import { + binarySearchQuiz, + binarySearchQuizEn, binaryTreeQuiz, binaryTreeQuizEn, bubbleSortQuiz, bubbleSortQuizEn, insertionSortQuiz, insertionSortQuizEn, + linearSearchQuiz, + linearSearchQuizEn, linkedListQuiz, linkedListQuizEn, queueQuiz, queueQuizEn, quickSortQuiz, quickSortQuizEn, - searchMethodsQuiz, - searchMethodsQuizEn, shellSortQuiz, shellSortQuizEn, + simpleListQuiz, + simpleListQuizEn, sortingFundamentalsQuiz, sortingFundamentalsQuizEn, sortingMethodsQuiz, @@ -34,11 +38,13 @@ export const getQuizByName = name => { quick: quickSortQuiz, shell: shellSortQuiz, insertion: insertionSortQuiz, + linearSearch: linearSearchQuiz, + binarySearch: binarySearchQuiz, linkedList: linkedListQuiz, - searchMethods: searchMethodsQuiz, binaryTree: binaryTreeQuiz, stacks: stackQuiz, queues: queueQuiz, + simpleList: simpleListQuiz, }; const quizzesEn = { @@ -48,11 +54,13 @@ export const getQuizByName = name => { quick: quickSortQuizEn, shell: shellSortQuizEn, insertion: insertionSortQuizEn, + linearSearch: linearSearchQuizEn, + binarySearch: binarySearchQuizEn, linkedList: linkedListQuizEn, - searchMethods: searchMethodsQuizEn, binaryTree: binaryTreeQuizEn, stacks: stackQuizEn, queues: queueQuizEn, + simpleList: simpleListQuizEn, }; return userLanguage == 'es' ? quizzes[name] : quizzesEn[name]; diff --git a/src/quiz/pages/DataStructuresPage.jsx b/src/quiz/pages/DataStructuresPage.jsx new file mode 100644 index 0000000..f690778 --- /dev/null +++ b/src/quiz/pages/DataStructuresPage.jsx @@ -0,0 +1,93 @@ +import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import CardTabla from '../../components/CardTabla'; +import { SearchMethodGrid } from '../components'; +import { useEffect } from 'react'; +import { resetStateForQuiz } from '../../store'; +import { + svgSimpleList, + svgLinkedListDataStructure, + svgBinaryTrees, + svgPiles, + svgQueue, +} from '../../assets/svg/SvgConstans'; +import { useTranslation } from 'react-i18next'; +import usePageActivity from '../../hooks/usePageActivity'; + +export const DataStructuresPage = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + usePageActivity('pagina_visitada', 'estructuras-datos'); + + useEffect(() => { + dispatch(resetStateForQuiz()); + }, [dispatch]); + + const dataStructures = [ + { + id: 1, + titulo: t('simpleLists'), + param: 'simpleList', + imagen: svgSimpleList, + }, + { + id: 2, + titulo: t('linkedLists'), + param: 'linkedList', + imagen: svgLinkedListDataStructure, + }, + { + id: 3, + titulo: t('binaryTrees'), + param: 'binaryTree', + imagen: svgBinaryTrees, + }, + { + id: 4, + titulo: t('stacks'), + param: 'stacks', + imagen: svgPiles, + }, + { + id: 5, + titulo: t('queues'), + param: 'queues', + imagen: svgQueue, + }, + ]; + + return ( +
+
+ +

+ {t('dataStructures') || 'Estructuras de Datos'} +

+
+
+ +
+ + {dataStructures.map(estructura => ( + + ))} + +
+
+ ); +}; diff --git a/src/quiz/pages/SearchMethodsPage.jsx b/src/quiz/pages/SearchMethodsPage.jsx new file mode 100644 index 0000000..5ea642c --- /dev/null +++ b/src/quiz/pages/SearchMethodsPage.jsx @@ -0,0 +1,64 @@ +import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import CardTabla from '../../components/CardTabla'; +import { SearchMethodGrid } from '../components'; +import { useEffect } from 'react'; +import { resetStateForQuiz } from '../../store'; +import { svgLinearSearch, svgBinarySearch } from '../../assets/svg/SvgConstans'; +import { useTranslation } from 'react-i18next'; +import usePageActivity from '../../hooks/usePageActivity'; + +export const SearchMethodsPage = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + usePageActivity('pagina_visitada', 'metodos-busqueda'); + + useEffect(() => { + dispatch(resetStateForQuiz()); + }, [dispatch]); + + const searchMethods = [ + { + id: 1, + titulo: t('linearSearch') || 'Búsqueda Lineal', + param: 'linearSearch', + imagen: svgLinearSearch, + }, + { + id: 2, + titulo: t('binarySearch') || 'Búsqueda Binaria', + param: 'binarySearch', + imagen: svgBinarySearch, + }, + ]; + + return ( +
+
+ +

+ {t('searchingMethods') || 'Métodos de Búsqueda'} +

+
+
+ +
+ + {searchMethods.map(metodo => ( + + ))} + +
+
+ ); +}; diff --git a/src/quiz/pages/SortingMethodsPage.jsx b/src/quiz/pages/SortingMethodsPage.jsx index 4e8cb86..1ef78a8 100644 --- a/src/quiz/pages/SortingMethodsPage.jsx +++ b/src/quiz/pages/SortingMethodsPage.jsx @@ -1,4 +1,5 @@ import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; import CardTabla from '../../components/CardTabla'; import { topicCategories } from '../../components/HomeCartas'; import { SearchMethodGrid } from '../components'; @@ -6,10 +7,13 @@ import { useEffect } from 'react'; import { resetStateForQuiz } from '../../store'; import { svgFundamentals } from '../../assets/svg/SvgConstans'; import { useTranslation } from 'react-i18next'; +import usePageActivity from '../../hooks/usePageActivity'; export const SortingMethodsPage = () => { const { t } = useTranslation(); const dispatch = useDispatch(); + const navigate = useNavigate(); + usePageActivity('pagina_visitada', 'metodos-ordenamiento'); useEffect(() => { dispatch(resetStateForQuiz()); @@ -22,9 +26,18 @@ export const SortingMethodsPage = () => { return (
-

- {t('sortingMethods')} -

+
+ +

+ {t('sortingMethods')} +

+
+
diff --git a/src/quiz/pages/index.js b/src/quiz/pages/index.js index cb0bf6d..5528e41 100644 --- a/src/quiz/pages/index.js +++ b/src/quiz/pages/index.js @@ -1 +1,3 @@ export * from './SortingMethodsPage'; +export * from './SearchMethodsPage'; +export * from './DataStructuresPage'; diff --git a/src/quiz/views/QuizAnswers.jsx b/src/quiz/views/QuizAnswers.jsx index 42313ab..12add9b 100644 --- a/src/quiz/views/QuizAnswers.jsx +++ b/src/quiz/views/QuizAnswers.jsx @@ -18,60 +18,162 @@ export const QuizAnswers = () => { } return ( -
- {questions.map((question, index) => ( -
- - - {Object.entries(question.options).map(([key, value]) => ( - {}} - isSelected={false} - disabled={true} - /> - ))} - - {t('correctOption')} - - {t('youChoose')} - - {t('explanation')}: -

{question.feedback}

- {question.type === questionType.PRACTICAL && ( - { - dispatch(setCurrentQuestion(question)); - navigate(`/animacion/${getAnimationNameByQuizName(quizName)}`); - }} +
+
+ {/* Header */} +
+

+ Revisión de Respuestas +

+

+ Aquí puedes ver todas tus respuestas y la retroalimentación +

+
+ + {/* Preguntas */} +
+ {questions.map((question, index) => ( +
- {' '} - {t('seeFeedback')}{' '} - - )} + {/* Número y pregunta */} +
+

+ Pregunta {index + 1} +

+

+ {question.statement} +

+
+ + {/* Comparativa de respuestas */} +
+ {/* Respuesta correcta */} +
+
+ + ✓ + +

+ {t('correctOption')} +

+
+
+
+ Opción {question.answer} +
+

+ {question.options[question.answer]} +

+
+
+ + {/* Tu respuesta */} +
+
+ + {question.selectedAnswer === question.answer ? '✓' : '✗'} + +

+ {t('youChoose')} +

+
+
+
+ Opción {question.selectedAnswer} +
+

+ {question.options[question.selectedAnswer]} +

+
+
+
+ + {/* Explicación */} +
+
+ 💡 +
+

+ {t('explanation')} +

+

+ {question.feedback} +

+
+
+
+ + {/* Botón ver retroalimentación */} + {question.type === questionType.PRACTICAL && ( + + )} +
+ ))}
- ))} -
- navigate(-1)}>{t('back')} - navigate('/quizzes')}> - {t('exit')} - + {/* Footer */} +
+ + +
); diff --git a/src/quiz/views/QuizView.jsx b/src/quiz/views/QuizView.jsx index 2bd8011..c62edb7 100644 --- a/src/quiz/views/QuizView.jsx +++ b/src/quiz/views/QuizView.jsx @@ -23,6 +23,7 @@ import { canFinishQuiz, formatQuestionIndicator, getQuizByName, + getAnimationNameByQuizName, } from '../helpers'; import { questionStates, questionType } from '../constants'; import { @@ -34,6 +35,7 @@ import { setQuiz, } from '../../store'; import { useTranslation } from 'react-i18next'; +import useActivity from '../../hooks/useActivity'; export const QuizView = () => { const { t } = useTranslation(); @@ -43,6 +45,8 @@ export const QuizView = () => { const { currentQuestion, currentQuestionIndex, totalQuestions, questions } = useSelector(state => state.quiz); const dispatch = useDispatch(); + const { trackActivity } = useActivity(); + const [quizStartTime] = useState(() => Date.now()); if (!quiz) { return ; @@ -83,6 +87,11 @@ export const QuizView = () => { const [isAlertDialogVisible, setIsAlertDialogVisible] = useState(false); const handleOnNextQuestion = () => { + // Si no ha seleccionado ninguna respuesta, no hacer nada + if (selectedAnswer === null) { + return; + } + if (state === questionStates.UNANSWERED && selectedAnswer != null) { dispatch(checkAnswer()); return; @@ -99,6 +108,10 @@ export const QuizView = () => { dispatch(checkAnswer()); dispatch(computeResults()); + // Calcular duración real en segundos + const duracionSegundos = Math.round((Date.now() - quizStartTime) / 1000); + trackActivity('ejercicio_completado', quizName, duracionSegundos); + setTimeout(() => { navigate('/quiz/results'); }, 1200); @@ -127,61 +140,127 @@ export const QuizView = () => { return ( <> -
-
-
- +
+ {/* Panel izquierdo - Navegación y retroalimentación (STICKY REAL) */} +
- -
-
- {isPreviousButtonVisible && ( - - {t('previous')} - - )} - {isQuizAtTheEnd ? ( - - {t('finish')} - - ) : ( - - {t('next')} - + )}
- - {formatQuestionIndicator(currentQuestionIndex, totalQuestions)} - -
+ + + {/* Panel derecho - Pregunta y opciones */} +
+
+ + +
+

+ Selecciona la respuesta correcta: +

+ + {Object.entries(options).map(([key, value]) => ( + + ))} + +
+
+
diff --git a/src/services/authService.js b/src/services/authService.js new file mode 100644 index 0000000..8311c24 --- /dev/null +++ b/src/services/authService.js @@ -0,0 +1,39 @@ +// Authentication service — wraps Firebase Auth methods +import { + GoogleAuthProvider, + signInWithPopup, + signOut, + onAuthStateChanged as firebaseOnAuthStateChanged, +} from 'firebase/auth'; +import { auth } from '../config/firebaseConfig'; + +const googleProvider = new GoogleAuthProvider(); + +/** + * Opens a Google sign-in popup and returns a normalized user object. + * @returns {{ uid, email, displayName, photoURL }} + */ +export const loginWithGoogle = async () => { + const result = await signInWithPopup(auth, googleProvider); + const { uid, email, displayName, photoURL } = result.user; + return { uid, email, displayName, photoURL }; +}; + +/** + * Signs the current user out. + */ +export const logout = () => signOut(auth); + +/** + * Returns the currently authenticated user, or null. + * @returns {import('firebase/auth').User | null} + */ +export const getCurrentUser = () => auth.currentUser; + +/** + * Subscribes to authentication state changes. + * @param {(user: import('firebase/auth').User | null) => void} callback + * @returns {() => void} Unsubscribe function + */ +export const onAuthStateChanged = callback => + firebaseOnAuthStateChanged(auth, callback); diff --git a/src/services/firestoreService.js b/src/services/firestoreService.js new file mode 100644 index 0000000..3aa44f4 --- /dev/null +++ b/src/services/firestoreService.js @@ -0,0 +1,138 @@ +// Firestore service — persists user data and activity tracking +import { + doc, + setDoc, + addDoc, + getDoc, + collection, + query, + where, + getDocs, + serverTimestamp, + Timestamp, +} from 'firebase/firestore'; +import { db } from '../config/firebaseConfig'; + +/** + * Creates or updates the user document in the 'usuarios' collection. + * On each login, calculates the duration of the PREVIOUS session + * (ultimaActividad - fechaUltimoLogin) and saves it in 'sesiones'. + * @param {{ uid, email, displayName, photoURL }} user + */ +export const saveUserData = async user => { + const userRef = doc(db, 'usuarios', user.uid); + + // Read existing user data + const snapshot = await getDoc(userRef); + const isFirstLogin = !snapshot.exists(); + + if (!isFirstLogin) { + const data = snapshot.data(); + const fechaUltimoLogin = data.fechaUltimoLogin; + const ultimaActividad = data.ultimaActividad; + + // Calculate previous session duration + if (fechaUltimoLogin && ultimaActividad) { + const loginMs = + fechaUltimoLogin instanceof Timestamp + ? fechaUltimoLogin.toMillis() + : fechaUltimoLogin; + const actividadMs = + ultimaActividad instanceof Timestamp + ? ultimaActividad.toMillis() + : ultimaActividad; + + const duracionSegundos = Math.round((actividadMs - loginMs) / 1000); + + if (duracionSegundos >= 10) { + await addDoc(collection(db, 'sesiones'), { + usuarioID: user.uid, + nombre: user.displayName ?? 'Anónimo', + fechaInicio: fechaUltimoLogin, + fechaFin: ultimaActividad, + duracionSegundos, + timestamp: serverTimestamp(), + }); + } + } + } + + const now = serverTimestamp(); + + await setDoc( + userRef, + { + nombre: user.displayName ?? '', + email: user.email ?? '', + photoURL: user.photoURL ?? '', + // fechaPrimerLogin: solo se escribe si es la primera vez + ...(isFirstLogin && { fechaPrimerLogin: now }), + // fechaUltimoLogin: se actualiza en cada login + fechaUltimoLogin: now, + // ultimaActividad: empieza igual al login, se actualiza con cada actividad + ultimaActividad: now, + }, + { merge: true } + ); +}; + +/** + * Records a user activity in the 'actividades' collection. + * @param {string} usuarioID + * @param {string} nombre — Display name of the user + * @param {'tema_abierto'|'ejercicio_completado'|'pagina_visitada'} tipo + * @param {string} contenido — Topic name, page path, etc. + * @param {number} [duracionSegundos] — Optional duration in seconds + */ +export const trackActivity = async ( + usuarioID, + nombre, + tipo, + contenido, + duracionSegundos, + botones = [] +) => { + const actividadData = { + usuarioID, + nombre: nombre ?? 'Anónimo', + tipo, + contenido, + timestamp: serverTimestamp(), + }; + + if (duracionSegundos !== undefined && duracionSegundos !== null) { + actividadData.duracionSegundos = duracionSegundos; + } + + if (botones.length > 0) { + actividadData.botones = botones; + } + + // Guardar actividad y actualizar ultimaActividad del usuario en paralelo + await Promise.all([ + addDoc(collection(db, 'actividades'), actividadData), + setDoc( + doc(db, 'usuarios', usuarioID), + { ultimaActividad: serverTimestamp() }, + { merge: true } + ), + ]); +}; + +/** + * Retrieves all activities for a given user, ordered by most recent first. + * @param {string} usuarioID + * @returns {Promise} + */ +export const getUserActivities = async usuarioID => { + const q = query( + collection(db, 'actividades'), + where('usuarioID', '==', usuarioID) + ); + + const snapshot = await getDocs(q); + return snapshot.docs.map(docSnap => ({ + id: docSnap.id, + ...docSnap.data(), + })); +}; diff --git a/src/store/binarySearch/binarySearchSlice.js b/src/store/binarySearch/binarySearchSlice.js new file mode 100644 index 0000000..d79d8c7 --- /dev/null +++ b/src/store/binarySearch/binarySearchSlice.js @@ -0,0 +1,120 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + array: [], + searchValue: null, + stepHistory: [], + history: -1, + isPlaying: false, + timeIds: [], + timeStep: 1, + repeat: false, + currentIndex: -1, + leftIndex: -1, + rightIndex: -1, + midIndex: -1, + foundIndex: -1, + description: '', + funAction: null, + currentLine: -1, +}; + +export const binarySearchSlice = createSlice({ + name: 'binarySearch', + initialState, + reducers: { + setArrayBinarySearch: (state, action) => { + state.array = action.payload; + state.stepHistory = []; + state.history = -1; + state.foundIndex = -1; + state.description = ''; + }, + + setSearchValueBinarySearch: (state, action) => { + state.searchValue = action.payload; + }, + + actionButtonBinarySearch: (state, action) => { + const { trace, value } = action.payload; + state.stepHistory = trace; + state.history = -1; + state.searchValue = value; + state.repeat = false; + state.funAction = 'busquedaBinaria'; + }, + + updateVisualizationBinarySearch: (state, action) => { + const step = action.payload; + if (step) { + state.array = step.array || state.array; + state.currentIndex = step.currentIndex ?? -1; + state.leftIndex = step.leftIndex ?? -1; + state.rightIndex = step.rightIndex ?? -1; + state.midIndex = step.midIndex ?? -1; + state.foundIndex = step.found ? step.midIndex : (step.foundIndex ?? -1); + state.description = step.message || step.description || ''; + state.currentLine = step.currentLine ?? -1; + } + }, + + incrementHistoryBinarySearch: state => { + if (state.history < state.stepHistory.length - 1) { + state.history += 1; + } + }, + + decrementHistoryBinarySearch: state => { + if (state.history > 0) { + state.history -= 1; + } + }, + + setPlayingBinarySearch: (state, action) => { + state.isPlaying = action.payload; + }, + + setTimeIdBinarySearch: (state, action) => { + state.timeIds = action.payload; + }, + + restoreTimeIdBinarySearch: state => { + state.timeIds = []; + }, + + restoreRepeatBinarySearch: state => { + state.repeat = true; + }, + + resetBinarySearch: state => { + state.stepHistory = []; + state.history = -1; + state.currentIndex = -1; + state.leftIndex = -1; + state.rightIndex = -1; + state.midIndex = -1; + state.foundIndex = -1; + state.description = ''; + state.isPlaying = false; + }, + + fullResetBinarySearch: () => initialState, + }, +}); + +export const { + setArrayBinarySearch, + setSearchValueBinarySearch, + actionButtonBinarySearch, + updateVisualizationBinarySearch, + incrementHistoryBinarySearch, + decrementHistoryBinarySearch, + setPlayingBinarySearch, + setTimeIdBinarySearch, + restoreTimeIdBinarySearch, + restoreRepeatBinarySearch, + resetBinarySearch, + fullResetBinarySearch, +} = binarySearchSlice.actions; + +export default binarySearchSlice.reducer; diff --git a/src/store/binarySearch/index.js b/src/store/binarySearch/index.js new file mode 100644 index 0000000..b3d09e0 --- /dev/null +++ b/src/store/binarySearch/index.js @@ -0,0 +1,15 @@ +export { + setArrayBinarySearch, + setSearchValueBinarySearch, + actionButtonBinarySearch, + updateVisualizationBinarySearch, + incrementHistoryBinarySearch, + decrementHistoryBinarySearch, + setPlayingBinarySearch, + setTimeIdBinarySearch, + restoreTimeIdBinarySearch, + restoreRepeatBinarySearch, + resetBinarySearch, + fullResetBinarySearch, +} from './binarySearchSlice'; +export { default as binarySearchSlice } from './binarySearchSlice'; diff --git a/src/store/doubleList/doubleListSlice.js b/src/store/doubleList/doubleListSlice.js new file mode 100644 index 0000000..55ea310 --- /dev/null +++ b/src/store/doubleList/doubleListSlice.js @@ -0,0 +1,149 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { + InsertarAlInicio, + InsertarAlFinal, + InsertarEnPosicion, + EliminarDelInicio, + EliminarDelFinal, + EliminarEnPosicion, + Buscar, +} from '../../animations/doubleList/algorithms'; + +const initialState = { + head: null, + stepHistory: [], + history: -1, + firstSet: [], + secondSet: [], + thirdSet: [], + isHead: -1, + highlightedNode: null, + pointer: null, + currentIndex: null, + srcHead: null, + playing: false, + funAction: null, + timeIds: [], +}; + +export const doubleListSlice = createSlice({ + name: 'doubleList', + initialState, + reducers: { + setHeadDoubleList: (state, action) => { + state.srcHead = action.payload; + state.head = state.srcHead; + state.isHead = state.srcHead ? state.srcHead.getValue() : -1; + }, + + actionButtonDoubleList: (state, action) => { + const { action: operation, value, position } = action.payload; + const head = state.head; + + let result = null; + switch (operation) { + case 'insertarAlInicio': + result = InsertarAlInicio(head, value); + break; + case 'insertarAlFinal': + result = InsertarAlFinal(head, value); + break; + case 'insertarEnPosicion': + result = InsertarEnPosicion(head, value, position); + break; + case 'eliminarDelInicio': + result = EliminarDelInicio(head); + break; + case 'eliminarDelFinal': + result = EliminarDelFinal(head); + break; + case 'eliminarEnPosicion': + result = EliminarEnPosicion(head, position); + break; + case 'buscar': + result = Buscar(head, value); + break; + default: + break; + } + + if (result) { + state.stepHistory = result.historialPasos; + state.funAction = operation; + state.srcHead = result.nuevaCabeza; + state.head = state.srcHead; + state.history = -1; + state.isHead = state.srcHead ? state.srcHead.getValue() : -1; + state.firstSet = []; + state.secondSet = []; + state.thirdSet = []; + state.highlightedNode = null; + state.pointer = null; + state.currentIndex = null; + } + }, + + restoreRepeatDoubleList: state => { + state.head = state.srcHead; + state.history = -1; + state.firstSet = []; + state.secondSet = []; + state.thirdSet = []; + state.highlightedNode = null; + state.pointer = null; + state.currentIndex = null; + state.playing = false; + }, + + restoreTimeIdDoubleList: state => { + state.timeIds.forEach(timeoutId => clearTimeout(timeoutId)); + state.timeIds = []; + }, + + updateVisualizationDoubleList: (state, action) => { + const currVisualization = action.payload; + state.head = currVisualization.head; + state.firstSet = [...currVisualization.firstSet]; + state.secondSet = [...currVisualization.secondSet]; + state.thirdSet = [...currVisualization.thirdSet]; + state.isHead = currVisualization.isHead; + state.highlightedNode = currVisualization.highlightedNode; + state.pointer = currVisualization.pointer; + state.currentIndex = currVisualization.currentIndex; + }, + + setTimeIdDoubleList: (state, action) => { + state.timeIds = action.payload; + }, + + incrementHistoryDoubleList: state => { + if (state.history < state.stepHistory.length - 1) { + state.history += 1; + } + }, + + decrementHistoryDoubleList: state => { + if (state.history > -1) { + state.history -= 1; + } + }, + + setPlayingDoubleList: (state, action) => { + state.playing = action.payload; + }, + }, +}); + +export const { + setHeadDoubleList, + actionButtonDoubleList, + restoreRepeatDoubleList, + restoreTimeIdDoubleList, + updateVisualizationDoubleList, + setTimeIdDoubleList, + incrementHistoryDoubleList, + decrementHistoryDoubleList, + setPlayingDoubleList, +} = doubleListSlice.actions; + +export default doubleListSlice.reducer; diff --git a/src/store/doubleList/index.js b/src/store/doubleList/index.js new file mode 100644 index 0000000..2246e64 --- /dev/null +++ b/src/store/doubleList/index.js @@ -0,0 +1 @@ +export * from './doubleListSlice'; diff --git a/src/store/index.js b/src/store/index.js index 75291e0..8d46ab6 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2,3 +2,8 @@ export * from './store'; export * from './quiz'; export * from './sorts'; export * from './stack'; +export * from './simpleList'; +export * from './doubleList'; +export * from './linearSearch'; +export * from './binarySearch'; +export * from './queue'; diff --git a/src/store/linearSearch/index.js b/src/store/linearSearch/index.js new file mode 100644 index 0000000..9229754 --- /dev/null +++ b/src/store/linearSearch/index.js @@ -0,0 +1,15 @@ +export { + setArrayLinearSearch, + setSearchValueLinearSearch, + actionButtonLinearSearch, + updateVisualizationLinearSearch, + incrementHistoryLinearSearch, + decrementHistoryLinearSearch, + setPlayingLinearSearch, + setTimeIdLinearSearch, + restoreTimeIdLinearSearch, + restoreRepeatLinearSearch, + resetLinearSearch, + fullResetLinearSearch, +} from './linearSearchSlice'; +export { default as linearSearchSlice } from './linearSearchSlice'; diff --git a/src/store/linearSearch/linearSearchSlice.js b/src/store/linearSearch/linearSearchSlice.js new file mode 100644 index 0000000..cfb5cf4 --- /dev/null +++ b/src/store/linearSearch/linearSearchSlice.js @@ -0,0 +1,111 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + array: [], + searchValue: null, + stepHistory: [], + history: -1, + isPlaying: false, + timeIds: [], + timeStep: 1, + repeat: false, + currentIndex: -1, + foundIndex: -1, + description: '', + funAction: null, + currentLine: -1, +}; + +export const linearSearchSlice = createSlice({ + name: 'linearSearch', + initialState, + reducers: { + setArrayLinearSearch: (state, action) => { + state.array = action.payload; + state.stepHistory = []; + state.history = -1; + state.foundIndex = -1; + state.description = ''; + }, + + setSearchValueLinearSearch: (state, action) => { + state.searchValue = action.payload; + }, + + actionButtonLinearSearch: (state, action) => { + const { trace, value } = action.payload; + state.stepHistory = trace; + state.history = -1; + state.searchValue = value; + state.repeat = false; + state.funAction = 'busquedaLineal'; + }, + + updateVisualizationLinearSearch: (state, action) => { + const step = action.payload; + if (step) { + state.array = step.array || state.array; + state.currentIndex = step.currentIndex ?? -1; + state.foundIndex = step.foundIndex ?? -1; + state.description = step.description || ''; + state.currentLine = step.currentLine ?? -1; + } + }, + + incrementHistoryLinearSearch: state => { + if (state.history < state.stepHistory.length - 1) { + state.history += 1; + } + }, + + decrementHistoryLinearSearch: state => { + if (state.history > 0) { + state.history -= 1; + } + }, + + setPlayingLinearSearch: (state, action) => { + state.isPlaying = action.payload; + }, + + setTimeIdLinearSearch: (state, action) => { + state.timeIds = action.payload; + }, + + restoreTimeIdLinearSearch: state => { + state.timeIds = []; + }, + + restoreRepeatLinearSearch: state => { + state.repeat = true; + }, + + resetLinearSearch: state => { + state.stepHistory = []; + state.history = -1; + state.currentIndex = -1; + state.foundIndex = -1; + state.description = ''; + state.isPlaying = false; + }, + + fullResetLinearSearch: () => initialState, + }, +}); + +export const { + setArrayLinearSearch, + setSearchValueLinearSearch, + actionButtonLinearSearch, + updateVisualizationLinearSearch, + incrementHistoryLinearSearch, + decrementHistoryLinearSearch, + setPlayingLinearSearch, + setTimeIdLinearSearch, + restoreTimeIdLinearSearch, + restoreRepeatLinearSearch, + resetLinearSearch, + fullResetLinearSearch, +} = linearSearchSlice.actions; + +export default linearSearchSlice.reducer; diff --git a/src/store/queue/index.js b/src/store/queue/index.js new file mode 100644 index 0000000..0034225 --- /dev/null +++ b/src/store/queue/index.js @@ -0,0 +1 @@ +export * from './queueSlice'; diff --git a/src/store/queue/queueSlice.js b/src/store/queue/queueSlice.js new file mode 100644 index 0000000..0c07947 --- /dev/null +++ b/src/store/queue/queueSlice.js @@ -0,0 +1,191 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { + Dequeue, + Enqueue, + EliminarFinal, + InsertarQueue, + ExtraerQueue, +} from '../../animations'; + +const initialState = { + head: null, + elementos: [], + stepHistory: [], + timeStep: 1, + + funAction: null, + + history: -1, + firstSet: [], + secondSet: [], + highlightedNode: null, + pointer: null, + deletingNode: null, + timeId: [], + isHead: -1, + + srcHead: null, + playing: false, +}; + +export const queueSlice = createSlice({ + name: 'queue', + initialState, + reducers: { + setHeadQueue: (state, action) => { + restoreQueue(state, action); + restoreTimeIdQueue(state); + }, + actionButtonQueue: (state, action) => { + createRecordQueue(state, action); + }, + restoreQueue: (state, action) => { + state.head = action.payload; + state.stepHistory = []; + state.timeStep = 1; + + state.funAction = null; + + state.history = -1; + state.firstSet = []; + state.secondSet = []; + state.timeId = []; + + state.srcHead = action.payload; + + updateElementsFromHead(state); + }, + restoreTimeIdQueue: state => { + state.timeId.forEach(timeoutId => clearTimeout(timeoutId)); + state.timeId = []; + }, + updateVisualizationQueue: (state, action) => { + const currVisualization = action.payload; + + let elementos = []; + let current = currVisualization.head; + while (current !== null) { + elementos.push(current.getValue()); + current = current.getNext(); + } + + state.head = currVisualization.head; + state.firstSet = currVisualization.firstSet; + state.secondSet = currVisualization.secondSet; + state.highlightedNode = currVisualization.highlightedNode || null; + state.pointer = currVisualization.pointer || null; + state.deletingNode = currVisualization.deletingNode || null; + state.isHead = currVisualization.isHead; + state.elementos = elementos; + }, + restoreRepeatQueue: state => { + const head = state.srcHead; + state.head = head; + state.history = -1; + state.firstSet = []; + state.secondSet = []; + state.deletingNode = null; + state.isHead = -1; + + updateElementsFromHead(state); + }, + + setTimeIdQueue: (state, action) => { + state.timeId.push(...action.payload); + }, + + incrementHistoryQueue: state => { + state.history = state.history + 1; + }, + + decrementHistoryQueue: state => { + state.history = state.history - 1; + }, + + setPlayingQueue: (state, action) => { + state.playing = action.payload; + }, + }, +}); + +const updateElementsFromHead = state => { + let elementos = []; + let current = state.head; + while (current !== null) { + elementos.push(current.getValue()); + current = current.getNext(); + } + state.elementos = elementos; +}; + +const createRecordQueue = (state, action) => { + const head = state.head; + const isHead = state.isHead; + const value = action.payload.value; + const position = action.payload.position; + + const funAction = getActionQueue(action.payload.action); + if (funAction) { + let result; + + if (action.payload.action === 'insertar') { + result = funAction(head, value, position, isHead); + } else if (action.payload.action === 'extraer') { + result = funAction(head, position, isHead); + } else { + result = funAction(head, value, isHead); + } + + if (result && result.historialPasos) { + state.stepHistory = result.historialPasos; + state.funAction = action.payload.action; + + if (result.nuevaCabeza !== undefined) { + state.srcHead = result.nuevaCabeza; + } + } + } +}; + +export const getActionQueue = action => { + const actions = { + enqueue: Enqueue, + dequeue: Dequeue, + eliminarFinal: EliminarFinal, + insertar: InsertarQueue, + extraer: ExtraerQueue, + }; + return actions[action] || null; +}; + +export const verificarCola = (head, elemento) => { + let current = head; + while (current !== null) { + if (current.getValue() === elemento) return true; + current = current.getNext(); + } + return false; +}; + +const { + restoreQueue, + restoreTimeIdQueue, + updateVisualizationQueue, + restoreRepeatQueue, +} = queueSlice.actions; + +export const { + actionButtonQueue, + setHeadQueue, + decrementHistoryQueue, + incrementHistoryQueue, + setTimeIdQueue, + setPlayingQueue, +} = queueSlice.actions; + +export { + restoreQueue, + restoreTimeIdQueue, + updateVisualizationQueue, + restoreRepeatQueue, +}; diff --git a/src/store/simpleList/index.js b/src/store/simpleList/index.js new file mode 100644 index 0000000..ec4c334 --- /dev/null +++ b/src/store/simpleList/index.js @@ -0,0 +1 @@ +export * from './simpleListSlice'; diff --git a/src/store/simpleList/newFile.js b/src/store/simpleList/newFile.js new file mode 100644 index 0000000..ec4c334 --- /dev/null +++ b/src/store/simpleList/newFile.js @@ -0,0 +1 @@ +export * from './simpleListSlice'; diff --git a/src/store/simpleList/simpleListSlice.js b/src/store/simpleList/simpleListSlice.js new file mode 100644 index 0000000..a0a3919 --- /dev/null +++ b/src/store/simpleList/simpleListSlice.js @@ -0,0 +1,149 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { + InsertarAlInicio, + InsertarAlFinal, + InsertarEnPosicion, + EliminarDelInicio, + EliminarDelFinal, + EliminarEnPosicion, + Buscar, +} from '../../animations/simpleList/algorithms'; + +const initialState = { + head: null, + stepHistory: [], + history: -1, + firstSet: [], + secondSet: [], + thirdSet: [], + isHead: -1, + highlightedNode: null, + pointer: null, + currentIndex: null, + srcHead: null, + playing: false, + funAction: null, + timeIds: [], +}; + +export const simpleListSlice = createSlice({ + name: 'simpleList', + initialState, + reducers: { + setHeadSimpleList: (state, action) => { + state.srcHead = action.payload; + state.head = state.srcHead; + state.isHead = state.srcHead ? state.srcHead.getValue() : -1; + }, + + actionButtonSimpleList: (state, action) => { + const { action: operation, value, position } = action.payload; + const head = state.head; + + let result = null; + switch (operation) { + case 'insertarAlInicio': + result = InsertarAlInicio(head, value); + break; + case 'insertarAlFinal': + result = InsertarAlFinal(head, value); + break; + case 'insertarEnPosicion': + result = InsertarEnPosicion(head, value, position); + break; + case 'eliminarDelInicio': + result = EliminarDelInicio(head); + break; + case 'eliminarDelFinal': + result = EliminarDelFinal(head); + break; + case 'eliminarEnPosicion': + result = EliminarEnPosicion(head, position); + break; + case 'buscar': + result = Buscar(head, value); + break; + default: + break; + } + + if (result) { + state.stepHistory = result.historialPasos; + state.funAction = operation; + state.srcHead = result.nuevaCabeza; + state.head = state.srcHead; + state.history = -1; + state.isHead = state.srcHead ? state.srcHead.getValue() : -1; + state.firstSet = []; + state.secondSet = []; + state.thirdSet = []; + state.highlightedNode = null; + state.pointer = null; + state.currentIndex = null; + } + }, + + restoreRepeatSimpleList: state => { + state.head = state.srcHead; + state.history = -1; + state.firstSet = []; + state.secondSet = []; + state.thirdSet = []; + state.highlightedNode = null; + state.pointer = null; + state.currentIndex = null; + state.playing = false; + }, + + restoreTimeIdSimpleList: state => { + state.timeIds.forEach(timeoutId => clearTimeout(timeoutId)); + state.timeIds = []; + }, + + updateVisualizationSimpleList: (state, action) => { + const currVisualization = action.payload; + state.head = currVisualization.head; + state.firstSet = [...currVisualization.firstSet]; + state.secondSet = [...currVisualization.secondSet]; + state.thirdSet = [...currVisualization.thirdSet]; + state.isHead = currVisualization.isHead; + state.highlightedNode = currVisualization.highlightedNode; + state.pointer = currVisualization.pointer; + state.currentIndex = currVisualization.currentIndex; + }, + + setTimeIdSimpleList: (state, action) => { + state.timeIds = action.payload; + }, + + incrementHistorySimpleList: state => { + if (state.history < state.stepHistory.length - 1) { + state.history += 1; + } + }, + + decrementHistorySimpleList: state => { + if (state.history > -1) { + state.history -= 1; + } + }, + + setPlayingSimpleList: (state, action) => { + state.playing = action.payload; + }, + }, +}); + +export const { + setHeadSimpleList, + actionButtonSimpleList, + restoreRepeatSimpleList, + restoreTimeIdSimpleList, + updateVisualizationSimpleList, + setTimeIdSimpleList, + incrementHistorySimpleList, + decrementHistorySimpleList, + setPlayingSimpleList, +} = simpleListSlice.actions; + +export default simpleListSlice.reducer; diff --git a/src/store/stack/stackSlice.js b/src/store/stack/stackSlice.js index e892c9c..ef2a066 100644 --- a/src/store/stack/stackSlice.js +++ b/src/store/stack/stackSlice.js @@ -1,5 +1,5 @@ import { createSlice } from '@reduxjs/toolkit'; -import { Pop, Push } from '../../animations'; +import { Pop, Push, Sumergir, Insertar, Extraer } from '../../animations'; const initialState = { head: null, @@ -12,6 +12,7 @@ const initialState = { history: -1, firstSet: [], secondSet: [], + deletingNode: null, timeId: [], isHead: -1, @@ -24,8 +25,9 @@ export const stackSlice = createSlice({ initialState, reducers: { setHead: (state, action) => { - stackSlice.caseReducers.restoreStack(state, action); - stackSlice.caseReducers.restoreTimeId(state); + // Usar el reducer interno correctamente + restoreStack(state, action); + restoreTimeId(state); }, actionButton: (state, action) => { createRecordStack(state, action); @@ -40,9 +42,13 @@ export const stackSlice = createSlice({ state.history = -1; state.firstSet = []; state.secondSet = []; + state.deletingNode = null; state.timeId = []; state.srcHead = action.payload; + + // Actualizar elementos basado en la nueva cabeza + updateElementsFromHead(state); }, restoreTimeId: state => { state.timeId.forEach(timeoutId => clearTimeout(timeoutId)); @@ -51,6 +57,7 @@ export const stackSlice = createSlice({ updateVisualizationStack: (state, action) => { const currVisualization = action.payload; + // Actualizar elementos desde la cabeza de la visualización actual let elementos = []; let current = currVisualization.head; while (current !== null) { @@ -58,14 +65,13 @@ export const stackSlice = createSlice({ current = current.getNext(); } - return { - ...state, - head: currVisualization.head, - firstSet: currVisualization.firstSet, - secondSet: currVisualization.secondSet, - isHead: currVisualization.isHead, - elementos: elementos, - }; + // Actualizar el estado con la visualización actual + state.head = currVisualization.head; + state.firstSet = currVisualization.firstSet; + state.secondSet = currVisualization.secondSet; + state.isHead = currVisualization.isHead; + state.elementos = elementos; + state.deletingNode = currVisualization.deletingNode || null; }, restoreRepeatStack: state => { const head = state.srcHead; @@ -73,26 +79,23 @@ export const stackSlice = createSlice({ state.history = -1; state.firstSet = []; state.secondSet = []; + state.deletingNode = null; state.isHead = -1; - state.elementos = []; + + // Actualizar elementos desde la cabeza restaurada + updateElementsFromHead(state); }, setTimeIdStack: (state, action) => { - state.timeId.push(action.payload); + state.timeId.push(...action.payload); }, incrementHistoryStack: state => { - return { - ...state, - history: state.history + 1, - }; + state.history = state.history + 1; }, decrementHistoryStack: state => { - return { - ...state, - history: state.history - 1, - }; + state.history = state.history - 1; }, setPlayingStack: (state, action) => { @@ -101,16 +104,51 @@ export const stackSlice = createSlice({ }, }); +// Función auxiliar para actualizar elementos desde la cabeza +const updateElementsFromHead = state => { + let elementos = []; + let current = state.head; + while (current !== null) { + elementos.push(current.getValue()); + current = current.getNext(); + } + state.elementos = elementos; +}; + const createRecordStack = (state, action) => { const head = state.head; const isHead = state.isHead; const value = action.payload.value; + const position = action.payload.position; const funAction = getAction(action.payload.action); if (funAction) { - const stepHistory = funAction(head, value, isHead); - state.stepHistory = stepHistory; - state.funAction = action.payload.action; + let result; + + // Determinar qué parámetros pasar según la acción + if (action.payload.action === 'insertar') { + // En la pila, posición 0 significa al inicio (en el tope de la pila) + // y posición length significa al final (en el fondo de la pila) + result = funAction(head, value, position, isHead); + } else if (action.payload.action === 'extraer') { + // Para extraer, la posición debe ser interpretada desde el tope (0) hasta el fondo + result = funAction(head, position, isHead); + } else { + result = funAction(head, value, isHead); + } + + // Las funciones de animación devuelven un objeto con historialPasos y nuevaCabeza + if (result && result.historialPasos) { + state.stepHistory = result.historialPasos; + state.funAction = action.payload.action; + + // Si hay una nueva cabeza, actualizarla en el estado temporal + if (result.nuevaCabeza !== undefined) { + // No actualizamos state.head aquí porque será actualizado durante la animación + // Pero podríamos guardar la cabeza final para referencia + state.srcHead = result.nuevaCabeza; + } + } } }; @@ -118,6 +156,9 @@ export const getAction = action => { const actions = { push: Push, pop: Pop, + sumergir: Sumergir, + insertar: Insertar, + extraer: Extraer, }; return actions[action] || null; }; @@ -133,15 +174,25 @@ export const verificar = (head, elemento) => { return false; }; -export const { - actionButton, +const { restoreStack, restoreTimeId, + updateVisualizationStack, + restoreRepeatStack, +} = stackSlice.actions; + +export const { + actionButton, setHead, decrementHistoryStack, incrementHistoryStack, - updateVisualizationStack, setTimeIdStack, setPlayingStack, - restoreRepeatStack, } = stackSlice.actions; + +export { + restoreStack, + restoreTimeId, + updateVisualizationStack, + restoreRepeatStack, +}; diff --git a/src/store/store.js b/src/store/store.js index 84e278f..6544e23 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -2,12 +2,22 @@ import { configureStore } from '@reduxjs/toolkit'; import { quizSlice } from './quiz'; import { sortsSlice } from './sorts'; import { stackSlice } from './stack'; +import simpleListSlice from './simpleList/simpleListSlice'; +import doubleListSlice from './doubleList/doubleListSlice'; +import linearSearchSlice from './linearSearch/linearSearchSlice'; +import binarySearchSlice from './binarySearch/binarySearchSlice'; +import { queueSlice } from './queue'; export const store = configureStore({ reducer: { quiz: quizSlice.reducer, sorts: sortsSlice.reducer, stack: stackSlice.reducer, + simpleList: simpleListSlice, + doubleList: doubleListSlice, + linearSearch: linearSearchSlice, + binarySearch: binarySearchSlice, + queue: queueSlice.reducer, }, middleware: getDefaultMiddleware => getDefaultMiddleware({ diff --git a/src/theme/theme.js b/src/theme/theme.js index e0b4b99..10151fa 100644 --- a/src/theme/theme.js +++ b/src/theme/theme.js @@ -11,6 +11,14 @@ export const theme = createTheme({ tertiary: { main: '#ECE6E4', }, + info: { + main: '#0284C7', + dark: '#0369A1', + }, + warning: { + main: '#EA580C', + dark: '#C2410C', + }, gray: { main: '#1E1E1E', primary: '#D9D9D9', diff --git a/src/utils/clickRegistry.js b/src/utils/clickRegistry.js new file mode 100644 index 0000000..7617d9b --- /dev/null +++ b/src/utils/clickRegistry.js @@ -0,0 +1,21 @@ +// clickRegistry — módulo singleton que acumula clicks EXITOSOS de la sesión actual +// Los componentes llaman a registrarClick() solo cuando la operación fue exitosa. +// usePageActivity lee y limpia el registro al guardar la actividad. + +let clicks = []; + +/** + * Registra un click exitoso. Solo guarda el nombre del botón. + * @param {string} nombre — Nombre/funcionalidad del botón (ej: "Push", "Pop") + */ +export const registrarClick = nombre => { + clicks.push(nombre); +}; + +/** Devuelve copia del registro actual */ +export const obtenerClicks = () => [...clicks]; + +/** Limpia el registro (se llama al guardar la actividad) */ +export const limpiarClicks = () => { + clicks = []; +}; diff --git a/src/utils/googleAnalytics.js b/src/utils/googleAnalytics.js new file mode 100644 index 0000000..a10d86c --- /dev/null +++ b/src/utils/googleAnalytics.js @@ -0,0 +1,50 @@ +// Variable global para almacenar el measurement ID +let MEASUREMENT_ID = ''; + +// Inicializar Google Analytics +export const initializeGoogleAnalytics = measurementId => { + // Guardar el measurement ID + MEASUREMENT_ID = measurementId; + + // Crear el script de Google Analytics + const script = document.createElement('script'); + script.async = true; + script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`; + + // Agregar el script al head del documento + document.head.appendChild(script); + + // Inicializar dataLayer de Google Analytics + window.dataLayer = window.dataLayer || []; + + function gtag() { + window.dataLayer.push(arguments); + } + + gtag('js', new Date()); + // Desactivar el page_view automático para manejarlo manualmente en la SPA + gtag('config', measurementId, { + send_page_view: false, + }); + + // Guardar gtag en window para usarla en toda la app + window.gtag = gtag; +}; + +// Función para rastrear eventos personalizados +export const trackEvent = (eventName, eventParams = {}) => { + if (window.gtag) { + window.gtag('event', eventName, eventParams); + } +}; + +// Función para rastrear cambios de página (GA4 - evento page_view) +export const trackPageView = pageName => { + if (window.gtag) { + window.gtag('event', 'page_view', { + page_path: pageName, + page_location: window.location.origin + pageName, + page_title: document.title, + }); + } +}; diff --git a/src/utils/trackingEvents.js b/src/utils/trackingEvents.js new file mode 100644 index 0000000..97b17e6 --- /dev/null +++ b/src/utils/trackingEvents.js @@ -0,0 +1,43 @@ +// Ejemplo de cómo rastrear eventos personalizados en tu proyecto +import { trackEvent } from './googleAnalytics'; + +// Ejemplo 1: Rastrear cuando un usuario hace clic en un botón de quiz +export const handleQuizClick = quizName => { + trackEvent('quiz_started', { + quiz_name: quizName, + timestamp: new Date().toISOString(), + }); +}; + +// Ejemplo 2: Rastrear cuando un usuario completa un quiz +export const handleQuizComplete = (quizName, score) => { + trackEvent('quiz_completed', { + quiz_name: quizName, + score: score, + timestamp: new Date().toISOString(), + }); +}; + +// Ejemplo 3: Rastrear cuando un usuario ve una animación +export const handleAnimationView = animationName => { + trackEvent('animation_viewed', { + animation_name: animationName, + timestamp: new Date().toISOString(), + }); +}; + +// Ejemplo 4: Rastrear búsquedas +export const handleSearch = searchQuery => { + trackEvent('search_performed', { + search_query: searchQuery, + timestamp: new Date().toISOString(), + }); +}; + +// Ejemplo 5: Rastrear descargas o exports +export const handleExport = fileType => { + trackEvent('content_exported', { + file_type: fileType, + timestamp: new Date().toISOString(), + }); +};