From 8ac759ec7fdbad053cd7b8aeeded76bbb1dde720 Mon Sep 17 00:00:00 2001 From: jdbcamilo <149972010+jdbcamilo@users.noreply.github.com> Date: Sun, 18 May 2025 10:38:54 -0500 Subject: [PATCH 01/18] Feature: Se agrega la funcionalidad de sumergir y agregar entre nodos a las pilas --- .../stack/algorithms/StackAlgorithm.jsx | 116 ++++++++----- src/animations/stack/components/StackCode.jsx | 62 +++---- .../stack/components/StackControls.jsx | 157 +++++++++--------- src/store/stack/stackSlice.js | 21 +-- 4 files changed, 186 insertions(+), 170 deletions(-) diff --git a/src/animations/stack/algorithms/StackAlgorithm.jsx b/src/animations/stack/algorithms/StackAlgorithm.jsx index 5038fdd..ca3ce85 100644 --- a/src/animations/stack/algorithms/StackAlgorithm.jsx +++ b/src/animations/stack/algorithms/StackAlgorithm.jsx @@ -1,4 +1,4 @@ -import { newTraceStack, moveInHistoryRecordStack } from './helpers'; +import { newTraceStack, moveInHistoryRecordStack } from "./helpers"; import Nodo from './Nodo'; export const Push = (head, value, isHead) => { @@ -8,24 +8,11 @@ export const Push = (head, value, isHead) => { 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 - ); + moveInHistoryRecordStack(stepHistory, head, [head.getValue()], [], (head.getNext() === null ? -1 : head.getNext().getValue()), 0); + moveInHistoryRecordStack(stepHistory, head, [], [head.getValue()], head.getValue(), 1); return stepHistory; + }; export const Pop = (head, value, isHead) => { @@ -33,36 +20,61 @@ export const Pop = (head, value, 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 - ); + 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); head = temp.getNext(); - moveInHistoryRecordStack( - stepHistory, - head, - [], - [head.getValue()], - head.getValue(), - 3 - ); + moveInHistoryRecordStack(stepHistory, head, [], [head.getValue()], head.getValue(), 3); } else { head = null; moveInHistoryRecordStack(stepHistory, head, [], [], -1, -1); } + return stepHistory; +} + +export const Sumergir = (head, value, isHead) => { + const stepHistory = newTraceStack(head, isHead); + + // Si la pila está vacía o tiene un solo elemento, no hay nada que sumergir + if (head === null || head.getNext() === null) { + moveInHistoryRecordStack(stepHistory, head, [], [], (head ? head.getValue() : -1), 0); + return stepHistory; + } + + // Guardamos la referencia al nodo actual head que moveremos al final + const oldHead = head; + + // Marcamos el nodo cabeza que se va a sumergir + moveInHistoryRecordStack(stepHistory, head, [head.getValue()], [], head.getValue(), 0); + + // El nuevo head será el siguiente nodo + const newHead = head.getNext(); + moveInHistoryRecordStack(stepHistory, head, [head.getValue()], [newHead.getValue()], head.getValue(), 1); + + // Ahora actualizamos head para que sea el segundo nodo + head = newHead; + + // Encontrar el último nodo + let current = head; + let last = null; + while (current !== null) { + last = current; + current = current.getNext(); + } + + // Marcar el último nodo que encontramos + moveInHistoryRecordStack(stepHistory, head, [oldHead.getValue(), last.getValue()], [], newHead.getValue(), 2); + + // Desconectar el nodo original (oldHead) de la lista para evitar ciclos + oldHead.setNext(null); + + // Conectar el último nodo con el nodo sumergido + last.setNext(oldHead); + moveInHistoryRecordStack(stepHistory, head, [], [last.getValue(), oldHead.getValue()], newHead.getValue(), 3); + + // Mostrar la nueva estructura con el head actualizado + moveInHistoryRecordStack(stepHistory, head, [], [], head.getValue(), 4); + return stepHistory; }; @@ -73,8 +85,8 @@ export const PushCode = ({ codeRef }) => { nodo.next = head`}
{`head = nodo`}
- ); -}; + ) +} export const PopCode = ({ codeRef }) => { return ( @@ -84,5 +96,19 @@ export const PopCode = ({ codeRef }) => {
{`head = tmp.next`}
{`delete tmp`}
- ); -}; + ) +} + +export const SumergirCode = ({ codeRef }) => { + return ( + +
{`if empty or single node, do nothing`}
+
{`newHead = head.next`}
+
{`find last node in list`}
+
{`last.next = head`}
+
{`head.next = null`}
+
{`head = newHead`}
+
+ ) + +} \ No newline at end of file diff --git a/src/animations/stack/components/StackCode.jsx b/src/animations/stack/components/StackCode.jsx index 3354a05..989860a 100644 --- a/src/animations/stack/components/StackCode.jsx +++ b/src/animations/stack/components/StackCode.jsx @@ -1,17 +1,19 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react' import { useSelector } from 'react-redux'; -import { PopCode, PushCode } from '../algorithms'; +import { PopCode, PushCode, SumergirCode } from '../algorithms'; import { SvgIconDropdown, svgAlgo } from '../../../assets/svg/SvgConstans'; import { useTranslation } from 'react-i18next'; export const StackCode = () => { + const codeRef = useRef(null); const [prevCurrentLine, setPrevCurrentLine] = useState(null); const { funAction, history, stepHistory } = useSelector(state => state.stack); const [infoVisible, setInfoVisible] = useState(false); - const { t } = useTranslation(); + const { t } = useTranslation() useEffect(() => { + if (history === -1 || history >= stepHistory.length) return; const currentLine = stepHistory[history].currentIndex; @@ -30,51 +32,42 @@ export const StackCode = () => { setPrevCurrentLine(currentLine); }, [history, stepHistory, funAction]); - const Code = funAction => { + const Code = (funAction) => { switch (funAction) { - case 'push': - return ; - case 'pop': - return ; - default: - null; + case 'push': return + case 'pop': return + case 'sumergir': return + default: null; } - }; + } const toggleInfoPanel = () => { setInfoVisible(!infoVisible); - }; + } return ( -
+
{/* Panel de información */} {infoVisible && ( -
+
  • - {t('definition')}:{' '} - {t('stackDefinition')} + {t("definition")}: {t("stackDefinition")}
  • - - {t('principalOperations')} - + { t("principalOperations") }
      -
    • Push: {t('pushExplanation')}
    • -
    • Pop: {t('popExplanation')}
    • +
    • Push: { t("pushExplanation") }
    • +
    • Pop: { t("popExplanation") }
    • +
    • Sumergir: Mueve el elemento del tope de la pila al final
  • - {t('utility')}:{' '} - {t('stackUsageExplanation')} + {t("utility")}: { t("stackUsageExplanation") }
  • -
  • - {t('implementations')}:{' '} - {t('stackImplementations')} +
  • + {t("implementations")}: { t("stackImplementations")}
@@ -84,14 +77,13 @@ export const StackCode = () => {
{svgAlgo} - {t('algorithm')} + { t("algorithm") }
- {t('explanation')} + onClick={toggleInfoPanel}> + { t("explanation")}
@@ -101,5 +93,5 @@ export const StackCode = () => { {Code(funAction)}
- ); -}; + ) +} \ No newline at end of file diff --git a/src/animations/stack/components/StackControls.jsx b/src/animations/stack/components/StackControls.jsx index 1a9fa1e..7a90757 100644 --- a/src/animations/stack/components/StackControls.jsx +++ b/src/animations/stack/components/StackControls.jsx @@ -1,35 +1,20 @@ import { useDispatch, useSelector } from 'react-redux'; -import { - decrementHistoryStack, - incrementHistoryStack, - actionButton, - restoreRepeatStack, - restoreTimeId, - setHead, - setPlayingStack, - setTimeIdStack, - updateVisualizationStack, -} from '../../../store'; +import { decrementHistoryStack, incrementHistoryStack, actionButton, restoreRepeatStack, restoreTimeId, setHead, setPlayingStack, setTimeIdStack, updateVisualizationStack } from '../../../store'; import { Alert, Button, Snackbar, TextField } from '@mui/material'; import { useEffect, useState } from 'react'; import { BasicButton } from '../../../components'; -import { - svgPause, - svgBack, - svgPlay, - svgRepeat, - svgForward, -} from '../../../assets/svg/SvgConstans'; +import { svgPause, svgBack, svgPlay, svgRepeat, svgForward } from '../../../assets/svg/SvgConstans'; export const StackControls = () => { - const stackState = useSelector(state => state.stack); + const stackState = useSelector((state) => state.stack); const dispatch = useDispatch(); const [value, setValue] = useState(''); const [toast, setToast] = useState(''); const [open, setOpen] = useState(false); - const handleNumeroChange = event => { + const handleNumeroChange = (event) => { + setValue(parseInt(event.target.value)); dispatch(setHead(stackState.head)); @@ -39,21 +24,19 @@ export const StackControls = () => { } if (stackState.elementos.length > 6) { - setToast('La pila está llena'); + setToast("La pila está llena"); setOpen(true); return; } if (!stackState.elementos.includes(parseInt(event.target.value))) { - dispatch( - actionButton({ - action: 'push', - value: parseInt(event.target.value), - }) - ); + dispatch(actionButton({ + action: 'push', + value: parseInt(event.target.value), + })); } else { setValue(''); - setToast('El elemento ya existe'); + setToast("El elemento ya existe"); setOpen(true); } }; @@ -68,7 +51,7 @@ export const StackControls = () => { return run(stepHistory); }; - const onPlayPause = e => { + const onPlayPause = (e) => { e.preventDefault(); if (stackState.playing) { pause(); @@ -91,32 +74,29 @@ export const StackControls = () => { dispatch(restoreTimeId()); }; - const run = stepHistory => { + const run = (stepHistory) => { const timeId = []; stepHistory.forEach((item, i) => { - let timeoutId = setTimeout( - () => { - dispatch(incrementHistoryStack()); - dispatch(updateVisualizationStack(item)); - }, - i * (250 / stackState.timeStep) - ); + 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) - ); + let timeoutId = setTimeout(() => { + dispatch(restoreTimeId()); + }, stepHistory.length * (250 / stackState.timeStep)); + timeId.push(timeoutId); dispatch(setTimeIdStack(timeId)); + }; const goBackward = () => { + if (stackState.playing) { pause(); } @@ -131,6 +111,7 @@ export const StackControls = () => { }; const goForward = () => { + if (stackState.playing) { pause(); } @@ -159,17 +140,34 @@ export const StackControls = () => { dispatch(setHead(stackState.head)); if (stackState.head) { - dispatch( - actionButton({ - action: 'pop', - value: stackState.head.getValue(), - }) - ); + dispatch(actionButton({ + action: 'pop', + value: stackState.head.getValue(), + })); } else { - setToast('La pila está vacía'); + setToast("La pila está vacía"); setOpen(true); } - }; + } + + const handleSumergirButton = () => { + if (stackState.playing) { + pause(); + } + + dispatch(setHead(stackState.head)); + + if (stackState.head && stackState.head.getNext()) { + dispatch(actionButton({ + action: 'sumergir', + value: stackState.head.getValue(), + })); + run(stackState.stepHistory); + } else { + setToast("La pila está vacía o solo tiene un elemento"); + setOpen(true); + } + } const [svg, setSvg] = useState(svgPlay); @@ -181,14 +179,15 @@ export const StackControls = () => { } }, [stackState.playing]); + return ( -
+
{svgBack} - onPlayPause(e)}> + onPlayPause(e)}> {svg} @@ -199,22 +198,23 @@ export const StackControls = () => { {svgRepeat} +
-
+
+
+ +
- { - setOpen(false); - }} - anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} - > - { - setOpen(false); - }} - severity="error" - sx={{ width: '100%' }} - > + { setOpen(false) }} anchorOrigin={{ vertical: "bottom", horizontal: "right" }}> + { setOpen(false) }} severity="error" sx={{ width: '100%' }}> {toast}
); -}; +} \ No newline at end of file diff --git a/src/store/stack/stackSlice.js b/src/store/stack/stackSlice.js index e892c9c..8ebf9f2 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 { createSlice } from "@reduxjs/toolkit"; +import { Pop, Push, Sumergir } from "../../animations"; const initialState = { head: null, @@ -20,7 +20,7 @@ const initialState = { }; export const stackSlice = createSlice({ - name: 'stack', + name: "stack", initialState, reducers: { setHead: (state, action) => { @@ -44,8 +44,8 @@ export const stackSlice = createSlice({ state.srcHead = action.payload; }, - restoreTimeId: state => { - state.timeId.forEach(timeoutId => clearTimeout(timeoutId)); + restoreTimeId: (state) => { + state.timeId.forEach((timeoutId) => clearTimeout(timeoutId)); state.timeId = []; }, updateVisualizationStack: (state, action) => { @@ -67,7 +67,7 @@ export const stackSlice = createSlice({ elementos: elementos, }; }, - restoreRepeatStack: state => { + restoreRepeatStack: (state) => { const head = state.srcHead; state.head = head; state.history = -1; @@ -81,14 +81,14 @@ export const stackSlice = createSlice({ state.timeId.push(action.payload); }, - incrementHistoryStack: state => { + incrementHistoryStack: (state) => { return { ...state, history: state.history + 1, }; }, - decrementHistoryStack: state => { + decrementHistoryStack: (state) => { return { ...state, history: state.history - 1, @@ -114,10 +114,11 @@ const createRecordStack = (state, action) => { } }; -export const getAction = action => { +export const getAction = (action) => { const actions = { push: Push, pop: Pop, + sumergir: Sumergir }; return actions[action] || null; }; @@ -144,4 +145,4 @@ export const { setTimeIdStack, setPlayingStack, restoreRepeatStack, -} = stackSlice.actions; +} = stackSlice.actions; \ No newline at end of file From 02acab2bda43e7eba3a49b7a3087c3bf1b0e5824 Mon Sep 17 00:00:00 2001 From: jdbcamilo <149972010+jdbcamilo@users.noreply.github.com> Date: Sun, 18 May 2025 15:02:48 -0500 Subject: [PATCH 02/18] Estructura de datos Pilas completado --- src/animations/stack/algorithms/Nodo.js | 34 +- .../stack/algorithms/StackAlgorithm.jsx | 405 +++++++++++--- src/animations/stack/components/StackCode.jsx | 174 ++++-- .../stack/components/StackControls.jsx | 517 +++++++++++++----- src/store/stack/stackSlice.js | 106 +++- 5 files changed, 942 insertions(+), 294 deletions(-) diff --git a/src/animations/stack/algorithms/Nodo.js b/src/animations/stack/algorithms/Nodo.js index 1637c91..b058c96 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; \ No newline at end of file diff --git a/src/animations/stack/algorithms/StackAlgorithm.jsx b/src/animations/stack/algorithms/StackAlgorithm.jsx index ca3ce85..df1dbbd 100644 --- a/src/animations/stack/algorithms/StackAlgorithm.jsx +++ b/src/animations/stack/algorithms/StackAlgorithm.jsx @@ -1,100 +1,337 @@ 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); - - return stepHistory; +// Función mejorada para copiar la pila completa +const copiarPila = (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 mejorada para crear un registro de estado para la animación +const crearRegistro = (cabeza, primerConjunto = [], segundoConjunto = [], esHead = -1, indiceActual = 0) => { + return { + head: copiarPila(cabeza), + firstSet: [...primerConjunto], + secondSet: [...segundoConjunto], + isHead: esHead, + currentIndex: indiceActual + }; }; -export const Pop = (head, value, isHead) => { - const stepHistory = newTraceStack(head, isHead); +// Animación Push: Añadir un elemento al tope de la pila +export const Push = (cabeza, valor, esHead) => { + const historialPasos = []; + + // Paso 1: Visualizar el estado inicial + historialPasos.push(crearRegistro(cabeza, [], [], esHead, 0)); + + // Paso 2: Crear un nuevo nodo + const nuevoNodo = new Nodo(valor); + historialPasos.push(crearRegistro(cabeza, [valor], [], esHead, 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, [], [], 0, 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], [], esHead, 2)); + + // Paso 5: El nuevo nodo se convierte en el tope + cabeza = nuevoNodo; + // Actualizar cabeza + historialPasos.push(crearRegistro(cabeza, [], [], 0, 3)); + } + + // Paso 6: Mostrar estado final + historialPasos.push(crearRegistro(cabeza, [], [], -1, 4)); + + return { historialPasos, nuevaCabeza: cabeza }; +}; - 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); - head = temp.getNext(); - moveInHistoryRecordStack(stepHistory, head, [], [head.getValue()], head.getValue(), 3); - } else { - head = null; - moveInHistoryRecordStack(stepHistory, head, [], [], -1, -1); +// Animación Pop: Eliminar un elemento del tope de la pila +export const Pop = (cabeza, esHead) => { + const historialPasos = []; + + // Paso 1: Visualizar el estado inicial + historialPasos.push(crearRegistro(cabeza, [], [], esHead, 0)); + + // Paso 2: Verificar si la pila está vacía + if (cabeza === null) { + historialPasos.push(crearRegistro(cabeza, [], [], -1, 1)); + return { historialPasos, nuevaCabeza: cabeza }; } + + // Paso 3: Guardar el valor a eliminar + const valorEliminado = cabeza.getValue(); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 0, 2)); + + // Paso 4: Actualizar el tope para que apunte al siguiente elemento + cabeza = cabeza.getNext(); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], esHead, 3)); + + // Paso 5: Mostrar estado final + historialPasos.push(crearRegistro(cabeza, [], [], -1, 4)); + + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; +}; - return stepHistory; -} +// Animación Sumergir: Mover el elemento del tope al fondo de la pila +export const Sumergir = (cabeza, esHead) => { + const historialPasos = []; + + // Paso 1: Visualizar el estado inicial + historialPasos.push(crearRegistro(cabeza, [], [], esHead, 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, 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], [], 0, 2)); + + // Paso 4: Actualizar el tope para que apunte al segundo elemento + cabeza = cabeza.getNext(); + historialPasos.push(crearRegistro(cabeza, [valorTope], [], 0, 3)); + + // 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()], -1, 4)); + } + + // 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()], -1, 5)); + + // Paso 7: Mostrar estado final + historialPasos.push(crearRegistro(cabeza, [], [], -1, 5)); + + return { historialPasos, nuevaCabeza: cabeza }; +}; -export const Sumergir = (head, value, isHead) => { - const stepHistory = newTraceStack(head, isHead); +// Animación Insertar: Insertar un elemento en una posición específica +// Posición 0 = fondo, posición 1 = segundo desde fondo, etc. Tope = última posición +export const Insertar = (cabeza, valor, posicion, esHead) => { + const historialPasos = []; - // Si la pila está vacía o tiene un solo elemento, no hay nada que sumergir - if (head === null || head.getNext() === null) { - moveInHistoryRecordStack(stepHistory, head, [], [], (head ? head.getValue() : -1), 0); - return stepHistory; + // Paso 1: Visualizar el estado inicial + historialPasos.push(crearRegistro(cabeza, [], [], esHead, 0)); + + // Paso 2: Crear un nuevo nodo + const nuevoNodo = new Nodo(valor); + historialPasos.push(crearRegistro(cabeza, [valor], [], esHead, 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, [], [], 0, 2)); + historialPasos.push(crearRegistro(cabeza, [], [], -1, 5)); + return { historialPasos, nuevaCabeza: cabeza }; + } else { + historialPasos.push(crearRegistro(cabeza, [], [], -1, 1)); + return { historialPasos, nuevaCabeza: cabeza }; + } } - // Guardamos la referencia al nodo actual head que moveremos al final - const oldHead = head; + // Paso 4: Contar el tamaño actual de la pila + let tamanoPila = 0; + let temp = cabeza; + while (temp !== null) { + tamanoPila++; + temp = temp.getNext(); + } - // Marcamos el nodo cabeza que se va a sumergir - moveInHistoryRecordStack(stepHistory, head, [head.getValue()], [], head.getValue(), 0); + // Paso 5: Si la posición es mayor o igual al tamaño, insertar en el tope (push) + if (posicion >= tamanoPila) { + nuevoNodo.setNext(cabeza); + cabeza = nuevoNodo; + historialPasos.push(crearRegistro(cabeza, [], [], 0, 2)); + historialPasos.push(crearRegistro(cabeza, [], [], -1, 5)); + return { historialPasos, nuevaCabeza: cabeza }; + } - // El nuevo head será el siguiente nodo - const newHead = head.getNext(); - moveInHistoryRecordStack(stepHistory, head, [head.getValue()], [newHead.getValue()], head.getValue(), 1); + // Paso 6: Si la posición es 0, insertar al fondo + if (posicion === 0) { + // Encontrar el último nodo + let nodoActual = cabeza; + historialPasos.push(crearRegistro(cabeza, [valor], [], esHead, 3)); + + while (nodoActual.getNext() !== null) { + nodoActual = nodoActual.getNext(); + historialPasos.push(crearRegistro(cabeza, [valor], [nodoActual.getValue()], -1, 3)); + } + + // Insertar al final + nodoActual.setNext(nuevoNodo); + historialPasos.push(crearRegistro(cabeza, [], [nuevoNodo.getValue()], -1, 4)); + } + // Paso 7: Insertar en posición intermedia + else { + // Convertir posición (desde el fondo) a índice (desde el tope) + let indiceDesdeTop = tamanoPila - posicion - 1; + let nodoActual = cabeza; + let contador = 0; + historialPasos.push(crearRegistro(cabeza, [valor], [], esHead, 3)); + + // Navegar hasta la posición anterior + while (contador < indiceDesdeTop) { + nodoActual = nodoActual.getNext(); + contador++; + historialPasos.push(crearRegistro(cabeza, [valor], [nodoActual.getValue()], -1, 3)); + } + + // Insertar el nuevo nodo + nuevoNodo.setNext(nodoActual.getNext()); + historialPasos.push(crearRegistro(cabeza, [valor], [nodoActual.getValue()], -1, 4)); + + nodoActual.setNext(nuevoNodo); + historialPasos.push(crearRegistro(cabeza, [], [nuevoNodo.getValue()], -1, 4)); + } + + // Paso 8: Mostrar estado final + historialPasos.push(crearRegistro(cabeza, [], [], -1, 5)); - // Ahora actualizamos head para que sea el segundo nodo - head = newHead; + return { historialPasos, nuevaCabeza: cabeza }; +}; + +// Animación Extraer: Eliminar un elemento en una posición específica +// Posición 0 = fondo, posición 1 = segundo desde fondo, etc. Tope = última posición +export const Extraer = (cabeza, posicion, esHead) => { + const historialPasos = []; + + // Paso 1: Visualizar el estado inicial + historialPasos.push(crearRegistro(cabeza, [], [], esHead, 0)); + + // Paso 2: Verificar si la pila está vacía + if (cabeza === null) { + historialPasos.push(crearRegistro(cabeza, [], [], -1, 1)); + return { historialPasos, nuevaCabeza: cabeza }; + } - // Encontrar el último nodo - let current = head; - let last = null; - while (current !== null) { - last = current; - current = current.getNext(); + // Paso 3: Contar el tamaño de la pila + let tamanoPila = 0; + let temp = cabeza; + while (temp !== null) { + tamanoPila++; + temp = temp.getNext(); } - // Marcar el último nodo que encontramos - moveInHistoryRecordStack(stepHistory, head, [oldHead.getValue(), last.getValue()], [], newHead.getValue(), 2); + // Paso 4: Verificar que la posición sea válida + if (posicion >= tamanoPila) { + historialPasos.push(crearRegistro(cabeza, [], [], -1, 1)); + return { historialPasos, nuevaCabeza: cabeza }; + } - // Desconectar el nodo original (oldHead) de la lista para evitar ciclos - oldHead.setNext(null); + let valorEliminado; - // Conectar el último nodo con el nodo sumergido - last.setNext(oldHead); - moveInHistoryRecordStack(stepHistory, head, [], [last.getValue(), oldHead.getValue()], newHead.getValue(), 3); + // Paso 5: Si la posición es la última (tope), eliminar del tope (pop) + if (posicion === tamanoPila - 1) { + valorEliminado = cabeza.getValue(); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 0, 2)); + + cabeza = cabeza.getNext(); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], esHead, 3)); + } + // Paso 6: Eliminar en otras posiciones + else { + historialPasos.push(crearRegistro(cabeza, [], [], esHead, 2)); + + // Si la posición es 0 (fondo), usar lógica especial + if (posicion === 0) { + // Caso especial: eliminar el último nodo de la cadena + if (tamanoPila === 1) { + // Solo hay un elemento, eliminarlo + valorEliminado = cabeza.getValue(); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 0, 3)); + cabeza = null; + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], -1, 4)); + } else { + // Encontrar el penúltimo nodo + let nodoActual = cabeza; + while (nodoActual.getNext().getNext() !== null) { + nodoActual = nodoActual.getNext(); + historialPasos.push(crearRegistro(cabeza, [], [nodoActual.getValue()], -1, 2)); + } + + // Eliminar el último nodo + valorEliminado = nodoActual.getNext().getValue(); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [nodoActual.getValue()], -1, 3)); + + nodoActual.setNext(null); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], -1, 4)); + } + } + // Eliminar en posición intermedia + else { + // Convertir posición (desde el fondo) a índice (desde el tope) + let indiceDesdeTop = tamanoPila - posicion - 1; + let nodoActual = cabeza; + let contador = 0; + + // Navegar hasta el nodo anterior al que se va a eliminar + while (contador < indiceDesdeTop - 1) { + nodoActual = nodoActual.getNext(); + contador++; + historialPasos.push(crearRegistro(cabeza, [], [nodoActual.getValue()], -1, 2)); + } + + // Guardar el valor a eliminar + valorEliminado = nodoActual.getNext().getValue(); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [nodoActual.getValue()], -1, 3)); + + // Eliminar el nodo de la cadena + nodoActual.setNext(nodoActual.getNext().getNext()); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], -1, 4)); + } + } - // Mostrar la nueva estructura con el head actualizado - moveInHistoryRecordStack(stepHistory, head, [], [], head.getValue(), 4); + // Paso 7: Mostrar estado final + historialPasos.push(crearRegistro(cabeza, [], [], -1, 4)); - return stepHistory; + 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`}
-
+
{` nuevoNodo = new Nodo(valor)`}
+
{`if (cabeza === null) cabeza = nuevoNodo`}
+
{`nuevoNodo.setNext(cabeza)`}
+
{`cabeza = nuevoNodo`}
+ ) } export const PopCode = ({ codeRef }) => { return ( -
{`if empty, do nothing`}
-
{`tmp = head`}
-
{`head = tmp.next`}
-
{`delete tmp`}
+
{`if (cabeza === null) return error`}
+
{`valorEliminado = cabeza.getValue()`}
+
{`cabeza = cabeza.getNext()`}
) } @@ -102,13 +339,39 @@ export const PopCode = ({ codeRef }) => { export const SumergirCode = ({ codeRef }) => { return ( -
{`if empty or single node, do nothing`}
-
{`newHead = head.next`}
-
{`find last node in list`}
-
{`last.next = head`}
-
{`head.next = null`}
-
{`head = newHead`}
+
{`if (cabeza === null || cabeza.getNext() === null) return`}
+
{`nodoTope = cabeza`}
+
{`cabeza = cabeza.getNext()`}
+
{`while (nodoActual.getNext() !== null) nodoActual = nodoActual.getNext()`}
+
{`nodoTope.setNext(null); nodoActual.setNext(nodoTope)`}
) - -} \ No newline at end of file +} + +export const InsertarCode = ({ codeRef }) => { + return ( + +
{`nuevoNodo = new Nodo(valor)`}
+
{`if (cabeza === null && posicion === 0) cabeza = nuevoNodo`}
+
{`while (temp !== null) { tamanoPila++; temp = temp.getNext() }`}
+
{`nuevoNodo.setNext(cabeza); cabeza = nuevoNodo`}
+
{`ultimo.setNext(nuevoNodo)`}
+
{`indiceDesdeTop = tamanoPila - posicion - 1`}
+
+ ) +} + +export const ExtraerCode = ({ codeRef }) => { + return ( + +
{`if (cabeza === null) return error`}
+
{`while (temp !== null) { tamanoPila++; temp = temp.getNext() }`}
+
{`if (posicion >= tamanoPila) return error`}
+
{`valorEliminado = cabeza.getValue(); cabeza = cabeza.getNext()`}
+
{`penúltimo.setNext(null)`}
+
{`indiceDesdeTop = tamanoPila - posicion - 1`}
+
{`valorEliminado = nodoActual.getNext().getValue()`}
+
{`nodoActual.setNext(nodoActual.getNext().getNext())`}
+
+ ); +}; \ No newline at end of file diff --git a/src/animations/stack/components/StackCode.jsx b/src/animations/stack/components/StackCode.jsx index 989860a..64b3985 100644 --- a/src/animations/stack/components/StackCode.jsx +++ b/src/animations/stack/components/StackCode.jsx @@ -1,97 +1,171 @@ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; -import { PopCode, PushCode, SumergirCode } from '../algorithms'; +import { PopCode, PushCode, SumergirCode, InsertarCode, ExtraerCode } from '../algorithms'; import { SvgIconDropdown, svgAlgo } from '../../../assets/svg/SvgConstans'; import { useTranslation } from 'react-i18next'; export const StackCode = () => { - const codeRef = useRef(null); const [prevCurrentLine, setPrevCurrentLine] = useState(null); const { funAction, history, stepHistory } = useSelector(state => state.stack); const [infoVisible, setInfoVisible] = useState(false); - const { t } = useTranslation() + const { t } = useTranslation(); + // Efecto para actualizar la línea destacada en el código useEffect(() => { + // 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; + } - if (history === -1 || history >= stepHistory.length) 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 - default: null; + case 'push': + return ; + case 'pop': + return ; + case 'sumergir': + return ; + case 'insertar': + return ; + case 'extraer': + return ; + default: + 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 && ( -
-
    -
  • - {t("definition")}: {t("stackDefinition")} -
  • -
  • - { t("principalOperations") } -
      -
    • Push: { t("pushExplanation") }
    • -
    • Pop: { t("popExplanation") }
    • -
    • Sumergir: Mueve el elemento del tope de la pila al final
    • -
    -
  • -
  • - {t("utility")}: { t("stackUsageExplanation") } -
  • -
  • - {t("implementations")}: { t("stackImplementations")} -
  • -
+
+
+
    +
  • + {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."} +
  • +
+
)} - {/* Div header */} -
+ {/* Header del panel de código */} +
{svgAlgo} - { t("algorithm") } + {t("algorithm") || "Algoritmo"}
- - { t("explanation")} +
-
- {Code(funAction)} + + {/* Contenido del código */} +
+
+ {renderCode(funAction)} +
- ) -} \ No newline at end of file + ); +}; \ No newline at end of file diff --git a/src/animations/stack/components/StackControls.jsx b/src/animations/stack/components/StackControls.jsx index 7a90757..b90dd9d 100644 --- a/src/animations/stack/components/StackControls.jsx +++ b/src/animations/stack/components/StackControls.jsx @@ -1,7 +1,17 @@ import { useDispatch, useSelector } from 'react-redux'; -import { decrementHistoryStack, incrementHistoryStack, actionButton, restoreRepeatStack, restoreTimeId, setHead, setPlayingStack, setTimeIdStack, updateVisualizationStack } from '../../../store'; +import { + decrementHistoryStack, + incrementHistoryStack, + actionButton, + restoreRepeatStack, + restoreTimeId, + setHead, + setPlayingStack, + setTimeIdStack, + updateVisualizationStack +} from '../../../store'; import { Alert, Button, Snackbar, TextField } from '@mui/material'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { BasicButton } from '../../../components'; import { svgPause, svgBack, svgPlay, svgRepeat, svgForward } from '../../../assets/svg/SvgConstans'; @@ -9,246 +19,481 @@ 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)); + // Handlers para cambios en inputs + const handleValueChange = (event) => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue !== null)) { + setValue(inputValue); + } + }; - dispatch(setHead(stackState.head)); + const handleInsertPositionChange = (event) => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue >= 0)) { + setInsertPosition(inputValue); + } + }; - if (stackState.playing) { - pause(); + const handleExtractPositionChange = (event) => { + const inputValue = event.target.value; + if (inputValue === '' || (!isNaN(inputValue) && inputValue >= 0)) { + setExtractPosition(inputValue); } + }; - if (stackState.elementos.length > 6) { - setToast("La pila está llena"); - setOpen(true); + // Función para mostrar mensajes de error + const showError = useCallback((message) => { + setToast(message); + setOpen(true); + }, []); + + // 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; } - if (!stackState.elementos.includes(parseInt(event.target.value))) { - dispatch(actionButton({ - action: 'push', - value: parseInt(event.target.value), - })); - } else { - setValue(''); - setToast("El elemento ya existe"); - setOpen(true); + 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()); + + setIsAnimating(true); + + } catch (error) { + console.error('Error executing animation:', error); + showError("Error al ejecutar la animación"); + setIsAnimating(false); } - }; + }, [dispatch, stackState.head, isAnimating, showError]); + + // Función para ejecutar la animación paso a paso + const runAnimation = useCallback((stepHistory) => { + if (!stepHistory || stepHistory.length === 0) { + setIsAnimating(false); + return; + } + + dispatch(setPlayingStack(true)); + const timeIds = []; + + // Ejecutar cada paso con retraso + stepHistory.forEach((step, index) => { + const timeoutId = setTimeout(() => { + dispatch(incrementHistoryStack()); + dispatch(updateVisualizationStack(step)); + }, index * (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); + }, stepHistory.length * (250 / (stackState.timeStep || 1))); + + timeIds.push(finalTimeoutId); + dispatch(setTimeIdStack(timeIds)); + }, [dispatch, stackState.timeStep]); + + // 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); 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); }; - const onPlayPause = (e) => { - e.preventDefault(); - if (stackState.playing) { - pause(); - } else { - play(); + const handleSumergirButton = () => { + if (!stackState.head) { + showError("La pila está vacía"); + return; } - }; - const play = () => { - dispatch(setPlayingStack(true)); - if (stackState.history === -1) { - run(stackState.stepHistory); - } else { - keepPlaying(); + if (!stackState.head.getNext()) { + showError("La pila necesita al menos 2 elementos para sumergir"); + return; } - }; - const pause = () => { - dispatch(setPlayingStack(false)); - dispatch(restoreTimeId()); + const topValue = stackState.head.getValue(); + executeAnimation('sumergir', topValue); }; - const run = (stepHistory) => { - const timeId = []; - stepHistory.forEach((item, i) => { - let timeoutId = setTimeout(() => { - dispatch(incrementHistoryStack()); - dispatch(updateVisualizationStack(item)); - }, i * (250 / stackState.timeStep)); + 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; + } - timeId.push(timeoutId); - }); + if (position === null || position < 0) { + showError("Ingresa una posición válida (≥ 0)"); + return; + } - let timeoutId = setTimeout(() => { - dispatch(restoreTimeId()); - }, stepHistory.length * (250 / stackState.timeStep)); + 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; + } - timeId.push(timeoutId); - dispatch(setTimeIdStack(timeId)); + 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); + setValue(''); + setInsertPosition(''); }; - const goBackward = () => { + const handleExtraerButton = () => { + if (!stackState.head) { + showError("La pila está vacía"); + return; + } - if (stackState.playing) { - pause(); + const position = extractPosition === '' ? null : parseInt(extractPosition); + + if (position === null || position < 0) { + showError("Ingresa una posición válida (≥ 0)"); + return; } - const stepHistory = stackState.stepHistory; - const current = stackState.history; - if (current > 0) { - const item = stepHistory[current - 1]; - dispatch(decrementHistoryStack()); - dispatch(updateVisualizationStack(item)); + const maxPosition = stackState.elementos ? stackState.elementos.length - 1 : 0; + if (position > maxPosition) { + showError(`La posición máxima válida es ${maxPosition}`); + return; } - }; - const goForward = () => { + executeAnimation('extraer', null, position); + setExtractPosition(''); + }; + // Controles de reproducción manual + const onPlayPause = (e) => { + e.preventDefault(); if (stackState.playing) { pause(); + } else { + play(); } + }; - const stepHistory = stackState.stepHistory; - const current = stackState.history; - if (current < stepHistory.length - 1) { - const item = stepHistory[current + 1]; - dispatch(incrementHistoryStack()); - dispatch(updateVisualizationStack(item)); + const play = () => { + 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) { + runAnimation(remainingSteps); + } else { + runAnimation(stackState.stepHistory); + } } }; - const repeat = () => { + const pause = () => { + dispatch(setPlayingStack(false)); dispatch(restoreTimeId()); - dispatch(restoreRepeatStack()); - dispatch(setPlayingStack(true)); - run(stackState.stepHistory); + setIsAnimating(false); }; - const handlePopButton = () => { + const goBackward = () => { 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); + const currentHistory = stackState.history || -1; + if (currentHistory > 0 && stackState.stepHistory) { + const previousStep = stackState.stepHistory[currentHistory - 1]; + dispatch(decrementHistoryStack()); + dispatch(updateVisualizationStack(previousStep)); } - } + }; - const handleSumergirButton = () => { + const goForward = () => { if (stackState.playing) { pause(); } - dispatch(setHead(stackState.head)); - - if (stackState.head && stackState.head.getNext()) { - dispatch(actionButton({ - action: 'sumergir', - value: stackState.head.getValue(), - })); - run(stackState.stepHistory); - } else { - setToast("La pila está vacía o solo tiene un elemento"); - setOpen(true); + const currentHistory = stackState.history || -1; + if (stackState.stepHistory && currentHistory < stackState.stepHistory.length - 1) { + const nextStep = stackState.stepHistory[currentHistory + 1]; + dispatch(incrementHistoryStack()); + dispatch(updateVisualizationStack(nextStep)); } - } + }; - const [svg, setSvg] = useState(svgPlay); + const repeat = () => { + if (stackState.stepHistory && stackState.stepHistory.length > 0) { + dispatch(restoreTimeId()); + dispatch(restoreRepeatStack()); + setIsAnimating(true); + runAnimation(stackState.stepHistory); + } + }; + // Efecto para manejar cambios en stepHistory useEffect(() => { - if (stackState.playing) { - setSvg(svgPause); - } else { - setSvg(svgPlay); + if (stackState.stepHistory && + stackState.stepHistory.length > 0 && + stackState.funAction && + !stackState.playing && + isAnimating) { + runAnimation(stackState.stepHistory); } - }, [stackState.playing]); + }, [stackState.stepHistory, stackState.funAction, stackState.playing, isAnimating, runAnimation]); + // 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 */} +
- { setOpen(false) }} anchorOrigin={{ vertical: "bottom", horizontal: "right" }}> - { setOpen(false) }} severity="error" sx={{ width: '100%' }}> + {/* Notificaciones */} + setOpen(false)} + anchorOrigin={{ vertical: "bottom", horizontal: "right" }} + > + setOpen(false)} + severity="error" + sx={{ width: '100%' }} + > {toast}
); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/store/stack/stackSlice.js b/src/store/stack/stackSlice.js index 8ebf9f2..d5dfb32 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, Sumergir } from "../../animations"; +import { Pop, Push, Sumergir, Insertar, Extraer } from "../../animations"; const initialState = { head: null, @@ -24,8 +24,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); @@ -43,6 +44,9 @@ export const stackSlice = createSlice({ 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 +55,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 +63,12 @@ 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; }, restoreRepeatStack: (state) => { const head = state.srcHead; @@ -74,25 +77,21 @@ export const stackSlice = createSlice({ state.firstSet = []; state.secondSet = []; 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 +100,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,7 +152,9 @@ export const getAction = (action) => { const actions = { push: Push, pop: Pop, - sumergir: Sumergir + sumergir: Sumergir, + insertar: Insertar, + extraer: Extraer }; return actions[action] || null; }; @@ -134,15 +170,25 @@ export const verificar = (head, elemento) => { return false; }; +const { + restoreStack, + restoreTimeId, + updateVisualizationStack, + restoreRepeatStack +} = stackSlice.actions; + export const { actionButton, - restoreStack, - restoreTimeId, setHead, decrementHistoryStack, incrementHistoryStack, - updateVisualizationStack, setTimeIdStack, setPlayingStack, - restoreRepeatStack, -} = stackSlice.actions; \ No newline at end of file +} = stackSlice.actions; + +export { + restoreStack, + restoreTimeId, + updateVisualizationStack, + restoreRepeatStack +}; \ No newline at end of file From a55feba72cda8c6175372d784e0b23fa0ef50436 Mon Sep 17 00:00:00 2001 From: FrankGarcia Date: Tue, 24 Jun 2025 08:59:11 -0500 Subject: [PATCH 03/18] Bugs arreglados pilas --- .../stack/algorithms/StackAlgorithm.jsx | 271 +++++------ src/animations/stack/algorithms/helpers.jsx | 7 +- .../stack/components/StackChart.jsx | 2 +- .../stack/components/StackControls.jsx | 419 +++++++++++------- 4 files changed, 402 insertions(+), 297 deletions(-) diff --git a/src/animations/stack/algorithms/StackAlgorithm.jsx b/src/animations/stack/algorithms/StackAlgorithm.jsx index df1dbbd..16285a3 100644 --- a/src/animations/stack/algorithms/StackAlgorithm.jsx +++ b/src/animations/stack/algorithms/StackAlgorithm.jsx @@ -1,161 +1,169 @@ -import { newTraceStack, moveInHistoryRecordStack } from "./helpers"; +import { newTraceStack, moveInHistoryRecordStack } from './helpers'; import Nodo from './Nodo'; // Función mejorada para copiar la pila completa -const copiarPila = (cabeza) => { +const copiarPila = 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 mejorada para crear un registro de estado para la animación -const crearRegistro = (cabeza, primerConjunto = [], segundoConjunto = [], esHead = -1, indiceActual = 0) => { +const crearRegistro = ( + cabeza, + primerConjunto = [], + segundoConjunto = [], + indiceActual = 0 +) => { return { head: copiarPila(cabeza), firstSet: [...primerConjunto], secondSet: [...segundoConjunto], - isHead: esHead, - currentIndex: indiceActual + isHead: cabeza ? cabeza.getValue() : -1, + currentIndex: indiceActual, }; }; // Animación Push: Añadir un elemento al tope de la pila -export const Push = (cabeza, valor, esHead) => { +export const Push = (cabeza, valor) => { const historialPasos = []; - + // Paso 1: Visualizar el estado inicial - historialPasos.push(crearRegistro(cabeza, [], [], esHead, 0)); - + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + // Paso 2: Crear un nuevo nodo const nuevoNodo = new Nodo(valor); - historialPasos.push(crearRegistro(cabeza, [valor], [], esHead, 1)); - + 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, [], [], 0, 3)); - } + 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], [], esHead, 2)); - + historialPasos.push(crearRegistro(cabeza, [valor], [], 2)); + // Paso 5: El nuevo nodo se convierte en el tope cabeza = nuevoNodo; // Actualizar cabeza - historialPasos.push(crearRegistro(cabeza, [], [], 0, 3)); + historialPasos.push(crearRegistro(cabeza, [], [], 3)); } - + // Paso 6: Mostrar estado final - historialPasos.push(crearRegistro(cabeza, [], [], -1, 4)); - + historialPasos.push(crearRegistro(cabeza, [], [], 4)); + return { historialPasos, nuevaCabeza: cabeza }; }; // Animación Pop: Eliminar un elemento del tope de la pila -export const Pop = (cabeza, esHead) => { +export const Pop = cabeza => { const historialPasos = []; - + // Paso 1: Visualizar el estado inicial - historialPasos.push(crearRegistro(cabeza, [], [], esHead, 0)); - + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + // Paso 2: Verificar si la pila está vacía if (cabeza === null) { - historialPasos.push(crearRegistro(cabeza, [], [], -1, 1)); + historialPasos.push(crearRegistro(cabeza, [], [], 1)); return { historialPasos, nuevaCabeza: cabeza }; } - + // Paso 3: Guardar el valor a eliminar const valorEliminado = cabeza.getValue(); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 0, 2)); - + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 2)); + // Paso 4: Actualizar el tope para que apunte al siguiente elemento cabeza = cabeza.getNext(); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], esHead, 3)); - + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 3)); + // Paso 5: Mostrar estado final - historialPasos.push(crearRegistro(cabeza, [], [], -1, 4)); - + 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, esHead) => { +export const Sumergir = cabeza => { const historialPasos = []; - + // Paso 1: Visualizar el estado inicial - historialPasos.push(crearRegistro(cabeza, [], [], esHead, 0)); - + 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, 1)); + 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], [], 0, 2)); - + historialPasos.push(crearRegistro(cabeza, [valorTope], [], 2)); + // Paso 4: Actualizar el tope para que apunte al segundo elemento cabeza = cabeza.getNext(); - historialPasos.push(crearRegistro(cabeza, [valorTope], [], 0, 3)); - + historialPasos.push(crearRegistro(cabeza, [valorTope], [], 3)); + // 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()], -1, 4)); + historialPasos.push( + crearRegistro(cabeza, [valorTope], [nodoActual.getValue()], 4) + ); } - + // 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()], -1, 5)); - + historialPasos.push( + crearRegistro(cabeza, [valorTope], [nodoActual.getValue()], 5) + ); + // Paso 7: Mostrar estado final - historialPasos.push(crearRegistro(cabeza, [], [], -1, 5)); - + historialPasos.push(crearRegistro(cabeza, [], [], 5)); + return { historialPasos, nuevaCabeza: cabeza }; }; // Animación Insertar: Insertar un elemento en una posición específica -// Posición 0 = fondo, posición 1 = segundo desde fondo, etc. Tope = última posición -export const Insertar = (cabeza, valor, posicion, esHead) => { +export const Insertar = (cabeza, valor, posicion) => { const historialPasos = []; - + // Paso 1: Visualizar el estado inicial - historialPasos.push(crearRegistro(cabeza, [], [], esHead, 0)); - + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + // Paso 2: Crear un nuevo nodo const nuevoNodo = new Nodo(valor); - historialPasos.push(crearRegistro(cabeza, [valor], [], esHead, 1)); - + 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, [], [], 0, 2)); - historialPasos.push(crearRegistro(cabeza, [], [], -1, 5)); + historialPasos.push(crearRegistro(cabeza, [], [], 2)); + historialPasos.push(crearRegistro(cabeza, [], [], 5)); return { historialPasos, nuevaCabeza: cabeza }; } else { - historialPasos.push(crearRegistro(cabeza, [], [], -1, 1)); + 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; @@ -163,74 +171,79 @@ export const Insertar = (cabeza, valor, posicion, esHead) => { tamanoPila++; temp = temp.getNext(); } - + // Paso 5: Si la posición es mayor o igual al tamaño, insertar en el tope (push) if (posicion >= tamanoPila) { nuevoNodo.setNext(cabeza); cabeza = nuevoNodo; - historialPasos.push(crearRegistro(cabeza, [], [], 0, 2)); - historialPasos.push(crearRegistro(cabeza, [], [], -1, 5)); + historialPasos.push(crearRegistro(cabeza, [], [], 2)); + historialPasos.push(crearRegistro(cabeza, [], [], 5)); return { historialPasos, nuevaCabeza: cabeza }; } - + // Paso 6: Si la posición es 0, insertar al fondo if (posicion === 0) { // Encontrar el último nodo let nodoActual = cabeza; - historialPasos.push(crearRegistro(cabeza, [valor], [], esHead, 3)); - + historialPasos.push(crearRegistro(cabeza, [valor], [], 3)); + while (nodoActual.getNext() !== null) { nodoActual = nodoActual.getNext(); - historialPasos.push(crearRegistro(cabeza, [valor], [nodoActual.getValue()], -1, 3)); + historialPasos.push( + crearRegistro(cabeza, [valor], [nodoActual.getValue()], 3) + ); } - + // Insertar al final nodoActual.setNext(nuevoNodo); - historialPasos.push(crearRegistro(cabeza, [], [nuevoNodo.getValue()], -1, 4)); - } + historialPasos.push(crearRegistro(cabeza, [], [nuevoNodo.getValue()], 4)); + } // Paso 7: Insertar en posición intermedia else { // Convertir posición (desde el fondo) a índice (desde el tope) let indiceDesdeTop = tamanoPila - posicion - 1; let nodoActual = cabeza; let contador = 0; - historialPasos.push(crearRegistro(cabeza, [valor], [], esHead, 3)); - + historialPasos.push(crearRegistro(cabeza, [valor], [], 3)); + // Navegar hasta la posición anterior while (contador < indiceDesdeTop) { nodoActual = nodoActual.getNext(); contador++; - historialPasos.push(crearRegistro(cabeza, [valor], [nodoActual.getValue()], -1, 3)); + historialPasos.push( + crearRegistro(cabeza, [valor], [nodoActual.getValue()], 3) + ); } - + // Insertar el nuevo nodo nuevoNodo.setNext(nodoActual.getNext()); - historialPasos.push(crearRegistro(cabeza, [valor], [nodoActual.getValue()], -1, 4)); - + historialPasos.push( + crearRegistro(cabeza, [valor], [nodoActual.getValue()], 4) + ); + nodoActual.setNext(nuevoNodo); - historialPasos.push(crearRegistro(cabeza, [], [nuevoNodo.getValue()], -1, 4)); + historialPasos.push(crearRegistro(cabeza, [], [nuevoNodo.getValue()], 4)); } - + // Paso 8: Mostrar estado final - historialPasos.push(crearRegistro(cabeza, [], [], -1, 5)); - + historialPasos.push(crearRegistro(cabeza, [], [], 5)); + return { historialPasos, nuevaCabeza: cabeza }; }; // Animación Extraer: Eliminar un elemento en una posición específica -// Posición 0 = fondo, posición 1 = segundo desde fondo, etc. Tope = última posición -export const Extraer = (cabeza, posicion, esHead) => { +export const Extraer = (cabeza, posicion) => { const historialPasos = []; - + // Paso 1: Visualizar el estado inicial - historialPasos.push(crearRegistro(cabeza, [], [], esHead, 0)); - + historialPasos.push(crearRegistro(cabeza, [], [], 0)); + // Paso 2: Verificar si la pila está vacía if (cabeza === null) { - historialPasos.push(crearRegistro(cabeza, [], [], -1, 1)); + historialPasos.push(crearRegistro(cabeza, [], [], 1)); return { historialPasos, nuevaCabeza: cabeza }; } - + // Paso 3: Contar el tamaño de la pila let tamanoPila = 0; let temp = cabeza; @@ -238,79 +251,87 @@ export const Extraer = (cabeza, posicion, esHead) => { tamanoPila++; temp = temp.getNext(); } - + // Paso 4: Verificar que la posición sea válida if (posicion >= tamanoPila) { - historialPasos.push(crearRegistro(cabeza, [], [], -1, 1)); + historialPasos.push(crearRegistro(cabeza, [], [], 1)); return { historialPasos, nuevaCabeza: cabeza }; } - + let valorEliminado; - + // Paso 5: Si la posición es la última (tope), eliminar del tope (pop) if (posicion === tamanoPila - 1) { valorEliminado = cabeza.getValue(); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 0, 2)); - + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 2)); + cabeza = cabeza.getNext(); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], esHead, 3)); - } + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 3)); + } // Paso 6: Eliminar en otras posiciones else { - historialPasos.push(crearRegistro(cabeza, [], [], esHead, 2)); - + historialPasos.push(crearRegistro(cabeza, [], [], 2)); + // Si la posición es 0 (fondo), usar lógica especial if (posicion === 0) { // Caso especial: eliminar el último nodo de la cadena if (tamanoPila === 1) { // Solo hay un elemento, eliminarlo valorEliminado = cabeza.getValue(); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 0, 3)); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 3)); cabeza = null; - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], -1, 4)); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 4)); } else { // Encontrar el penúltimo nodo let nodoActual = cabeza; while (nodoActual.getNext().getNext() !== null) { nodoActual = nodoActual.getNext(); - historialPasos.push(crearRegistro(cabeza, [], [nodoActual.getValue()], -1, 2)); + historialPasos.push( + crearRegistro(cabeza, [], [nodoActual.getValue()], 2) + ); } - + // Eliminar el último nodo valorEliminado = nodoActual.getNext().getValue(); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [nodoActual.getValue()], -1, 3)); - + historialPasos.push( + crearRegistro(cabeza, [valorEliminado], [nodoActual.getValue()], 3) + ); + nodoActual.setNext(null); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], -1, 4)); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 4)); } - } + } // Eliminar en posición intermedia else { // Convertir posición (desde el fondo) a índice (desde el tope) let indiceDesdeTop = tamanoPila - posicion - 1; let nodoActual = cabeza; let contador = 0; - + // Navegar hasta el nodo anterior al que se va a eliminar while (contador < indiceDesdeTop - 1) { nodoActual = nodoActual.getNext(); contador++; - historialPasos.push(crearRegistro(cabeza, [], [nodoActual.getValue()], -1, 2)); + historialPasos.push( + crearRegistro(cabeza, [], [nodoActual.getValue()], 2) + ); } - + // Guardar el valor a eliminar valorEliminado = nodoActual.getNext().getValue(); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [nodoActual.getValue()], -1, 3)); - + historialPasos.push( + crearRegistro(cabeza, [valorEliminado], [nodoActual.getValue()], 3) + ); + // Eliminar el nodo de la cadena nodoActual.setNext(nodoActual.getNext().getNext()); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], -1, 4)); + historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 4)); } } - + // Paso 7: Mostrar estado final - historialPasos.push(crearRegistro(cabeza, [], [], -1, 4)); - + historialPasos.push(crearRegistro(cabeza, [], [], 4)); + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; }; @@ -322,9 +343,9 @@ export const PushCode = ({ codeRef }) => {
{`if (cabeza === null) cabeza = nuevoNodo`}
{`nuevoNodo.setNext(cabeza)`}
{`cabeza = nuevoNodo`}
- - ) -} + + ); +}; export const PopCode = ({ codeRef }) => { return ( @@ -333,8 +354,8 @@ export const PopCode = ({ codeRef }) => {
{`valorEliminado = cabeza.getValue()`}
{`cabeza = cabeza.getNext()`}
- ) -} + ); +}; export const SumergirCode = ({ codeRef }) => { return ( @@ -345,8 +366,8 @@ export const SumergirCode = ({ codeRef }) => {
{`while (nodoActual.getNext() !== null) nodoActual = nodoActual.getNext()`}
{`nodoTope.setNext(null); nodoActual.setNext(nodoTope)`}
- ) -} + ); +}; export const InsertarCode = ({ codeRef }) => { return ( @@ -358,8 +379,8 @@ export const InsertarCode = ({ codeRef }) => {
{`ultimo.setNext(nuevoNodo)`}
{`indiceDesdeTop = tamanoPila - posicion - 1`}
- ) -} + ); +}; export const ExtraerCode = ({ codeRef }) => { return ( @@ -374,4 +395,4 @@ export const ExtraerCode = ({ codeRef }) => {
{`nodoActual.setNext(nodoActual.getNext().getNext())`}
); -}; \ No newline at end of file +}; 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..c64ef5f 100644 --- a/src/animations/stack/components/StackChart.jsx +++ b/src/animations/stack/components/StackChart.jsx @@ -16,7 +16,7 @@ export const StackChart = () => { const renderStack = (node, isHead, firstSet, secondSet) => { if (!node) { - return null; + return null; // Simplemente no mostrar nada cuando la pila está vacía } const getBackgroundColor = () => { diff --git a/src/animations/stack/components/StackControls.jsx b/src/animations/stack/components/StackControls.jsx index b90dd9d..4d7d561 100644 --- a/src/animations/stack/components/StackControls.jsx +++ b/src/animations/stack/components/StackControls.jsx @@ -1,22 +1,28 @@ import { useDispatch, useSelector } from 'react-redux'; -import { - decrementHistoryStack, - incrementHistoryStack, - actionButton, - restoreRepeatStack, - restoreTimeId, - setHead, - setPlayingStack, - setTimeIdStack, - updateVisualizationStack +import { + decrementHistoryStack, + incrementHistoryStack, + actionButton, + restoreRepeatStack, + restoreTimeId, + setHead, + setPlayingStack, + setTimeIdStack, + updateVisualizationStack, } from '../../../store'; import { Alert, Button, Snackbar, TextField } from '@mui/material'; -import { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState, useCallback, useRef } from 'react'; import { BasicButton } from '../../../components'; -import { svgPause, svgBack, svgPlay, svgRepeat, svgForward } from '../../../assets/svg/SvgConstans'; +import { + svgPause, + svgBack, + svgPlay, + svgRepeat, + svgForward, +} from '../../../assets/svg/SvgConstans'; export const StackControls = () => { - const stackState = useSelector((state) => state.stack); + const stackState = useSelector(state => state.stack); const dispatch = useDispatch(); // Estados locales @@ -27,22 +33,26 @@ export const StackControls = () => { const [open, setOpen] = useState(false); const [isAnimating, setIsAnimating] = useState(false); + // Ref para controlar si ya se ejecutó la animación automática + const animationExecutedRef = useRef(false); + const currentFunActionRef = useRef(null); + // Handlers para cambios en inputs - const handleValueChange = (event) => { + const handleValueChange = event => { const inputValue = event.target.value; if (inputValue === '' || (!isNaN(inputValue) && inputValue !== null)) { setValue(inputValue); } }; - const handleInsertPositionChange = (event) => { + const handleInsertPositionChange = event => { const inputValue = event.target.value; if (inputValue === '' || (!isNaN(inputValue) && inputValue >= 0)) { setInsertPosition(inputValue); } }; - const handleExtractPositionChange = (event) => { + const handleExtractPositionChange = event => { const inputValue = event.target.value; if (inputValue === '' || (!isNaN(inputValue) && inputValue >= 0)) { setExtractPosition(inputValue); @@ -50,95 +60,153 @@ export const StackControls = () => { }; // Función para mostrar mensajes de error - const showError = useCallback((message) => { + const showError = useCallback(message => { setToast(message); setOpen(true); }, []); - // 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; - } + // 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; + } - try { - // Cancelar cualquier animación en curso + // Limpiar timeouts previos 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()); - + dispatch(setPlayingStack(true)); setIsAnimating(true); - - } catch (error) { - console.error('Error executing animation:', error); - showError("Error al ejecutar la animación"); - setIsAnimating(false); - } - }, [dispatch, stackState.head, isAnimating, showError]); - // Función para ejecutar la animación paso a paso - const runAnimation = useCallback((stepHistory) => { - if (!stepHistory || stepHistory.length === 0) { - setIsAnimating(false); - return; - } + const timeIds = []; - dispatch(setPlayingStack(true)); - const timeIds = []; - - // Ejecutar cada paso con retraso - stepHistory.forEach((step, index) => { - const timeoutId = setTimeout(() => { - dispatch(incrementHistoryStack()); - dispatch(updateVisualizationStack(step)); - }, index * (250 / (stackState.timeStep || 1))); - - timeIds.push(timeoutId); - }); - - // Finalizar animación - const finalTimeoutId = setTimeout(() => { - const finalState = stepHistory[stepHistory.length - 1]; - if (finalState) { - dispatch(updateVisualizationStack(finalState)); + // Si empezamos desde el principio, mostrar el primer paso inmediatamente + if (startFromBeginning) { + dispatch(updateVisualizationStack(stepHistory[0])); + dispatch(incrementHistoryStack()); // Ahora incrementamos DESPUÉS de mostrar } - - dispatch(setPlayingStack(false)); - dispatch(restoreTimeId()); - setIsAnimating(false); - }, stepHistory.length * (250 / (stackState.timeStep || 1))); - timeIds.push(finalTimeoutId); - dispatch(setTimeIdStack(timeIds)); - }, [dispatch, stackState.timeStep]); + // 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)) + ); + + 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 = () => { const numValue = value === '' ? null : parseInt(value); - + if (numValue === null) { - showError("Ingresa un valor válido"); + showError('Ingresa un valor válido'); return; } if (stackState.elementos && stackState.elementos.length >= 7) { - showError("La pila está llena (máximo 7 elementos)"); + 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"); + showError('El elemento ya existe en la pila'); return; } @@ -148,7 +216,7 @@ export const StackControls = () => { const handlePopButton = () => { if (!stackState.head) { - showError("La pila está vacía"); + showError('La pila está vacía'); return; } @@ -158,12 +226,12 @@ export const StackControls = () => { const handleSumergirButton = () => { if (!stackState.head) { - showError("La pila está vacía"); + showError('La pila está vacía'); return; } if (!stackState.head.getNext()) { - showError("La pila necesita al menos 2 elementos para sumergir"); + showError('La pila necesita al menos 2 elementos para sumergir'); return; } @@ -174,24 +242,24 @@ export const StackControls = () => { 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"); + showError('Ingresa un valor válido para insertar'); return; } if (position === null || position < 0) { - showError("Ingresa una posición válida (≥ 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"); + 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)"); + showError('La pila está llena (máximo 7 elementos)'); return; } @@ -208,18 +276,20 @@ export const StackControls = () => { const handleExtraerButton = () => { if (!stackState.head) { - showError("La pila está vacía"); + 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)"); + showError('Ingresa una posición válida (≥ 0)'); return; } - const maxPosition = stackState.elementos ? stackState.elementos.length - 1 : 0; + const maxPosition = stackState.elementos + ? stackState.elementos.length - 1 + : 0; if (position > maxPosition) { showError(`La posición máxima válida es ${maxPosition}`); return; @@ -229,8 +299,8 @@ export const StackControls = () => { setExtractPosition(''); }; - // Controles de reproducción manual - const onPlayPause = (e) => { + // Controles de reproducción manual - ARREGLADO + const onPlayPause = e => { e.preventDefault(); if (stackState.playing) { pause(); @@ -242,12 +312,19 @@ export const StackControls = () => { const play = () => { if (stackState.stepHistory && stackState.stepHistory.length > 0) { const currentHistory = stackState.history || -1; - const remainingSteps = stackState.stepHistory.slice(Math.max(0, currentHistory + 1)); - + const remainingSteps = stackState.stepHistory.slice( + Math.max(0, currentHistory + 1) + ); + if (remainingSteps.length > 0) { - runAnimation(remainingSteps); + // Marcar que esta es una reproducción manual + animationExecutedRef.current = true; + runAnimation(remainingSteps, false); // No empezar desde el principio } else { - runAnimation(stackState.stepHistory); + // Reiniciar desde el principio + dispatch(restoreRepeatStack()); + animationExecutedRef.current = true; + runAnimation(stackState.stepHistory, true); // Empezar desde el principio } } }; @@ -277,7 +354,10 @@ export const StackControls = () => { } const currentHistory = stackState.history || -1; - if (stackState.stepHistory && currentHistory < stackState.stepHistory.length - 1) { + if ( + stackState.stepHistory && + currentHistory < stackState.stepHistory.length - 1 + ) { const nextStep = stackState.stepHistory[currentHistory + 1]; dispatch(incrementHistoryStack()); dispatch(updateVisualizationStack(nextStep)); @@ -288,30 +368,27 @@ export const StackControls = () => { if (stackState.stepHistory && stackState.stepHistory.length > 0) { dispatch(restoreTimeId()); dispatch(restoreRepeatStack()); - setIsAnimating(true); - runAnimation(stackState.stepHistory); + // Marcar que esta es una reproducción manual + animationExecutedRef.current = true; + runAnimation(stackState.stepHistory, true); // Empezar desde el principio } }; - // Efecto para manejar cambios en stepHistory + // Limpiar timeouts al desmontar el componente useEffect(() => { - if (stackState.stepHistory && - stackState.stepHistory.length > 0 && - stackState.funAction && - !stackState.playing && - isAnimating) { - runAnimation(stackState.stepHistory); - } - }, [stackState.stepHistory, stackState.funAction, stackState.playing, isAnimating, runAnimation]); + return () => { + dispatch(restoreTimeId()); + }; + }, [dispatch]); // Determinar el icono de play/pause const playPauseIcon = stackState.playing ? svgPause : svgPlay; return ( -
+
{/* Controles de reproducción */}
- { {svgBack} - {playPauseIcon} - = stackState.stepHistory.length - 1} + disabled={ + stackState.playing || + !stackState.stepHistory || + (stackState.history || -1) >= stackState.stepHistory.length - 1 + } title="Avanzar" > {svgForward} - {svgRepeat} @@ -354,7 +438,7 @@ export const StackControls = () => { value={value} onChange={handleValueChange} disabled={isAnimating} - style={{ width: "120px" }} + style={{ width: '120px' }} size="small" />
- + {/* Controles para Insertar */}
{ value={insertPosition} onChange={handleInsertPositionChange} disabled={isAnimating} - style={{ width: "120px" }} + style={{ width: '120px' }} size="small" - inputProps={{ - min: 0, - max: stackState.elementos ? stackState.elementos.length : 0 + inputProps={{ + min: 0, + max: stackState.elementos ? stackState.elementos.length : 0, }} />
- + {/* Controles para Extraer */}
{ value={extractPosition} onChange={handleExtractPositionChange} disabled={isAnimating} - style={{ width: "120px" }} + style={{ width: '120px' }} size="small" - inputProps={{ - min: 0, - max: stackState.elementos && stackState.elementos.length > 0 - ? stackState.elementos.length - 1 - : 0 + inputProps={{ + min: 0, + max: + stackState.elementos && stackState.elementos.length > 0 + ? stackState.elementos.length - 1 + : 0, }} />
- + {/* Control Sumergir */}
{/* Notificaciones */} - setOpen(false)} - anchorOrigin={{ vertical: "bottom", horizontal: "right" }} + setOpen(false)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} > - setOpen(false)} - severity="error" + setOpen(false)} + severity="error" sx={{ width: '100%' }} > {toast} @@ -496,4 +581,4 @@ export const StackControls = () => {
); -}; \ No newline at end of file +}; From 2df79df1aa3f9f686ec712fa46a567728bfc6fd8 Mon Sep 17 00:00:00 2001 From: FrankGarcia Date: Tue, 24 Jun 2025 09:06:12 -0500 Subject: [PATCH 04/18] format --- src/animations/stack/algorithms/Nodo.js | 2 +- src/animations/stack/components/StackCode.jsx | 145 ++++++++++++------ src/store/stack/stackSlice.js | 56 +++---- 3 files changed, 131 insertions(+), 72 deletions(-) diff --git a/src/animations/stack/algorithms/Nodo.js b/src/animations/stack/algorithms/Nodo.js index b058c96..968ba0b 100644 --- a/src/animations/stack/algorithms/Nodo.js +++ b/src/animations/stack/algorithms/Nodo.js @@ -35,4 +35,4 @@ class Nodo { } } -export default Nodo; \ No newline at end of file +export default Nodo; diff --git a/src/animations/stack/components/StackCode.jsx b/src/animations/stack/components/StackCode.jsx index 64b3985..4687a58 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, SumergirCode, InsertarCode, ExtraerCode } from '../algorithms'; +import { + PopCode, + PushCode, + SumergirCode, + InsertarCode, + ExtraerCode, +} from '../algorithms'; import { SvgIconDropdown, svgAlgo } from '../../../assets/svg/SvgConstans'; import { useTranslation } from 'react-i18next'; @@ -16,8 +22,13 @@ export const StackCode = () => { // 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'); + if ( + prevCurrentLine !== null && + codeRef.current?.childNodes[prevCurrentLine] + ) { + codeRef.current.childNodes[prevCurrentLine].classList.remove( + 'bg-secondary' + ); setPrevCurrentLine(null); } return; @@ -30,7 +41,10 @@ export const StackCode = () => { // Obtener elementos del DOM const currentElement = codeRef.current?.childNodes[currentLine]; - const prevElement = prevCurrentLine !== null ? codeRef.current?.childNodes[prevCurrentLine] : null; + const prevElement = + prevCurrentLine !== null + ? codeRef.current?.childNodes[prevCurrentLine] + : null; // Remover clase de la línea anterior si existe if (prevElement && prevCurrentLine !== currentLine) { @@ -47,24 +61,28 @@ export const StackCode = () => { }, [history, stepHistory, funAction, prevCurrentLine]); // Función para renderizar el código específico de cada operación - const renderCode = (funAction) => { + const renderCode = funAction => { switch (funAction) { - case 'push': + case 'push': return ; - case 'pop': + case 'pop': return ; - case 'sumergir': + case 'sumergir': return ; - case 'insertar': + case 'insertar': return ; - case 'extraer': + case 'extraer': return ; - default: + default: return (
-

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

+

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

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

); @@ -89,52 +107,90 @@ export const StackCode = () => { }, [prevCurrentLine]); return ( -
+
{/* Panel de información */} {infoVisible && ( -
  • - {t("definition") || "Definición"}: - {t("stackDefinition") || "Una pila es una estructura de datos LIFO (Last In, First Out)"} + + {t('definition') || 'Definición'}: + + + {t('stackDefinition') || + 'Una pila es una estructura de datos LIFO (Last In, First Out)'} +
  • - {t("principalOperations") || "Operaciones principales"}: + + {t('principalOperations') || 'Operaciones principales'}: +
    • - Push: - {t("pushExplanation") || "Añade un elemento al tope de la pila"} + + Push: + + + {t('pushExplanation') || + 'Añade un elemento al tope de la pila'} +
    • - Pop: - {t("popExplanation") || "Elimina el elemento del 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 + + Sumergir: + + + Mueve el elemento del tope de la pila al final +
    • - Insertar: - Agrega un elemento en una posición específica de la pila + + 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 + + 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('utility') || 'Utilidad'}: + + + {t('stackUsageExplanation') || + 'Útil para operaciones de deshacer, navegación, etc.'} +
  • - {t("implementations") || "Implementaciones"}: - {t("stackImplementations") || "Arrays, listas enlazadas, etc."} + + {t('implementations') || 'Implementaciones'}: + + + {t('stackImplementations') || + 'Arrays, listas enlazadas, etc.'} +
@@ -145,15 +201,17 @@ export const StackCode = () => {
{svgAlgo} - {t("algorithm") || "Algoritmo"} + {t('algorithm') || 'Algoritmo'}
@@ -161,11 +219,12 @@ export const StackCode = () => {
{/* Contenido del código */} -
-
- {renderCode(funAction)} -
+
+
{renderCode(funAction)}
); -}; \ No newline at end of file +}; diff --git a/src/store/stack/stackSlice.js b/src/store/stack/stackSlice.js index d5dfb32..532cf9c 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, Sumergir, Insertar, Extraer } from "../../animations"; +import { createSlice } from '@reduxjs/toolkit'; +import { Pop, Push, Sumergir, Insertar, Extraer } from '../../animations'; const initialState = { head: null, @@ -20,7 +20,7 @@ const initialState = { }; export const stackSlice = createSlice({ - name: "stack", + name: 'stack', initialState, reducers: { setHead: (state, action) => { @@ -44,12 +44,12 @@ export const stackSlice = createSlice({ state.timeId = []; state.srcHead = action.payload; - + // Actualizar elementos basado en la nueva cabeza updateElementsFromHead(state); }, - restoreTimeId: (state) => { - state.timeId.forEach((timeoutId) => clearTimeout(timeoutId)); + restoreTimeId: state => { + state.timeId.forEach(timeoutId => clearTimeout(timeoutId)); state.timeId = []; }, updateVisualizationStack: (state, action) => { @@ -70,14 +70,14 @@ export const stackSlice = createSlice({ state.isHead = currVisualization.isHead; state.elementos = elementos; }, - restoreRepeatStack: (state) => { + restoreRepeatStack: state => { const head = state.srcHead; state.head = head; state.history = -1; state.firstSet = []; state.secondSet = []; state.isHead = -1; - + // Actualizar elementos desde la cabeza restaurada updateElementsFromHead(state); }, @@ -86,11 +86,11 @@ export const stackSlice = createSlice({ state.timeId.push(...action.payload); }, - incrementHistoryStack: (state) => { + incrementHistoryStack: state => { state.history = state.history + 1; }, - decrementHistoryStack: (state) => { + decrementHistoryStack: state => { state.history = state.history - 1; }, @@ -101,7 +101,7 @@ export const stackSlice = createSlice({ }); // Función auxiliar para actualizar elementos desde la cabeza -const updateElementsFromHead = (state) => { +const updateElementsFromHead = state => { let elementos = []; let current = state.head; while (current !== null) { @@ -115,12 +115,12 @@ const createRecordStack = (state, action) => { const head = state.head; const isHead = state.isHead; const value = action.payload.value; - const position = action.payload.position; + const position = action.payload.position; const funAction = getAction(action.payload.action); if (funAction) { 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) @@ -132,12 +132,12 @@ const createRecordStack = (state, action) => { } 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 @@ -148,13 +148,13 @@ const createRecordStack = (state, action) => { } }; -export const getAction = (action) => { +export const getAction = action => { const actions = { push: Push, pop: Pop, sumergir: Sumergir, insertar: Insertar, - extraer: Extraer + extraer: Extraer, }; return actions[action] || null; }; @@ -170,11 +170,11 @@ export const verificar = (head, elemento) => { return false; }; -const { - restoreStack, - restoreTimeId, - updateVisualizationStack, - restoreRepeatStack +const { + restoreStack, + restoreTimeId, + updateVisualizationStack, + restoreRepeatStack, } = stackSlice.actions; export const { @@ -186,9 +186,9 @@ export const { setPlayingStack, } = stackSlice.actions; -export { - restoreStack, - restoreTimeId, - updateVisualizationStack, - restoreRepeatStack -}; \ No newline at end of file +export { + restoreStack, + restoreTimeId, + updateVisualizationStack, + restoreRepeatStack, +}; From fb7abb9b8782b36dfa54fb3bafa52fa95e3114b9 Mon Sep 17 00:00:00 2001 From: jdbcamilo <149972010+jdbcamilo@users.noreply.github.com> Date: Fri, 29 Aug 2025 09:01:41 -0500 Subject: [PATCH 05/18] creating simple lists beta --- README.md | 2 + src/animations/index.js | 1 + src/animations/simpleList/algorithms/Nodo.js | 32 + .../algorithms/SimpleListAlgorithm.jsx | 1315 +++++++++++++++++ .../simpleList/algorithms/helpers.jsx | 36 + src/animations/simpleList/algorithms/index.js | 3 + .../simpleList/components/SimpleListChart.jsx | 186 +++ .../simpleList/components/SimpleListCode.jsx | 149 ++ .../components/SimpleListControls.jsx | 655 ++++++++ src/animations/simpleList/components/index.js | 3 + src/animations/simpleList/index.js | 2 + src/animations/views/AnimationView.jsx | 45 +- src/assets/svg/SvgConstans.jsx | 22 + src/components/Footer.jsx | 2 +- src/components/HomeCartas.js | 14 +- src/i18n/en.json | 3 +- src/i18n/es.json | 5 +- src/store/index.js | 1 + src/store/simpleList/index.js | 1 + src/store/simpleList/newFile.js | 1 + src/store/simpleList/simpleListSlice.js | 149 ++ src/store/store.js | 2 + 22 files changed, 2607 insertions(+), 22 deletions(-) create mode 100644 src/animations/simpleList/algorithms/Nodo.js create mode 100644 src/animations/simpleList/algorithms/SimpleListAlgorithm.jsx create mode 100644 src/animations/simpleList/algorithms/helpers.jsx create mode 100644 src/animations/simpleList/algorithms/index.js create mode 100644 src/animations/simpleList/components/SimpleListChart.jsx create mode 100644 src/animations/simpleList/components/SimpleListCode.jsx create mode 100644 src/animations/simpleList/components/SimpleListControls.jsx create mode 100644 src/animations/simpleList/components/index.js create mode 100644 src/animations/simpleList/index.js create mode 100644 src/store/simpleList/index.js create mode 100644 src/store/simpleList/newFile.js create mode 100644 src/store/simpleList/simpleListSlice.js 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/src/animations/index.js b/src/animations/index.js index 6aeab94..2f940f4 100644 --- a/src/animations/index.js +++ b/src/animations/index.js @@ -1,3 +1,4 @@ export * from './views'; export * from './sorting'; export * from './stack'; +export * from './simpleList'; 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..871bd34 --- /dev/null +++ b/src/animations/simpleList/algorithms/SimpleListAlgorithm.jsx @@ -0,0 +1,1315 @@ +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 = [ + 'nuevoNodo = new Nodo(valor)', + 'nuevoNodo.setNext(cabeza)', + 'cabeza = nuevoNodo', + ]; + + return ( +
+ + {lines.map((line, index) => ( +
+            {line}
+          
+ ))} +
+
+ ); +}; + +export const InsertarAlFinalCode = ({ codeRef, highlightedLines = [] }) => { + const lines = [ + 'nuevoNodo = new Nodo(valor)', + 'if (cabeza === null) cabeza = nuevoNodo', + 'else {', + ' 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)', + 'nuevoNodo = new Nodo(valor)', + 'if (cabeza === null) return error', + 'actual = cabeza', + 'for (i = 0; i < posicion - 1; i++)', + ' if (actual === null) return error', + 'nuevoNodo.setNext(actual.getNext())', + 'actual.setNext(nuevoNodo)', + ]; + + return ( +
+ + {lines.map((line, index) => ( +
+            {line}
+          
+ ))} +
+
+ ); +}; + +export const EliminarDelInicioCode = ({ codeRef, highlightedLines = [] }) => { + const lines = [ + 'if (cabeza === null) return error', + 'valorEliminado = cabeza.getValue()', + 'cabeza = cabeza.getNext()', + ]; + + return ( +
+ + {lines.map((line, index) => ( +
+            {line}
+          
+ ))} +
+
+ ); +}; + +export const EliminarDelFinalCode = ({ codeRef, highlightedLines = [] }) => { + const lines = [ + 'if (cabeza === null) return error', + 'if (cabeza.getNext() === null) {', + ' valorEliminado = cabeza.getValue()', + ' cabeza = null', + '} else {', + ' while (actual.getNext().getNext() !== null)', + ' actual = actual.getNext()', + ' valorEliminado = actual.getNext().getValue()', + ' actual.setNext(null)', + ]; + + return ( +
+ + {lines.map((line, index) => ( +
+            {line}
+          
+ ))} +
+
+ ); +}; + +export const EliminarEnPosicionCode = ({ codeRef, highlightedLines = [] }) => { + const lines = [ + 'if (posicion === 0) return eliminarDelInicio()', + 'actual = cabeza', + 'for (i = 0; i < posicion - 1; i++)', + ' actual = actual.getNext()', + 'if (actual === null || actual.getNext() === null) return error', + 'valorEliminado = actual.getNext().getValue()', + 'actual.setNext(actual.getNext().getNext())', + ]; + + return ( +
+ + {lines.map((line, index) => ( +
+            {line}
+          
+ ))} +
+
+ ); +}; + +export const BuscarCode = ({ codeRef, highlightedLines = [] }) => { + const lines = [ + 'if (cabeza === null) return not found', + 'actual = cabeza', + 'posicion = 0', + 'while (actual !== null) {', + ' if (actual.getValue() === valor)', + ' return found at posicion', + ' actual = actual.getNext()', + ' posicion++', + '}', + 'return not found', + ]; + + 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..1e3daf8 --- /dev/null +++ b/src/animations/simpleList/components/SimpleListChart.jsx @@ -0,0 +1,186 @@ +import { useSelector } from 'react-redux'; +import { useState, useEffect } from 'react'; + +export const SimpleListChart = () => { + const { + head, + isHead, + firstSet, + secondSet, + thirdSet, + highlightedNode, + pointer, + } = useSelector(state => state.simpleList); + + if (!head) { + return ( +
+
+ 📋 Lista vacía +
+
+ ); + } + + return ( +
+
+ {renderList( + head, + isHead, + firstSet, + secondSet, + thirdSet, + highlightedNode, + pointer + )} +
+
+ ); +}; + +const renderList = ( + node, + isHead, + firstSet, + secondSet, + thirdSet, + highlightedNode, + pointer, + index = 0 +) => { + if (!node) { + return null; + } + + const getBackgroundColor = value => { + 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'; + }; + + const isHighlighted = value => highlightedNode === value; + const isPointed = value => pointer === value; + const nodeIsHead = node.value === isHead; + + return ( +
+ {/* Indicador Head */} + {nodeIsHead && ( +
+
+ HEAD +
+
⬇️
+
+ )} + + {/* Nodo */} +
+ + + {/* Flecha conectora */} + {node.getNext() && } +
+ + {/* Recursión para siguiente nodo */} + {renderList( + node.next, + isHead, + firstSet, + secondSet, + thirdSet, + highlightedNode, + pointer, + index + 1 + )} +
+ ); +}; + +const NodeComponent = ({ + value, + backgroundColor, + isHighlighted, + isPointed, +}) => { + const [isAnimating, setIsAnimating] = useState(false); + + useEffect(() => { + if (isHighlighted) { + setIsAnimating(true); + const timer = setTimeout(() => setIsAnimating(false), 1000); + return () => clearTimeout(timer); + } + }, [isHighlighted]); + + return ( +
+ {/* Puntero */} + {isPointed && ( +
+
+ 🎯 Puntero +
+
+
+ )} + + {/* Nodo principal */} +
+ {/* Efecto de brillo */} +
+ + {/* Valor del nodo */} +
+ {value} +
+ + {/* Efecto de resaltado */} + {isHighlighted && ( +
+ )} +
+
+ ); +}; + +const ArrowComponent = () => { + 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..b395fa4 --- /dev/null +++ b/src/animations/simpleList/components/SimpleListCode.jsx @@ -0,0 +1,149 @@ +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 ( +
+
+ {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 +
    • +
    +
  • +
+
+
+ )} + +
+
+ {svgAlgo} + {t('algorithm') || 'Algoritmo'} +
+ +
+
+ +
+
{renderCode(funAction)}
+
+
+ ); +}; diff --git a/src/animations/simpleList/components/SimpleListControls.jsx b/src/animations/simpleList/components/SimpleListControls.jsx new file mode 100644 index 0000000..4ad1369 --- /dev/null +++ b/src/animations/simpleList/components/SimpleListControls.jsx @@ -0,0 +1,655 @@ +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'; + +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]); + + // 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; + } + + dispatch(setHeadSimpleList(simpleListState.head)); + dispatch(actionButtonSimpleList(actionPayload)); + dispatch(restoreRepeatSimpleList()); + + animationExecutedRef.current = false; + currentFunActionRef.current = action; + } 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(() => { + if ( + simpleListState.stepHistory && + simpleListState.stepHistory.length > 0 && + simpleListState.funAction && + simpleListState.funAction === currentFunActionRef.current && + !simpleListState.playing && + !animationExecutedRef.current + ) { + animationExecutedRef.current = true; + runAnimation(simpleListState.stepHistory, true); + } + }, [simpleListState.stepHistory, simpleListState.funAction, 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; + } + + executeAnimation('insertarAlInicio', numValue); + setValue(''); + }; + + const handleInsertarAlFinal = () => { + const numValue = value === '' ? null : parseInt(value); + + if (numValue === null) { + showError('Ingresa un valor válido'); + return; + } + + executeAnimation('insertarAlFinal', numValue); + 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 (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); + setValue(''); + setPosition(''); + }; + + const handleEliminarDelInicio = () => { + if (!simpleListState.head) { + showError('La lista está vacía'); + return; + } + + executeAnimation('eliminarDelInicio', null); + }; + + const handleEliminarDelFinal = () => { + if (!simpleListState.head) { + showError('La lista está vacía'); + return; + } + + executeAnimation('eliminarDelFinal', null); + }; + + 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); + 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); + 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/views/AnimationView.jsx b/src/animations/views/AnimationView.jsx index 1853129..f2c4625 100644 --- a/src/animations/views/AnimationView.jsx +++ b/src/animations/views/AnimationView.jsx @@ -22,6 +22,11 @@ import { } from '@mui/material'; import { BasicButton } from '../../components'; import { StackChart, StackCode, StackControls } from '../stack'; +import { + SimpleListChart, + SimpleListControls, + SimpleListCode, +} from '../simpleList/components'; // Importa los componentes import { useTranslation } from 'react-i18next'; export const AnimationView = () => { @@ -29,16 +34,17 @@ export const AnimationView = () => { const { animacion } = useParams(); const dispatch = useDispatch(); const navigate = useNavigate(); + + // 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 +53,6 @@ export const AnimationView = () => { array: generatedArray, history: currentQuestion.stepToHistory, }; - dispatch(setGeneratedArray(payload)); setIsOpen(true); } else { @@ -55,43 +60,55 @@ export const AnimationView = () => { setIsOpen(false); } } + // Para listas simples no necesitamos inicializar arrays }, [animacion]); + // Determinar la clase para el contenedor principal const classNameChart = 'flex-grow mb-4 w-full items-center justify-center ' + - (animacion === 'stack' ? 'flex' : ''); + (['stack', 'simpleList'].includes(animacion) ? 'flex' : ''); + return (
- {animacion === 'stack' ? : ''} - {sort ? : ''} + {animacion === 'stack' && } + {animacion === 'simpleList' && } + {sort && }
- {animacion === 'stack' ? : ''} - {sort ? : ''} + {animacion === 'stack' && } + {animacion === 'simpleList' && } + {sort && }
- {animacion === 'stack' ? : ''} - {sort ? : ''} + {animacion === 'stack' && } + {animacion === 'simpleList' && } + {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..3ec1585 100644 --- a/src/assets/svg/SvgConstans.jsx +++ b/src/assets/svg/SvgConstans.jsx @@ -635,3 +635,25 @@ export const svgMenu = ( ); + +export const svgSimpleList = ( + + + {/* Nodos */} + + + + + {/* Flechas */} + + + + + + +); diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index 854c5da..687885d 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -1,7 +1,7 @@ export const Footer = () => { return (
-

© 2023 - All rights reserved

+

© 2025 - All rights reserved

); }; diff --git a/src/components/HomeCartas.js b/src/components/HomeCartas.js index 855a926..2e19f02 100644 --- a/src/components/HomeCartas.js +++ b/src/components/HomeCartas.js @@ -9,6 +9,7 @@ import { svgPilesHome, svgSelectionHome, svgHeapHome, + svgSimpleList, } from '../assets/svg/SvgConstans'; export const courses = [ @@ -32,7 +33,6 @@ export const courses = [ }, ]; -// Mantenemos la exportación original de topic para compatibilidad export const topic = [ { id: 1, @@ -83,18 +83,24 @@ export const topic = [ param: 'stack', imagen: svgPilesHome, }, + { + id: 8, + curso: 'Programacion2', + titulo: 'Listas Simples', + param: 'simpleList', + imagen: svgSimpleList, + }, ]; -// Nueva estructura por categorías export const topicCategories = [ { id: 1, nombre: 'Métodos de Ordenamiento', - temas: topic.filter(t => t.param !== 'stack'), // Todos excepto Pilas + temas: topic.filter(t => !['stack', 'simpleList'].includes(t.param)), }, { id: 2, nombre: 'Estructuras de Datos', - temas: topic.filter(t => t.param === 'stack'), // Solo Pilas + temas: topic.filter(t => ['stack', 'simpleList'].includes(t.param)), }, ]; diff --git a/src/i18n/en.json b/src/i18n/en.json index aadaacd..1903693 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -86,5 +86,6 @@ "selectCategoryToExplore": "Select a category to explore algorithms and data structures", "availableCategories": "Available Categories", "clickToExpandCategory": "Click on a category to see available topics", - "fundamentals": "Fundamentals" + "fundamentals": "Fundamentals", + "simpleLists": "Simple Lists" } diff --git a/src/i18n/es.json b/src/i18n/es.json index 0b022f1..708cc6b 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -73,7 +73,7 @@ "selectionSortAdvantages": "Simple y útil para listas pequeñas.", "selectionSortDisadvantages": "Ineficiente para listas grandes, no es estable, complejidad de tiempo cuadrática.", "selectionSortComplexity": "O(n^2), en todos los casos, ya que siempre realiza el mismo número de comparaciones y movimientos de elementos, independientemente del estado inicial de la lista.", - "shellSortLogic": "consiste en dividir el arreglo (o la lista de elementos) en intervalos (o bloques) de varios elementos para organizarlos después por medio del ordenamiento de inserción directa.", + "shellSortLogic": "consiste en dividir el arreglo (o la lista de elementos) en intervalos (or bloques) de varios elementos para organizarlos después por medio del ordenamiento de inserción directa.", "shellSortAdvantages": "Uno de los algoritmos mas rapidos, no requiere memoria adicional.", "shellSortDisadvantages": "Poco estable ya que pierde orden relativo, no utiliza pivote.", "shellSortComplexity": "La complejidad de Shell Sort depende del tipo de secuencia de incrementos que se utilice. La complejidad en el peor caso generalmente se considera entre O(n log^2 n) y O(n^2), según la secuencia de incrementos utilizada. La elección de la secuencia de incrementos puede afectar significativamente el rendimiento del algoritmo.", @@ -86,5 +86,6 @@ "selectCategoryToExplore": "Selecciona una categoría para explorar algoritmos y estructuras de datos", "availableCategories": "Categorías Disponibles", "clickToExpandCategory": "Haz clic en una categoría para ver los temas disponibles", - "fundamentals": "Fundamentos" + "fundamentals": "Fundamentos", + "simpleLists": "Listas Simples" } diff --git a/src/store/index.js b/src/store/index.js index 75291e0..29d009d 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -2,3 +2,4 @@ export * from './store'; export * from './quiz'; export * from './sorts'; export * from './stack'; +export * from './simpleList'; 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..6c323bd --- /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, + timeId: [], +}; + +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.timeId.forEach(timeoutId => clearTimeout(timeoutId)); + state.timeId = []; + }, + + 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.timeId = 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/store.js b/src/store/store.js index 84e278f..262bcf4 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -2,12 +2,14 @@ import { configureStore } from '@reduxjs/toolkit'; import { quizSlice } from './quiz'; import { sortsSlice } from './sorts'; import { stackSlice } from './stack'; +import simpleListSlice from './simpleList/simpleListSlice'; export const store = configureStore({ reducer: { quiz: quizSlice.reducer, sorts: sortsSlice.reducer, stack: stackSlice.reducer, + simpleList: simpleListSlice, }, middleware: getDefaultMiddleware => getDefaultMiddleware({ From a1a9128944e5c83b4afae2baf97ab15598611828 Mon Sep 17 00:00:00 2001 From: FrankGarcia Date: Fri, 31 Oct 2025 11:32:58 -0500 Subject: [PATCH 06/18] Quizes corregidos --- src/i18n/es.json | 2 +- src/quiz/components/WrongAnswerDialog.jsx | 39 +++++++++++++++++++++-- src/quiz/views/QuizView.jsx | 14 +++++++- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/i18n/es.json b/src/i18n/es.json index 708cc6b..aa1a8eb 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -16,7 +16,7 @@ "previous": "Volver", "finish": "Finalizar", "correctAnswer": "Respuesta correcta!!", - "wrongAnswer": "Respuesta incorrecta :C", + "wrongAnswer": "Respuesta incorrecta", "accept": "Acceptar", "uncompletedQuizText": "Debes responder todas las preguntas para finalizar el quiz", "correctAnswers": "Respuestas correctas", diff --git a/src/quiz/components/WrongAnswerDialog.jsx b/src/quiz/components/WrongAnswerDialog.jsx index 4d02ad4..52caaa7 100644 --- a/src/quiz/components/WrongAnswerDialog.jsx +++ b/src/quiz/components/WrongAnswerDialog.jsx @@ -1,10 +1,43 @@ 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')} +
+
{t('wrongAnswer')}
+ + {correctAnswer && correctOption && ( +
+
+ {t('correctOption')}:{' '} + {correctAnswer} +
+
+ {correctOption} +
+
+ )} + + {feedback && ( +
+
{t('explanation')}:
+
+ {feedback} +
+
+ )}
); }; + +WrongAnswerDialog.propTypes = { + correctAnswer: PropTypes.string, + correctOption: PropTypes.string, + feedback: PropTypes.string, +}; diff --git a/src/quiz/views/QuizView.jsx b/src/quiz/views/QuizView.jsx index 2bd8011..04007ad 100644 --- a/src/quiz/views/QuizView.jsx +++ b/src/quiz/views/QuizView.jsx @@ -83,6 +83,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; @@ -148,7 +153,14 @@ export const QuizView = () => { ))} {isQuestionCorrect && } - {isQuestionIncorrect && } + {isQuestionIncorrect && ( + + )} + {shouldShowFeedbackButton && ( { From 43450f8dd5b247cdd2176122f4b624edc02aa2ea Mon Sep 17 00:00:00 2001 From: jdbcamilo <149972010+jdbcamilo@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:53:31 -0500 Subject: [PATCH 07/18] =?UTF-8?q?QUIZZES=20MODIFICADOS:=20Quizzes=20de=20B?= =?UTF-8?q?=C3=BAsqueda=20Lineal=20y=20Binaria=20creados=20e=20integrados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Creados quizzes de Búsqueda Lineal (8 preguntas enfocadas en el algoritmo) - Creados quizzes de Búsqueda Binaria (8 preguntas enfocadas en el algoritmo) - Eliminado quiz genérico de Métodos de Búsqueda - Implementadas versiones en español (quizzes.js) e inglés (quizzesEn.js) - Actualizada la navegación de quizzes en getQuizByName.js - Actualizados los mappings de animación en getAnimationNameByQuizName.js ESTRUCTURAS DE DATOS IMPLEMENTADOS: - Página DataStructuresPage con 5 estructuras (Listas Simples, Listas Enlazadas, Árboles Binarios, Pilas, Colas) - Todos los quizzes con navegación correcta de ida y vuelta (quiz ↔ animación) - Traducción completa en es.json y en.json MÉTODOS DE BÚSQUEDA IMPLEMENTADOS: - Nueva página SearchMethodsPage que muestra Búsqueda Lineal y Binaria - Separación clara entre métodos de ordenamiento y métodos de búsqueda - Rutas organizadas: /quizzes/sortingMethods, /quizzes/searchMethods, /quizzes/dataStructures - Interfaz de usuario consistente y responsive en todos los módulos --- src/App.jsx | 17 +- .../binarySearch/algorithms/index.js | 156 +++ .../components/BinarySearchChart.jsx | 197 ++++ .../components/BinarySearchCode.jsx | 158 +++ .../components/BinarySearchControls.jsx | 456 ++++++++ .../binarySearch/components/index.js | 3 + src/animations/binarySearch/index.js | 4 + .../algorithms/DoubleListAlgorithm.jsx | 990 ++++++++++++++++++ .../doubleList/algorithms/NodoDoble.js | 45 + .../doubleList/algorithms/helpers.jsx | 36 + src/animations/doubleList/algorithms/index.js | 2 + .../doubleList/components/DoubleListChart.jsx | 228 ++++ .../doubleList/components/DoubleListCode.jsx | 173 +++ .../components/DoubleListControls.jsx | 693 ++++++++++++ src/animations/doubleList/components/index.js | 3 + src/animations/doubleList/index.js | 2 + src/animations/index.js | 3 + .../linearSearch/algorithms/index.js | 99 ++ .../components/LinearSearchChart.jsx | 155 +++ .../components/LinearSearchCode.jsx | 151 +++ .../components/LinearSearchControls.jsx | 451 ++++++++ .../linearSearch/components/index.js | 3 + src/animations/linearSearch/index.js | 4 + .../simpleList/components/SimpleListChart.jsx | 243 +++-- .../simpleList/components/SimpleListCode.jsx | 105 +- .../components/SimpleListControls.jsx | 98 +- .../sorting/components/SortCode.jsx | 47 +- .../stack/algorithms/StackAlgorithm.jsx | 52 +- .../stack/components/StackChart.jsx | 120 ++- src/animations/stack/components/StackCode.jsx | 186 ++-- .../stack/components/StackControls.jsx | 35 +- src/animations/views/AnimationView.jsx | 96 +- src/assets/svg/SvgConstans.jsx | 87 ++ src/components/HomeCartas.js | 46 +- src/components/HomeQuiz.jsx | 26 +- src/components/QuizCard.jsx | 10 +- src/components/QuizzesCards.js | 18 +- src/i18n/en.json | 1 + src/i18n/es.json | 1 + src/index.css | 45 + src/quiz/data/quizzes.js | 398 +++++-- src/quiz/data/quizzesEn.js | 395 +++++-- .../helpers/getAnimationNameByQuizName.js | 4 + src/quiz/helpers/getQuizByName.js | 16 +- src/quiz/pages/DataStructuresPage.jsx | 80 ++ src/quiz/pages/SearchMethodsPage.jsx | 51 + src/quiz/pages/index.js | 2 + src/quiz/views/QuizView.jsx | 5 +- src/store/binarySearch/binarySearchSlice.js | 120 +++ src/store/binarySearch/index.js | 15 + src/store/doubleList/doubleListSlice.js | 149 +++ src/store/doubleList/index.js | 1 + src/store/index.js | 3 + src/store/linearSearch/index.js | 15 + src/store/linearSearch/linearSearchSlice.js | 111 ++ src/store/simpleList/simpleListSlice.js | 8 +- src/store/store.js | 6 + src/theme/theme.js | 8 + 58 files changed, 6068 insertions(+), 564 deletions(-) create mode 100644 src/animations/binarySearch/algorithms/index.js create mode 100644 src/animations/binarySearch/components/BinarySearchChart.jsx create mode 100644 src/animations/binarySearch/components/BinarySearchCode.jsx create mode 100644 src/animations/binarySearch/components/BinarySearchControls.jsx create mode 100644 src/animations/binarySearch/components/index.js create mode 100644 src/animations/binarySearch/index.js create mode 100644 src/animations/doubleList/algorithms/DoubleListAlgorithm.jsx create mode 100644 src/animations/doubleList/algorithms/NodoDoble.js create mode 100644 src/animations/doubleList/algorithms/helpers.jsx create mode 100644 src/animations/doubleList/algorithms/index.js create mode 100644 src/animations/doubleList/components/DoubleListChart.jsx create mode 100644 src/animations/doubleList/components/DoubleListCode.jsx create mode 100644 src/animations/doubleList/components/DoubleListControls.jsx create mode 100644 src/animations/doubleList/components/index.js create mode 100644 src/animations/doubleList/index.js create mode 100644 src/animations/linearSearch/algorithms/index.js create mode 100644 src/animations/linearSearch/components/LinearSearchChart.jsx create mode 100644 src/animations/linearSearch/components/LinearSearchCode.jsx create mode 100644 src/animations/linearSearch/components/LinearSearchControls.jsx create mode 100644 src/animations/linearSearch/components/index.js create mode 100644 src/animations/linearSearch/index.js create mode 100644 src/quiz/pages/DataStructuresPage.jsx create mode 100644 src/quiz/pages/SearchMethodsPage.jsx create mode 100644 src/store/binarySearch/binarySearchSlice.js create mode 100644 src/store/binarySearch/index.js create mode 100644 src/store/doubleList/doubleListSlice.js create mode 100644 src/store/doubleList/index.js create mode 100644 src/store/linearSearch/index.js create mode 100644 src/store/linearSearch/linearSearchSlice.js diff --git a/src/App.jsx b/src/App.jsx index d38f380..84c8ae5 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -9,7 +9,14 @@ 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'; @@ -30,6 +37,14 @@ function App() { path="/quizzes/sortingMethods" element={} /> + } + /> + } + /> } /> } /> } /> diff --git a/src/animations/binarySearch/algorithms/index.js b/src/animations/binarySearch/algorithms/index.js new file mode 100644 index 0000000..ed2f4f9 --- /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 `function busquedaBinaria(arr, target) { + let izquierda = 0; + let derecha = arr.length - 1; + while (izquierda <= derecha) { + let medio = Math.floor((izq + der) / 2); + if (arr[medio] === target) { + return medio; // Encontrado + } else if (arr[medio] < target) { + izquierda = medio + 1; + } else { + derecha = medio - 1; + } + } + return -1; // No encontrado +}`; +}; + +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 && ( +
+
    +
  • + Definicion: + + Algoritmo eficiente que divide el espacio de busqueda a la mitad + +
  • +
  • + Requisito: + El array DEBE estar ordenado +
  • +
  • + + Complejidad: + +
      +
    • + Tiempo: O(log n) +
    • +
    • + Espacio: O(1) +
    • +
    +
  • +
  • + + Indicadores: + +
      +
    • + L - Limite izquierdo +
    • +
    • + M - Punto medio +
    • +
    • + R - Limite derecho +
    • +
    +
  • +
+
+ )} +
+ ); +}; + +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..129c752 --- /dev/null +++ b/src/animations/doubleList/algorithms/DoubleListAlgorithm.jsx @@ -0,0 +1,990 @@ +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 }) => ( +
+

{'// Insertar al inicio - Lista Doble'}

+

{'nuevoNodo = new NodoDoble(valor)'}

+

{'nuevoNodo.next = cabeza'}

+

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

+

{'cabeza = nuevoNodo'}

+

{'return cabeza'}

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

{'// Insertar al final - Lista Doble'}

+

{'nuevoNodo = new NodoDoble(valor)'}

+

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

+

{'actual = cabeza'}

+

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

+

{' actual = actual.next'}

+

{'actual.next = nuevoNodo'}

+

{'nuevoNodo.prev = actual'}

+

{'return cabeza'}

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

{'// Insertar en posición - Lista Doble'}

+

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

+

{'nuevoNodo = new NodoDoble(valor)'}

+

{'actual = cabeza; cont = 0'}

+

{'while (cont < pos - 1 && actual != null)'}

+

{' actual = actual.next; cont++'}

+

{'nuevoNodo.next = actual.next'}

+

{'nuevoNodo.prev = actual'}

+

{'if (actual.next != null) actual.next.prev = nuevoNodo'}

+

{'actual.next = nuevoNodo'}

+

{'return cabeza'}

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

{'// Eliminar del inicio - Lista Doble'}

+

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

+

{'cabeza = cabeza.next'}

+

{'if (cabeza != null) cabeza.prev = null'}

+

{'return cabeza'}

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

{'// Eliminar del final - Lista Doble'}

+

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

+

{'if (cabeza.next == null) return null'}

+

{'actual = cabeza'}

+

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

+

{' actual = actual.next'}

+

{'actual.prev.next = null'}

+

{'return cabeza'}

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

{'// Eliminar en posición - Lista Doble'}

+

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

+

{'actual = cabeza; cont = 0'}

+

{'while (cont < pos && actual != null)'}

+

{' actual = actual.next; cont++'}

+

{'if (actual.prev != null) actual.prev.next = actual.next'}

+

{'if (actual.next != null) actual.next.prev = actual.prev'}

+

{'return cabeza'}

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

{'// Buscar en lista doble'}

+

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

+

{'actual = cabeza; pos = 0'}

+

{'while (actual != null)'}

+

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

+

{' actual = actual.next; 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..c5c6f79 --- /dev/null +++ b/src/animations/doubleList/components/DoubleListChart.jsx @@ -0,0 +1,228 @@ +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]); + + 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..eeefe78 --- /dev/null +++ b/src/animations/doubleList/components/DoubleListControls.jsx @@ -0,0 +1,693 @@ +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'; + +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); + 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); + 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); + setValue(''); + setPosition(''); + }; + + const handleEliminarDelInicio = () => { + if (!doubleListState.head) { + showError('La lista está vacía'); + return; + } + + executeAnimation('eliminarDelInicio', null); + }; + + const handleEliminarDelFinal = () => { + if (!doubleListState.head) { + showError('La lista está vacía'); + return; + } + + executeAnimation('eliminarDelFinal', null); + }; + + 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); + 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); + 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 2f940f4..7873357 100644 --- a/src/animations/index.js +++ b/src/animations/index.js @@ -2,3 +2,6 @@ export * from './views'; export * from './sorting'; export * from './stack'; 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..3f1cfbb --- /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 `function busquedaLineal(arreglo, objetivo) { + for (let i = 0; i < arreglo.length; i++) { + if (arreglo[i] === objetivo) { + return i; // Encontrado + } + } + return -1; // No encontrado +}`; +}; 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..4da1135 --- /dev/null +++ b/src/animations/linearSearch/components/LinearSearchControls.jsx @@ -0,0 +1,451 @@ +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'; + +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(); + 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(); + 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); + 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/simpleList/components/SimpleListChart.jsx b/src/animations/simpleList/components/SimpleListChart.jsx index 1e3daf8..a2adb77 100644 --- a/src/animations/simpleList/components/SimpleListChart.jsx +++ b/src/animations/simpleList/components/SimpleListChart.jsx @@ -1,5 +1,18 @@ import { useSelector } from 'react-redux'; -import { useState, useEffect } from 'react'; +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 { @@ -12,48 +25,108 @@ export const SimpleListChart = () => { 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 +
+
+
+ 📋 Lista vacía +
); } return ( -
-
- {renderList( - head, - isHead, - firstSet, - secondSet, - thirdSet, - highlightedNode, - pointer - )} +
+
+
+ {nodes.map((node, index) => ( +
+ {/* Nodo */} + + + {/* Flecha conectora */} + {node.getNext() && ( + + )} +
+ ))} +
); }; -const renderList = ( - node, - isHead, +const NodeComponent = ({ + value, firstSet, secondSet, thirdSet, - highlightedNode, - pointer, - index = 0 -) => { - if (!node) { - return null; - } + isHighlighted, + isPointed, + nodeStyles, + isHead = false, +}) => { + const [isAnimating, setIsAnimating] = useState(false); - const getBackgroundColor = value => { + const getBackgroundColor = () => { if (firstSet.includes(value)) return 'bg-gradient-to-br from-green-400 to-green-600'; if (secondSet.includes(value)) @@ -63,64 +136,6 @@ const renderList = ( return 'bg-gradient-to-br from-blue-400 to-blue-600'; }; - const isHighlighted = value => highlightedNode === value; - const isPointed = value => pointer === value; - const nodeIsHead = node.value === isHead; - - return ( -
- {/* Indicador Head */} - {nodeIsHead && ( -
-
- HEAD -
-
⬇️
-
- )} - - {/* Nodo */} -
- - - {/* Flecha conectora */} - {node.getNext() && } -
- - {/* Recursión para siguiente nodo */} - {renderList( - node.next, - isHead, - firstSet, - secondSet, - thirdSet, - highlightedNode, - pointer, - index + 1 - )} -
- ); -}; - -const NodeComponent = ({ - value, - backgroundColor, - isHighlighted, - isPointed, -}) => { - const [isAnimating, setIsAnimating] = useState(false); - useEffect(() => { if (isHighlighted) { setIsAnimating(true); @@ -130,12 +145,27 @@ const NodeComponent = ({ }, [isHighlighted]); return ( -
+
+ {/* Indicador HEAD - solo para el primer nodo */} + {isHead && ( +
+
+ HEAD +
+
+ )} + {/* Puntero */} {isPointed && ( -
-
- 🎯 Puntero +
+
+ 🎯 Puntero
@@ -144,40 +174,55 @@ const NodeComponent = ({ {/* Nodo principal */}
{/* Efecto de brillo */} -
+
{/* Valor del nodo */} -
+
{value}
{/* Efecto de resaltado */} {isHighlighted && ( -
+
)}
); }; -const ArrowComponent = () => { +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 index b395fa4..2ec2a91 100644 --- a/src/animations/simpleList/components/SimpleListCode.jsx +++ b/src/animations/simpleList/components/SimpleListCode.jsx @@ -87,63 +87,66 @@ export const SimpleListCode = () => { }; return ( -
-
- {infoVisible && ( -
-
-
    +
    + {/* 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 +
      • - - {t('definition') || 'Definición'}: - - - Estructura lineal donde cada nodo contiene datos y un - puntero al siguiente nodo - + Eliminación: Del inicio, + final o posición específica
      • - - 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 -
        • -
        + Búsqueda: Recorrido + secuencial
      -
    -
    - )} - -
    -
    - {svgAlgo} - {t('algorithm') || 'Algoritmo'} -
    - + +
-
- -
-
{renderCode(funAction)}
-
+ )}
); }; diff --git a/src/animations/simpleList/components/SimpleListControls.jsx b/src/animations/simpleList/components/SimpleListControls.jsx index 4ad1369..e413756 100644 --- a/src/animations/simpleList/components/SimpleListControls.jsx +++ b/src/animations/simpleList/components/SimpleListControls.jsx @@ -54,6 +54,20 @@ export const SimpleListControls = () => { 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; @@ -172,12 +186,13 @@ export const SimpleListControls = () => { actionPayload.position = position; } - dispatch(setHeadSimpleList(simpleListState.head)); - dispatch(actionButtonSimpleList(actionPayload)); - dispatch(restoreRepeatSimpleList()); - + // 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'); @@ -195,18 +210,26 @@ export const SimpleListControls = () => { // 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 + !animationExecutedRef.current && + simpleListState.history === -1 // Solo si no ha empezado la animación ) { animationExecutedRef.current = true; runAnimation(simpleListState.stepHistory, true); } - }, [simpleListState.stepHistory, simpleListState.funAction, runAnimation]); + }, [ + simpleListState.stepHistory, + simpleListState.funAction, + simpleListState.playing, + simpleListState.history, + runAnimation, + ]); // Limpiar refs cuando cambia la acción useEffect(() => { @@ -225,6 +248,11 @@ export const SimpleListControls = () => { return; } + if (elementExists(numValue)) { + showError('El elemento ya existe en la lista'); + return; + } + executeAnimation('insertarAlInicio', numValue); setValue(''); }; @@ -237,6 +265,11 @@ export const SimpleListControls = () => { return; } + if (elementExists(numValue)) { + showError('El elemento ya existe en la lista'); + return; + } + executeAnimation('insertarAlFinal', numValue); setValue(''); }; @@ -250,6 +283,11 @@ export const SimpleListControls = () => { 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; @@ -407,9 +445,9 @@ export const SimpleListControls = () => { const playPauseIcon = simpleListState.playing ? svgPause : svgPlay; return ( -
+
{/* Controles de reproducción */} -
+
{
{/* Controles Insertar Inicio/Final */} -
+
{ value={value} onChange={handleValueChange} disabled={isAnimating} - style={{ width: '120px' }} + sx={{ width: { xs: '80px', sm: '100px' } }} size="small" />
{/* Controles para Insertar en Posición */} -
+
{ value={position} onChange={handlePositionChange} disabled={isAnimating} - style={{ width: '120px' }} + sx={{ width: { xs: '80px', sm: '100px' } }} size="small" inputProps={{ min: 0, @@ -526,8 +566,8 @@ export const SimpleListControls = () => { '&:hover': { backgroundColor: 'secondary.dark', }, - width: '100%', - maxWidth: '170px', + minWidth: { xs: '100px', sm: '140px' }, + fontSize: { xs: '0.7rem', sm: '0.875rem' }, }} > Insertar @@ -535,7 +575,7 @@ export const SimpleListControls = () => {
{/* Controles para Eliminar en Posición */} -
+
{ value={deletePosition} onChange={handleDeletePositionChange} disabled={isAnimating} - style={{ width: '120px' }} + sx={{ width: { xs: '80px', sm: '100px' } }} size="small" inputProps={{ min: 0, @@ -560,8 +600,8 @@ export const SimpleListControls = () => { '&:hover': { backgroundColor: 'secondary.dark', }, - width: '100%', - maxWidth: '170px', + minWidth: { xs: '100px', sm: '140px' }, + fontSize: { xs: '0.7rem', sm: '0.875rem' }, }} > Eliminar @@ -569,7 +609,7 @@ export const SimpleListControls = () => {
{/* Control Eliminar Inicio/Final */} -
+
{/* Control Buscar */} -
+
{ value={searchValue} onChange={handleSearchValueChange} disabled={isAnimating} - style={{ width: '120px' }} + sx={{ width: { xs: '80px', sm: '100px' } }} size="small" /> +
+
+ + {/* 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'}: + +
      • - - {t('definition') || 'Definición'}: + Push: + + {t('pushExplanation') || + 'Añade un elemento al tope de la pila'} +
      • +
      • + Pop: - {t('stackDefinition') || - 'Una pila es una estructura de datos LIFO (Last In, First Out)'} + {t('popExplanation') || + 'Elimina el elemento del tope de la pila'}
      • - - {t('principalOperations') || 'Operaciones principales'}: + + Sumergir: + + + Mueve el elemento del tope de la pila al final -
          -
        • - - 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'}: + + Insertar: - {t('stackUsageExplanation') || - 'Útil para operaciones de deshacer, navegación, etc.'} + Agrega un elemento en una posición específica de la pila
      • - - {t('implementations') || 'Implementaciones'}: + + Extraer: - {t('stackImplementations') || - 'Arrays, listas enlazadas, etc.'} + Elimina un elemento en una posición específica de la pila
      -
    -
- )} - - {/* Header del panel de código */} -
-
- {svgAlgo} - {t('algorithm') || 'Algoritmo'} -
-
- -
+ +
  • + + {t('utility') || 'Utilidad'}: + + + {t('stackUsageExplanation') || + 'Útil para operaciones de deshacer, navegación, etc.'} + +
  • +
  • + + {t('implementations') || 'Implementaciones'}: + + + {t('stackImplementations') || 'Arrays, listas enlazadas, etc.'} + +
  • +
    -
    - - {/* Contenido del código */} -
    -
    {renderCode(funAction)}
    -
    + )}
    ); }; diff --git a/src/animations/stack/components/StackControls.jsx b/src/animations/stack/components/StackControls.jsx index 4d7d561..0bedfbc 100644 --- a/src/animations/stack/components/StackControls.jsx +++ b/src/animations/stack/components/StackControls.jsx @@ -385,9 +385,9 @@ export const StackControls = () => { const playPauseIcon = stackState.playing ? svgPause : svgPlay; return ( -
    +
    {/* Controles de reproducción */} -
    +
    {
    {/* Controles Push/Pop */} -
    +
    { value={value} onChange={handleValueChange} disabled={isAnimating} - style={{ width: '120px' }} + sx={{ width: { xs: '80px', sm: '100px' } }} size="small" />
    {/* Controles para Insertar */} -
    +
    { value={insertPosition} onChange={handleInsertPositionChange} disabled={isAnimating} - style={{ width: '120px' }} + sx={{ width: { xs: '80px', sm: '100px' } }} size="small" inputProps={{ min: 0, @@ -499,8 +501,8 @@ export const StackControls = () => { '&:hover': { backgroundColor: 'secondary.dark', }, - width: '100%', - maxWidth: '170px', + minWidth: { xs: '100px', sm: '140px' }, + fontSize: { xs: '0.75rem', sm: '0.875rem' }, }} > Insertar @@ -508,7 +510,7 @@ export const StackControls = () => {
    {/* Controles para Extraer */} -
    +
    { value={extractPosition} onChange={handleExtractPositionChange} disabled={isAnimating} - style={{ width: '120px' }} + sx={{ width: { xs: '80px', sm: '100px' } }} size="small" inputProps={{ min: 0, @@ -536,8 +538,8 @@ export const StackControls = () => { '&:hover': { backgroundColor: 'secondary.dark', }, - width: '100%', - maxWidth: '170px', + minWidth: { xs: '100px', sm: '140px' }, + fontSize: { xs: '0.75rem', sm: '0.875rem' }, }} > Extraer @@ -545,7 +547,7 @@ export const StackControls = () => {
    {/* Control Sumergir */} -
    +
    ); }; 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..17a21ba 100644 --- a/src/quiz/components/WrongAnswerDialog.jsx +++ b/src/quiz/components/WrongAnswerDialog.jsx @@ -1,10 +1,65 @@ import { useTranslation } from 'react-i18next'; +import PropTypes from 'prop-types'; -export const WrongAnswerDialog = () => { +const WrongAnswerDialogComponent = ({ + 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} +

    +
    + )}
    ); }; + +WrongAnswerDialogComponent.propTypes = { + correctAnswer: PropTypes.string, + correctOption: PropTypes.string, + feedback: PropTypes.string, +}; + +export const WrongAnswerDialog = WrongAnswerDialogComponent; 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 4ca4f84..fb92934 100644 --- a/src/quiz/views/QuizView.jsx +++ b/src/quiz/views/QuizView.jsx @@ -84,6 +84,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; @@ -128,63 +133,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]) => ( + + ))} + +
    +
    +
    From 84c77ad9cef21b06381498b0464e71f9c6cd06f3 Mon Sep 17 00:00:00 2001 From: jdbcamilo <149972010+jdbcamilo@users.noreply.github.com> Date: Sat, 6 Dec 2025 10:33:18 -0500 Subject: [PATCH 09/18] =?UTF-8?q?Minijuegos=20organizados=20por=20temas=20?= =?UTF-8?q?y=20categor=C3=ADas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HomeMiniJuegos: Ahora muestra minijuegos organizados por categorías (Métodos de Ordenamiento, Estructuras de Datos, Métodos de Búsqueda) - Integración con topicCategories del sistema existente - MiniGameCard: Agrega badge 'Próximamente' en los minijuegos - Mejor organización y jerarquía visual - Grid responsive según pantalla --- src/components/HomeMiniJuegos.jsx | 90 ++++++++++++++++--------------- src/components/MiniGameCard.jsx | 40 ++++++++++---- 2 files changed, 75 insertions(+), 55 deletions(-) diff --git a/src/components/HomeMiniJuegos.jsx b/src/components/HomeMiniJuegos.jsx index cee0865..abda2db 100644 --- a/src/components/HomeMiniJuegos.jsx +++ b/src/components/HomeMiniJuegos.jsx @@ -1,59 +1,61 @@ import '../styles/mediaquerys.css'; import MiniGameCard from './MiniGameCard'; - -import { - svgGameCards, - svgGameMarbles, - svgGameThief, - svgGameSaveHomeland, - svgGameRescueOperation, - svgGamePrivacy, - svgGameFruitSplash, - svgGameMilitary, -} from '../assets/svg/SvgConstans'; +import { topicCategories } from './HomeCartas'; import { useTranslation } from 'react-i18next'; export const HomeMiniJuegos = () => { const { t } = useTranslation(); return ( -
    -
    -

    - Aprende mientras
    - juegas en CodeScape3D -

    -

    - Selecciona tu minijuego favorito
    y presiona en "jugar" -

    +
    + {/* Header */} +
    +
    +

    + Minijuegos +

    +

    + Aprende mientras juegas. Explora los minijuegos de cada tema +

    +
    -
    - - - - - - - - + {/* Categories */} +
    + {topicCategories.map(categoria => ( +
    + {/* Category Title */} +
    +

    + {categoria.nombre} +

    +
    +
    + + {/* Topics Grid */} +
    + {categoria.temas.map(tema => ( + + ))} +
    +
    + ))}
    -
    -
    -

    {t('comingSoon')}

    + {/* Coming Soon Banner */} +
    +
    +

    + Más minijuegos próximamente +

    +

    {t('comingSoon')}

    +
    ); diff --git a/src/components/MiniGameCard.jsx b/src/components/MiniGameCard.jsx index 6b82e0d..4fe1537 100644 --- a/src/components/MiniGameCard.jsx +++ b/src/components/MiniGameCard.jsx @@ -1,21 +1,39 @@ import { Link } from 'react-router-dom'; import '../styles/mediaquerys.css'; -import { HomeMiniJuegos } from './HomeMiniJuegos'; -const MiniGameCard = ({ logo, titulo, ruta }) => { +const MiniGameCard = ({ logo, titulo, ruta, comingSoon = true }) => { return ( -
    - -
    {logo}
    +
    + {/* Logo Container */} +
    +
    + {logo} +
    +
    -
    -

    {titulo}

    + {/* Title Container */} +
    +

    + {titulo} +

    - -
    + + {/* Coming Soon Badge */} + {comingSoon && ( +
    + + Próximamente + +
    + )} +
    + ); }; From 32054638038793a9a4722b28da3a59752b718ff5 Mon Sep 17 00:00:00 2001 From: FrankGarcia Date: Sat, 6 Dec 2025 10:56:36 -0500 Subject: [PATCH 10/18] Quizes respuesta incorrecta arreglado --- src/i18n/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/es.json b/src/i18n/es.json index 5eadac6..fe27c4f 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -17,7 +17,7 @@ "previous": "Volver", "finish": "Finalizar", "correctAnswer": "Respuesta correcta!!", - "wrongAnswer": "Respuesta incorrecta :C", + "wrongAnswer": "Respuesta incorrecta ", "accept": "Acceptar", "uncompletedQuizText": "Debes responder todas las preguntas para finalizar el quiz", "correctAnswers": "Respuestas correctas", From 7662fd230624acb3a0b9a7aab7ed465979339ceb Mon Sep 17 00:00:00 2001 From: FrankGarcia Date: Thu, 5 Feb 2026 10:26:32 -0500 Subject: [PATCH 11/18] format --- .../doubleList/components/DoubleListChart.jsx | 3 +++ src/animations/index.js | 1 + .../simpleList/components/SimpleListChart.jsx | 3 +++ .../stack/components/StackChart.jsx | 4 +++- src/animations/views/AnimationView.jsx | 17 +++++++++----- src/assets/svg/SvgConstans.jsx | 4 ++-- src/components/BasicButton.jsx | 23 ++++++++++++++----- src/components/HomeCartas.js | 11 ++++++++- src/index.css | 14 +++++++++++ .../helpers/getAnimationNameByQuizName.js | 1 + src/quiz/views/QuizView.jsx | 2 +- src/store/index.js | 1 + src/store/store.js | 2 ++ 13 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/animations/doubleList/components/DoubleListChart.jsx b/src/animations/doubleList/components/DoubleListChart.jsx index c5c6f79..2c2350b 100644 --- a/src/animations/doubleList/components/DoubleListChart.jsx +++ b/src/animations/doubleList/components/DoubleListChart.jsx @@ -132,6 +132,8 @@ const NodeComponent = ({ } }, [isHighlighted]); + const isDangerNode = secondSet.includes(value); + return (
    {/* Indicador HEAD - solo para el primer nodo */} @@ -167,6 +169,7 @@ const NodeComponent = ({ ${getBackgroundColor()} ${isHighlighted ? 'ring-2 sm:ring-4 ring-purple-400 ring-opacity-75 animate-pulse scale-105' : ''} ${isAnimating ? 'animate-wiggle' : ''} + ${isDangerNode ? 'animate-pulse-glow-danger ring-2 ring-red-400' : ''} cursor-pointer `} style={{ diff --git a/src/animations/index.js b/src/animations/index.js index 7873357..53a9c6e 100644 --- a/src/animations/index.js +++ b/src/animations/index.js @@ -1,6 +1,7 @@ export * from './views'; export * from './sorting'; export * from './stack'; +export * from './queue'; export * from './simpleList'; export * from './doubleList'; export * from './linearSearch'; diff --git a/src/animations/simpleList/components/SimpleListChart.jsx b/src/animations/simpleList/components/SimpleListChart.jsx index a2adb77..685237f 100644 --- a/src/animations/simpleList/components/SimpleListChart.jsx +++ b/src/animations/simpleList/components/SimpleListChart.jsx @@ -144,6 +144,8 @@ const NodeComponent = ({ } }, [isHighlighted]); + const isDangerNode = secondSet.includes(value); + return (
    {/* Indicador HEAD - solo para el primer nodo */} @@ -179,6 +181,7 @@ const NodeComponent = ({ ${getBackgroundColor()} ${isHighlighted ? 'ring-2 sm:ring-4 ring-blue-400 ring-opacity-75 animate-pulse scale-105' : ''} ${isAnimating ? 'animate-wiggle' : ''} + ${isDangerNode ? 'animate-pulse-glow-danger ring-2 ring-red-400' : ''} cursor-pointer `} style={{ diff --git a/src/animations/stack/components/StackChart.jsx b/src/animations/stack/components/StackChart.jsx index 8a586cc..0754a2b 100644 --- a/src/animations/stack/components/StackChart.jsx +++ b/src/animations/stack/components/StackChart.jsx @@ -93,9 +93,11 @@ const StackNode = ({ node, isHead, firstSet, secondSet, height, fontSize }) => { return 'bg-secondary'; }; + const isDangerNode = secondSet.includes(node.value); + return (
    { 'flex-grow mb-4 w-full max-w-full overflow-hidden items-center justify-center ' + ([ 'stack', + 'queue', 'simpleList', 'doubleList', 'linearSearch', @@ -96,6 +98,8 @@ export const AnimationView = () => { switch (animacion) { case 'stack': return 'Pilas'; + case 'queue': + return 'Colas'; case 'simpleList': return 'Listas Simples'; case 'doubleList': @@ -113,9 +117,7 @@ export const AnimationView = () => { const getQuizNameFromAnimation = () => { const quizMapping = { stack: 'stacks', - simpleList: 'simpleList', - doubleList: 'doubleList', - linearSearch: 'linearSearch', + queue: 'queues', binarySearch: 'binarySearch', bubble: 'bubble', quick: 'quick', @@ -134,7 +136,8 @@ export const AnimationView = () => { />
    - {animacion === 'stack' && } + {animacion === 'stack' && }{' '} + {animacion === 'queue' && }{' '} {animacion === 'simpleList' && } {animacion === 'doubleList' && } {animacion === 'linearSearch' && } @@ -144,7 +147,8 @@ export const AnimationView = () => {
    - {animacion === 'stack' && } + {animacion === 'stack' && }{' '} + {animacion === 'queue' && }{' '} {animacion === 'simpleList' && } {animacion === 'doubleList' && } {animacion === 'linearSearch' && } @@ -153,7 +157,8 @@ export const AnimationView = () => {
    - {animacion === 'stack' && } + {animacion === 'stack' && }{' '} + {animacion === 'queue' && }{' '} {animacion === 'simpleList' && } {animacion === 'doubleList' && } {animacion === 'linearSearch' && } diff --git a/src/assets/svg/SvgConstans.jsx b/src/assets/svg/SvgConstans.jsx index 5b1d9a3..15ad438 100644 --- a/src/assets/svg/SvgConstans.jsx +++ b/src/assets/svg/SvgConstans.jsx @@ -217,8 +217,8 @@ export const svgPilesHome = ( export const svgQueue = ( 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 ( )}
    - >>>>>>> origin/feature/quiz-y-busqueda-implementados + {shouldShowFeedbackButton && ( +
    +
    + +
    +
    {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..8f5f3b1 --- /dev/null +++ b/src/animations/queue/components/QueueControls.jsx @@ -0,0 +1,541 @@ +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 { + 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); + setValue(''); + }; + + const handleDequeue = () => { + if (!queueState.head) { + showError('La cola está vacía'); + return; + } + + const frontValue = queueState.head.getValue(); + executeAnimation('dequeue', frontValue); + }; + + const handleEliminarFinal = () => { + if (!queueState.head) { + showError('La cola está vacía'); + return; + } + + executeAnimation('eliminarFinal', null); + }; + + 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); + 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); + 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/assets/svg/SvgConstans.jsx b/src/assets/svg/SvgConstans.jsx index 15ad438..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 = ( @@ -638,8 +652,8 @@ export const svgMenu = ( export const svgSimpleList = ( diff --git a/src/components/CardTabla.jsx b/src/components/CardTabla.jsx index cfa5e11..1e998fd 100644 --- a/src/components/CardTabla.jsx +++ b/src/components/CardTabla.jsx @@ -6,9 +6,11 @@ const CardTabla = ({ tema, ruta = '' }) => { return ( -
    -
    {imagen}
    -

    {titulo}

    +
    +
    + {imagen} +
    +

    {titulo}

    ); diff --git a/src/components/QuizCard.jsx b/src/components/QuizCard.jsx index 370d6c3..cc07ddf 100644 --- a/src/components/QuizCard.jsx +++ b/src/components/QuizCard.jsx @@ -6,12 +6,12 @@ const QuizCard = ({ logo, titulo, ruta }) => { return (
    -
    +
    {logo}
    -
    +

    {titulo}

    diff --git a/src/index.css b/src/index.css index 6209c68..4f2f673 100644 --- a/src/index.css +++ b/src/index.css @@ -63,6 +63,17 @@ } } +@keyframes fadeOutScale { + 0% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(0.5); + } +} + @keyframes fadeIn { 0% { opacity: 0; @@ -98,6 +109,10 @@ animation: fadeInScale 0.5s ease-out forwards; } +.animate-fadeOutScale { + animation: fadeOutScale 0.5s ease-out forwards; +} + .animate-pulse-glow-danger { animation: pulse-glow-danger 2s ease-in-out infinite; } diff --git a/src/quiz/pages/DataStructuresPage.jsx b/src/quiz/pages/DataStructuresPage.jsx index 8c72122..3f3f263 100644 --- a/src/quiz/pages/DataStructuresPage.jsx +++ b/src/quiz/pages/DataStructuresPage.jsx @@ -5,7 +5,7 @@ import { useEffect } from 'react'; import { resetStateForQuiz } from '../../store'; import { svgSimpleList, - svgLinkedList, + svgLinkedListDataStructure, svgBinaryTrees, svgPiles, svgQueue, @@ -31,7 +31,7 @@ export const DataStructuresPage = () => { id: 2, titulo: t('linkedLists'), param: 'linkedList', - imagen: svgLinkedList, + imagen: svgLinkedListDataStructure, }, { id: 3, 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, +}; From a169fad7a99f39a3483bfc579262076cf0c5cfc9 Mon Sep 17 00:00:00 2001 From: FrankDev Date: Wed, 25 Feb 2026 15:42:51 -0500 Subject: [PATCH 13/18] Animaciones de pilas corregidas y boton volver quizzes --- .../stack/algorithms/StackAlgorithm.jsx | 198 ++++++++++-------- .../stack/components/StackChart.jsx | 65 ++++-- src/quiz/pages/DataStructuresPage.jsx | 17 +- src/quiz/pages/SearchMethodsPage.jsx | 17 +- src/quiz/pages/SortingMethodsPage.jsx | 17 +- src/store/stack/stackSlice.js | 4 + 6 files changed, 202 insertions(+), 116 deletions(-) diff --git a/src/animations/stack/algorithms/StackAlgorithm.jsx b/src/animations/stack/algorithms/StackAlgorithm.jsx index b7a9891..80787f0 100644 --- a/src/animations/stack/algorithms/StackAlgorithm.jsx +++ b/src/animations/stack/algorithms/StackAlgorithm.jsx @@ -23,7 +23,8 @@ const crearRegistro = ( cabeza, primerConjunto = [], segundoConjunto = [], - indiceActual = 0 + indiceActual = 0, + nodoAEliminar = null ) => { return { head: copiarPila(cabeza), @@ -31,6 +32,7 @@ const crearRegistro = ( secondSet: [...segundoConjunto], isHead: cabeza ? cabeza.getValue() : -1, currentIndex: indiceActual, + deletingNode: nodoAEliminar, }; }; @@ -84,11 +86,19 @@ export const Pop = cabeza => { // Paso 3: Guardar el valor a eliminar const valorEliminado = cabeza.getValue(); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 2)); + + // 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, [valorEliminado], [], 3)); + historialPasos.push(crearRegistro(cabeza, [], [], 3)); // Paso 5: Mostrar estado final historialPasos.push(crearRegistro(cabeza, [], [], 4)); @@ -112,18 +122,18 @@ export const Sumergir = cabeza => { // Paso 3: Guardar el nodo del tope y su valor const nodoTope = cabeza; const valorTope = nodoTope.getValue(); - historialPasos.push(crearRegistro(cabeza, [valorTope], [], 2)); + 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)); + 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) + crearRegistro(cabeza, [valorTope], [nodoActual.getValue()], 4, valorTope) ); } @@ -172,8 +182,8 @@ export const Insertar = (cabeza, valor, posicion) => { temp = temp.getNext(); } - // Paso 5: Si la posición es mayor o igual al tamaño, insertar en el tope (push) - if (posicion >= tamanoPila) { + // Paso 5: Si posición <= 0, insertar en el tope (cima) + if (posicion <= 0) { nuevoNodo.setNext(cabeza); cabeza = nuevoNodo; historialPasos.push(crearRegistro(cabeza, [], [], 2)); @@ -181,8 +191,8 @@ export const Insertar = (cabeza, valor, posicion) => { return { historialPasos, nuevaCabeza: cabeza }; } - // Paso 6: Si la posición es 0, insertar al fondo - if (posicion === 0) { + // 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)); @@ -197,34 +207,33 @@ export const Insertar = (cabeza, valor, posicion) => { // 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 - else { - // Convertir posición (desde el fondo) a índice (desde el tope) - let indiceDesdeTop = tamanoPila - posicion - 1; - let nodoActual = cabeza; - let contador = 0; - historialPasos.push(crearRegistro(cabeza, [valor], [], 3)); - // Navegar hasta la posición anterior - while (contador < indiceDesdeTop) { - nodoActual = nodoActual.getNext(); - contador++; - historialPasos.push( - crearRegistro(cabeza, [valor], [nodoActual.getValue()], 3) - ); - } + // Paso 7: Insertar en posición intermedia (contando desde el tope) + let nodoActual = cabeza; + let contador = 0; + historialPasos.push(crearRegistro(cabeza, [valor], [], 3)); - // Insertar el nuevo nodo - nuevoNodo.setNext(nodoActual.getNext()); + // Navegar hasta la posición anterior + while (contador < posicion - 1) { + nodoActual = nodoActual.getNext(); + contador++; historialPasos.push( - crearRegistro(cabeza, [valor], [nodoActual.getValue()], 4) + crearRegistro(cabeza, [valor], [nodoActual.getValue()], 3) ); - - nodoActual.setNext(nuevoNodo); - historialPasos.push(crearRegistro(cabeza, [], [nuevoNodo.getValue()], 4)); } + // 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)); @@ -252,83 +261,88 @@ export const Extraer = (cabeza, posicion) => { temp = temp.getNext(); } - // Paso 4: Verificar que la posición sea válida - if (posicion >= tamanoPila) { + // 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; - // Paso 5: Si la posición es la última (tope), eliminar del tope (pop) - if (posicion === tamanoPila - 1) { + // Caso 1: Eliminar en posición 0 (tope -> pop-like) + if (posicion === 0) { valorEliminado = cabeza.getValue(); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 2)); + + // 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, [valorEliminado], [], 3)); + historialPasos.push(crearRegistro(cabeza, [], [], 3)); + historialPasos.push(crearRegistro(cabeza, [], [], 4)); + return { historialPasos, nuevaCabeza: cabeza, valorEliminado }; } - // Paso 6: Eliminar en otras posiciones - else { - historialPasos.push(crearRegistro(cabeza, [], [], 2)); - // Si la posición es 0 (fondo), usar lógica especial - if (posicion === 0) { - // Caso especial: eliminar el último nodo de la cadena - if (tamanoPila === 1) { - // Solo hay un elemento, eliminarlo - valorEliminado = cabeza.getValue(); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 3)); - cabeza = null; - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 4)); - } else { - // Encontrar el penúltimo nodo - let nodoActual = cabeza; - while (nodoActual.getNext().getNext() !== null) { - nodoActual = nodoActual.getNext(); - historialPasos.push( - crearRegistro(cabeza, [], [nodoActual.getValue()], 2) - ); - } - - // Eliminar el último nodo - valorEliminado = nodoActual.getNext().getValue(); - historialPasos.push( - crearRegistro(cabeza, [valorEliminado], [nodoActual.getValue()], 3) - ); - - nodoActual.setNext(null); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 4)); - } - } - // Eliminar en posición intermedia - else { - // Convertir posición (desde el fondo) a índice (desde el tope) - let indiceDesdeTop = tamanoPila - posicion - 1; - let nodoActual = cabeza; - let contador = 0; - - // Navegar hasta el nodo anterior al que se va a eliminar - while (contador < indiceDesdeTop - 1) { - nodoActual = nodoActual.getNext(); - contador++; - historialPasos.push( - crearRegistro(cabeza, [], [nodoActual.getValue()], 2) - ); - } - - // Guardar el valor a eliminar - valorEliminado = nodoActual.getNext().getValue(); + // 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, [valorEliminado], [nodoActual.getValue()], 3) + crearRegistro(cabeza, [], [nodoActual.getValue()], 2) ); - - // Eliminar el nodo de la cadena - nodoActual.setNext(nodoActual.getNext().getNext()); - historialPasos.push(crearRegistro(cabeza, [valorEliminado], [], 4)); } + + // 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)); diff --git a/src/animations/stack/components/StackChart.jsx b/src/animations/stack/components/StackChart.jsx index 0754a2b..4efbe2b 100644 --- a/src/animations/stack/components/StackChart.jsx +++ b/src/animations/stack/components/StackChart.jsx @@ -1,4 +1,5 @@ import { useSelector } from 'react-redux'; +import { useState, useEffect } from 'react'; // Función para contar nodos en la pila const countNodes = node => { @@ -14,7 +15,7 @@ const stackToArray = (node, arr = []) => { }; export const StackChart = () => { - const { head, isHead, firstSet, secondSet } = useSelector( + const { head, isHead, firstSet, secondSet, deletingNode } = useSelector( state => state.stack ); @@ -77,6 +78,7 @@ export const StackChart = () => { isHead={isHead} firstSet={firstSet} secondSet={secondSet} + deletingNode={deletingNode} height={height} fontSize={fontSize} /> @@ -86,35 +88,68 @@ export const StackChart = () => { ); }; -const StackNode = ({ node, isHead, firstSet, secondSet, height, fontSize }) => { +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 isDangerNode = secondSet.includes(node.value); + 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 -
    - )} - {node.value} +
    + {node.value === isHead && ( +
    + + head +
    + )} + {node.value} +
    ); }; diff --git a/src/quiz/pages/DataStructuresPage.jsx b/src/quiz/pages/DataStructuresPage.jsx index 3f3f263..0910a36 100644 --- a/src/quiz/pages/DataStructuresPage.jsx +++ b/src/quiz/pages/DataStructuresPage.jsx @@ -1,4 +1,5 @@ import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; import CardTabla from '../../components/CardTabla'; import { SearchMethodGrid } from '../components'; import { useEffect } from 'react'; @@ -15,6 +16,7 @@ import { useTranslation } from 'react-i18next'; export const DataStructuresPage = () => { const { t } = useTranslation(); const dispatch = useDispatch(); + const navigate = useNavigate(); useEffect(() => { dispatch(resetStateForQuiz()); @@ -55,9 +57,18 @@ export const DataStructuresPage = () => { return (
    -

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

    +
    + +

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

    +
    +
    diff --git a/src/quiz/pages/SearchMethodsPage.jsx b/src/quiz/pages/SearchMethodsPage.jsx index 82d6c94..a74ed18 100644 --- a/src/quiz/pages/SearchMethodsPage.jsx +++ b/src/quiz/pages/SearchMethodsPage.jsx @@ -1,4 +1,5 @@ import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; import CardTabla from '../../components/CardTabla'; import { SearchMethodGrid } from '../components'; import { useEffect } from 'react'; @@ -9,6 +10,7 @@ import { useTranslation } from 'react-i18next'; export const SearchMethodsPage = () => { const { t } = useTranslation(); const dispatch = useDispatch(); + const navigate = useNavigate(); useEffect(() => { dispatch(resetStateForQuiz()); @@ -31,9 +33,18 @@ export const SearchMethodsPage = () => { return (
    -

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

    +
    + +

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

    +
    +
    diff --git a/src/quiz/pages/SortingMethodsPage.jsx b/src/quiz/pages/SortingMethodsPage.jsx index 4e8cb86..53ce894 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'; @@ -10,6 +11,7 @@ import { useTranslation } from 'react-i18next'; export const SortingMethodsPage = () => { const { t } = useTranslation(); const dispatch = useDispatch(); + const navigate = useNavigate(); useEffect(() => { dispatch(resetStateForQuiz()); @@ -22,9 +24,18 @@ export const SortingMethodsPage = () => { return (
    -

    - {t('sortingMethods')} -

    +
    + +

    + {t('sortingMethods')} +

    +
    +
    diff --git a/src/store/stack/stackSlice.js b/src/store/stack/stackSlice.js index 532cf9c..ef2a066 100644 --- a/src/store/stack/stackSlice.js +++ b/src/store/stack/stackSlice.js @@ -12,6 +12,7 @@ const initialState = { history: -1, firstSet: [], secondSet: [], + deletingNode: null, timeId: [], isHead: -1, @@ -41,6 +42,7 @@ export const stackSlice = createSlice({ state.history = -1; state.firstSet = []; state.secondSet = []; + state.deletingNode = null; state.timeId = []; state.srcHead = action.payload; @@ -69,6 +71,7 @@ export const stackSlice = createSlice({ state.secondSet = currVisualization.secondSet; state.isHead = currVisualization.isHead; state.elementos = elementos; + state.deletingNode = currVisualization.deletingNode || null; }, restoreRepeatStack: state => { const head = state.srcHead; @@ -76,6 +79,7 @@ export const stackSlice = createSlice({ state.history = -1; state.firstSet = []; state.secondSet = []; + state.deletingNode = null; state.isHead = -1; // Actualizar elementos desde la cabeza restaurada From bf31c82c060d91b9c461d687ab26c4786ce0fcb0 Mon Sep 17 00:00:00 2001 From: FrankDev Date: Wed, 4 Mar 2026 18:37:29 -0500 Subject: [PATCH 14/18] Animaciones Corregidas,Codigos de JAVA, Implementacion de GoogleAnalytics --- GOOGLE_ANALYTICS_SETUP.md | 183 ++++++++++++++++++ package-lock.json | 14 +- package.json | 11 +- src/App.jsx | 61 +++--- .../binarySearch/algorithms/index.js | 26 +-- .../algorithms/DoubleListAlgorithm.jsx | 127 ++++++------ .../linearSearch/algorithms/index.js | 12 +- .../queue/algorithms/QueueAlgorithm.jsx | 87 +++++---- .../algorithms/SimpleListAlgorithm.jsx | 98 +++++----- .../sorting/algorithms/BubbleSort.jsx | 19 +- .../sorting/algorithms/HeapSort.jsx | 17 +- .../sorting/algorithms/InsertionSort.jsx | 19 +- .../sorting/algorithms/QuickSort.jsx | 14 +- .../sorting/algorithms/SelectionSort.jsx | 18 +- .../sorting/algorithms/ShellSort.jsx | 24 ++- .../stack/algorithms/StackAlgorithm.jsx | 74 ++++--- src/hooks/usePageTracking.js | 12 ++ src/main.jsx | 4 + src/utils/googleAnalytics.js | 42 ++++ src/utils/trackingEvents.js | 43 ++++ 20 files changed, 649 insertions(+), 256 deletions(-) create mode 100644 GOOGLE_ANALYTICS_SETUP.md create mode 100644 src/hooks/usePageTracking.js create mode 100644 src/utils/googleAnalytics.js create mode 100644 src/utils/trackingEvents.js 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/package-lock.json b/package-lock.json index 6852f45..a5f4384 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,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", @@ -12287,6 +12288,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", @@ -21450,6 +21457,11 @@ "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", diff --git a/package.json b/package.json index e8842e7..0a3cee9 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,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 +43,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 84c8ae5..9eb7534 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -21,39 +21,46 @@ import { AnimationView } from './animations'; import { AppTheme } from './theme'; import { Provider } from 'react-redux'; import { store } from './store'; +import { usePageTracking } from './hooks/usePageTracking'; + +function AppContent() { + // Rastrear cambios de página automáticamente + usePageTracking(); + + return ( +
    + + + } /> + } /> + } /> + } + /> + } /> + } + /> + } /> + } /> + } /> + } /> + } /> + } /> + +
    +
    + ); +} function App() { return ( -
    - - - } /> - } /> - } /> - } - /> - } - /> - } - /> - } /> - } /> - } /> - } /> - } /> - } /> - -
    -
    +
    diff --git a/src/animations/binarySearch/algorithms/index.js b/src/animations/binarySearch/algorithms/index.js index ed2f4f9..feee8b5 100644 --- a/src/animations/binarySearch/algorithms/index.js +++ b/src/animations/binarySearch/algorithms/index.js @@ -136,20 +136,20 @@ export const busquedaBinaria = (arr, target) => { }; export const BusquedaBinariaCode = () => { - return `function busquedaBinaria(arr, target) { - let izquierda = 0; - let derecha = arr.length - 1; - while (izquierda <= derecha) { - let medio = Math.floor((izq + der) / 2); - if (arr[medio] === target) { - return medio; // Encontrado - } else if (arr[medio] < target) { - izquierda = medio + 1; - } else { - derecha = medio - 1; + 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; // No encontrado + return -1; }`; }; diff --git a/src/animations/doubleList/algorithms/DoubleListAlgorithm.jsx b/src/animations/doubleList/algorithms/DoubleListAlgorithm.jsx index 129c752..5277604 100644 --- a/src/animations/doubleList/algorithms/DoubleListAlgorithm.jsx +++ b/src/animations/doubleList/algorithms/DoubleListAlgorithm.jsx @@ -902,89 +902,108 @@ export const Buscar = (cabeza, valor) => { // Componentes de código para visualización export const InsertarAlInicioCode = ({ codeRef }) => (
    -

    {'// Insertar al inicio - Lista Doble'}

    -

    {'nuevoNodo = new NodoDoble(valor)'}

    -

    {'nuevoNodo.next = cabeza'}

    -

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

    -

    {'cabeza = nuevoNodo'}

    -

    {'return cabeza'}

    +

    {'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 }) => (
    -

    {'// Insertar al final - Lista Doble'}

    -

    {'nuevoNodo = new NodoDoble(valor)'}

    -

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

    -

    {'actual = cabeza'}

    -

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

    -

    {' actual = actual.next'}

    -

    {'actual.next = nuevoNodo'}

    -

    {'nuevoNodo.prev = actual'}

    -

    {'return cabeza'}

    +

    {'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 }) => (
    -

    {'// Insertar en posición - Lista Doble'}

    -

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

    -

    {'nuevoNodo = new NodoDoble(valor)'}

    -

    {'actual = cabeza; cont = 0'}

    -

    {'while (cont < pos - 1 && actual != null)'}

    -

    {' actual = actual.next; cont++'}

    -

    {'nuevoNodo.next = actual.next'}

    -

    {'nuevoNodo.prev = actual'}

    -

    {'if (actual.next != null) actual.next.prev = nuevoNodo'}

    -

    {'actual.next = nuevoNodo'}

    -

    {'return cabeza'}

    +

    {'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 }) => (
    -

    {'// Eliminar del inicio - Lista Doble'}

    -

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

    -

    {'cabeza = cabeza.next'}

    -

    {'if (cabeza != null) cabeza.prev = null'}

    -

    {'return cabeza'}

    +

    {'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 }) => (
    -

    {'// Eliminar del final - Lista Doble'}

    -

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

    -

    {'if (cabeza.next == null) return null'}

    -

    {'actual = cabeza'}

    -

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

    -

    {' actual = actual.next'}

    -

    {'actual.prev.next = null'}

    -

    {'return cabeza'}

    +

    {'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 }) => (
    -

    {'// Eliminar en posición - Lista Doble'}

    -

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

    -

    {'actual = cabeza; cont = 0'}

    -

    {'while (cont < pos && actual != null)'}

    -

    {' actual = actual.next; cont++'}

    -

    {'if (actual.prev != null) actual.prev.next = actual.next'}

    -

    {'if (actual.next != null) actual.next.prev = actual.prev'}

    -

    {'return cabeza'}

    +

    {'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 }) => (
    -

    {'// Buscar en lista doble'}

    -

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

    -

    {'actual = cabeza; pos = 0'}

    -

    {'while (actual != null)'}

    -

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

    -

    {' actual = actual.next; pos++'}

    -

    {'return -1'}

    +

    {'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/linearSearch/algorithms/index.js b/src/animations/linearSearch/algorithms/index.js index 3f1cfbb..da44bb9 100644 --- a/src/animations/linearSearch/algorithms/index.js +++ b/src/animations/linearSearch/algorithms/index.js @@ -88,12 +88,12 @@ export const busquedaLineal = (array, target) => { }; export const BusquedaLinealCode = () => { - return `function busquedaLineal(arreglo, objetivo) { - for (let i = 0; i < arreglo.length; i++) { - if (arreglo[i] === objetivo) { - return i; // Encontrado + 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; // No encontrado + return -1; }`; }; diff --git a/src/animations/queue/algorithms/QueueAlgorithm.jsx b/src/animations/queue/algorithms/QueueAlgorithm.jsx index 7f0e3e0..7ad10ba 100644 --- a/src/animations/queue/algorithms/QueueAlgorithm.jsx +++ b/src/animations/queue/algorithms/QueueAlgorithm.jsx @@ -352,13 +352,17 @@ export const Extraer = (cabeza, posicion) => { export const EnqueueCode = ({ codeRef }) => { return ( -
    {`nuevoNodo = new Nodo(valor)`}
    -
    {`if (head === null) head = nuevoNodo`}
    -
    {`else {`}
    -
    {`  actual = head`}
    -
    {`  while (actual.getNext() !== null)`}
    -
    {`    actual = actual.getNext()`}
    -
    {`  actual.setNext(nuevoNodo)`}
    +
    {`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);`}
    {`}`}
    ); @@ -367,9 +371,12 @@ export const EnqueueCode = ({ codeRef }) => { export const DequeueCode = ({ codeRef }) => { return ( -
    {`if (head === null) return error`}
    -
    {`valor = head.getValue()`}
    -
    {`head = head.getNext()`}
    +
    {`public int dequeue() {`}
    +
    {`    if (head == null) return -1;`}
    +
    {`    int valor = head.getValue();`}
    +
    {`    head = head.getNext();`}
    +
    {`    return valor;`}
    +
    {`}`}
    ); }; @@ -377,12 +384,18 @@ export const DequeueCode = ({ codeRef }) => { export const EliminarFinalCode = ({ codeRef }) => { return ( -
    {`if (head === null) return error`}
    -
    {`if (head.getNext() === null) { head = null; return }`}
    -
    {`actual = head`}
    -
    {`while (actual.getNext().getNext() !== null)`}
    -
    {`  actual = actual.getNext()`}
    -
    {`actual.setNext(null)`}
    +
    {`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);`}
    +
    {`}`}
    ); }; @@ -390,15 +403,16 @@ export const EliminarFinalCode = ({ codeRef }) => { export const InsertarCode = ({ codeRef }) => { return ( -
    {`nuevoNodo = new Nodo(valor)`}
    -
    {`if (head === null && posicion === 0) head = nuevoNodo`}
    -
    {`else if (posicion === 0) { nuevoNodo.setNext(head); head = nuevoNodo }`}
    -
    {`else {`}
    -
    {`  actual = head`}
    -
    {`  for (i = 0; i < posicion - 1; i++)`}
    -
    {`    actual = actual.getNext()`}
    -
    {`  nuevoNodo.setNext(actual.getNext())`}
    -
    {`  actual.setNext(nuevoNodo)`}
    +
    {`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;`}
    +
    {`    }`}
    {`}`}
    ); @@ -407,15 +421,20 @@ export const InsertarCode = ({ codeRef }) => { export const ExtraerCode = ({ codeRef }) => { return ( -
    {`if (head === null) return error`}
    -
    {`if (posicion >= tamano) return error`}
    -
    {`if (posicion === 0) head = head.getNext()`}
    -
    {`else {`}
    -
    {`  actual = head`}
    -
    {`  for (i = 0; i < posicion - 1; i++)`}
    -
    {`    actual = actual.getNext()`}
    -
    {`  eliminado = actual.getNext()`}
    -
    {`  actual.setNext(actual.getNext().getNext())`}
    +
    {`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/simpleList/algorithms/SimpleListAlgorithm.jsx b/src/animations/simpleList/algorithms/SimpleListAlgorithm.jsx index 871bd34..c135df6 100644 --- a/src/animations/simpleList/algorithms/SimpleListAlgorithm.jsx +++ b/src/animations/simpleList/algorithms/SimpleListAlgorithm.jsx @@ -1129,9 +1129,9 @@ export const DrawLinkedList = ({ record, cabeza }) => { // Funciones de visualización de código export const InsertarAlInicioCode = ({ codeRef, highlightedLines = [] }) => { const lines = [ - 'nuevoNodo = new Nodo(valor)', - 'nuevoNodo.setNext(cabeza)', - 'cabeza = nuevoNodo', + 'Node nuevoNodo = new Node(valor);', + 'nuevoNodo.setNext(cabeza);', + 'cabeza = nuevoNodo;', ]; return ( @@ -1152,13 +1152,12 @@ export const InsertarAlInicioCode = ({ codeRef, highlightedLines = [] }) => { export const InsertarAlFinalCode = ({ codeRef, highlightedLines = [] }) => { const lines = [ - 'nuevoNodo = new Nodo(valor)', - 'if (cabeza === null) cabeza = nuevoNodo', - 'else {', - ' actual = cabeza', - ' while (actual.getNext() !== null)', - ' actual = actual.getNext()', - ' actual.setNext(nuevoNodo)', + '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 ( @@ -1179,14 +1178,16 @@ export const InsertarAlFinalCode = ({ codeRef, highlightedLines = [] }) => { export const InsertarEnPosicionCode = ({ codeRef, highlightedLines = [] }) => { const lines = [ - 'if (posicion === 0) return insertarAlInicio(valor)', - 'nuevoNodo = new Nodo(valor)', - 'if (cabeza === null) return error', - 'actual = cabeza', - 'for (i = 0; i < posicion - 1; i++)', - ' if (actual === null) return error', - 'nuevoNodo.setNext(actual.getNext())', - 'actual.setNext(nuevoNodo)', + '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 ( @@ -1207,9 +1208,10 @@ export const InsertarEnPosicionCode = ({ codeRef, highlightedLines = [] }) => { export const EliminarDelInicioCode = ({ codeRef, highlightedLines = [] }) => { const lines = [ - 'if (cabeza === null) return error', - 'valorEliminado = cabeza.getValue()', - 'cabeza = cabeza.getNext()', + 'if (cabeza == null) return -1;', + 'int valor = cabeza.getValue();', + 'cabeza = cabeza.getNext();', + 'return valor;', ]; return ( @@ -1230,15 +1232,18 @@ export const EliminarDelInicioCode = ({ codeRef, highlightedLines = [] }) => { export const EliminarDelFinalCode = ({ codeRef, highlightedLines = [] }) => { const lines = [ - 'if (cabeza === null) return error', - 'if (cabeza.getNext() === null) {', - ' valorEliminado = cabeza.getValue()', - ' cabeza = null', - '} else {', - ' while (actual.getNext().getNext() !== null)', - ' actual = actual.getNext()', - ' valorEliminado = actual.getNext().getValue()', - ' actual.setNext(null)', + '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 ( @@ -1259,13 +1264,14 @@ export const EliminarDelFinalCode = ({ codeRef, highlightedLines = [] }) => { export const EliminarEnPosicionCode = ({ codeRef, highlightedLines = [] }) => { const lines = [ - 'if (posicion === 0) return eliminarDelInicio()', - 'actual = cabeza', - 'for (i = 0; i < posicion - 1; i++)', - ' actual = actual.getNext()', - 'if (actual === null || actual.getNext() === null) return error', - 'valorEliminado = actual.getNext().getValue()', - 'actual.setNext(actual.getNext().getNext())', + '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 ( @@ -1286,16 +1292,16 @@ export const EliminarEnPosicionCode = ({ codeRef, highlightedLines = [] }) => { export const BuscarCode = ({ codeRef, highlightedLines = [] }) => { const lines = [ - 'if (cabeza === null) return not found', - 'actual = cabeza', - 'posicion = 0', - 'while (actual !== null) {', - ' if (actual.getValue() === valor)', - ' return found at posicion', - ' actual = actual.getNext()', - ' posicion++', + '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 not found', + 'return -1;', ]; return ( 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/stack/algorithms/StackAlgorithm.jsx b/src/animations/stack/algorithms/StackAlgorithm.jsx index 80787f0..711c91a 100644 --- a/src/animations/stack/algorithms/StackAlgorithm.jsx +++ b/src/animations/stack/algorithms/StackAlgorithm.jsx @@ -353,10 +353,15 @@ export const Extraer = (cabeza, posicion) => { export const PushCode = ({ codeRef }) => { return ( -
    {` nuevoNodo = new Nodo(valor)`}
    -
    {`if (cabeza === null) cabeza = nuevoNodo`}
    -
    {`nuevoNodo.setNext(cabeza)`}
    -
    {`cabeza = nuevoNodo`}
    +
    {`public void push(int valor) {`}
    +
    {`    Node nuevoNodo = new Node(valor);`}
    +
    {`    if (cabeza == null) {`}
    +
    {`        cabeza = nuevoNodo;`}
    +
    {`    } else {`}
    +
    {`        nuevoNodo.setNext(cabeza);`}
    +
    {`        cabeza = nuevoNodo;`}
    +
    {`    }`}
    +
    {`}`}
    ); }; @@ -364,9 +369,12 @@ export const PushCode = ({ codeRef }) => { export const PopCode = ({ codeRef }) => { return ( -
    {`if (cabeza === null) return error`}
    -
    {`valorEliminado = cabeza.getValue()`}
    -
    {`cabeza = cabeza.getNext()`}
    +
    {`public int pop() {`}
    +
    {`    if (cabeza == null) return -1;`}
    +
    {`    int valor = cabeza.getValue();`}
    +
    {`    cabeza = cabeza.getNext();`}
    +
    {`    return valor;`}
    +
    {`}`}
    ); }; @@ -374,11 +382,16 @@ export const PopCode = ({ codeRef }) => { export const SumergirCode = ({ codeRef }) => { return ( -
    {`if (cabeza === null || cabeza.getNext() === null) return`}
    -
    {`nodoTope = cabeza`}
    -
    {`cabeza = cabeza.getNext()`}
    -
    {`while (nodoActual.getNext() !== null) nodoActual = nodoActual.getNext()`}
    -
    {`nodoTope.setNext(null); nodoActual.setNext(nodoTope)`}
    +
    {`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);`}
    +
    {`}`}
    ); }; @@ -386,12 +399,19 @@ export const SumergirCode = ({ codeRef }) => { export const InsertarCode = ({ codeRef }) => { return ( -
    {`nuevoNodo = new Nodo(valor)`}
    -
    {`if (cabeza === null && posicion === 0) cabeza = nuevoNodo`}
    -
    {`while (temp !== null) { tamanoPila++; temp = temp.getNext() }`}
    -
    {`nuevoNodo.setNext(cabeza); cabeza = nuevoNodo`}
    -
    {`ultimo.setNext(nuevoNodo)`}
    -
    {`indiceDesdeTop = tamanoPila - posicion - 1`}
    +
    {`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();`}
    +
    {`    }`}
    +
    {`}`}
    ); }; @@ -399,14 +419,16 @@ export const InsertarCode = ({ codeRef }) => { export const ExtraerCode = ({ codeRef }) => { return ( -
    {`if (cabeza === null) return error`}
    -
    {`while (temp !== null) { tamanoPila++; temp = temp.getNext() }`}
    -
    {`if (posicion >= tamanoPila) return error`}
    -
    {`valorEliminado = cabeza.getValue(); cabeza = cabeza.getNext()`}
    -
    {`penúltimo.setNext(null)`}
    -
    {`indiceDesdeTop = tamanoPila - posicion - 1`}
    -
    {`valorEliminado = nodoActual.getNext().getValue()`}
    -
    {`nodoActual.setNext(nodoActual.getNext().getNext())`}
    +
    {`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/hooks/usePageTracking.js b/src/hooks/usePageTracking.js new file mode 100644 index 0000000..06cba5a --- /dev/null +++ b/src/hooks/usePageTracking.js @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; +import { trackPageView } from '../utils/googleAnalytics'; + +export const usePageTracking = () => { + const location = useLocation(); + + useEffect(() => { + // Rastrear cada vez que cambia la ruta + trackPageView(location.pathname); + }, [location.pathname]); +}; diff --git a/src/main.jsx b/src/main.jsx index ca820a1..39ba509 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -6,6 +6,7 @@ import i18next from 'i18next'; import { initReactI18next } from 'react-i18next'; import en from './i18n/en.json'; import es from './i18n/es.json'; +import { initializeGoogleAnalytics } from './utils/googleAnalytics'; export const defaultLanguage = 'es'; const storedLanguage = localStorage.getItem('lang') || defaultLanguage; @@ -25,6 +26,9 @@ i18next.use(initReactI18next).init({ }, }); +// Inicializar Google Analytics +initializeGoogleAnalytics('G-6G1Z8N0TNS'); + ReactDOM.createRoot(document.getElementById('root')).render( // diff --git a/src/utils/googleAnalytics.js b/src/utils/googleAnalytics.js new file mode 100644 index 0000000..57d3770 --- /dev/null +++ b/src/utils/googleAnalytics.js @@ -0,0 +1,42 @@ +// Inicializar Google Analytics +export const initializeGoogleAnalytics = 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()); + gtag('config', measurementId, { + page_path: window.location.pathname, + }); + + // 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 +export const trackPageView = pageName => { + if (window.gtag) { + window.gtag('config', '', { + page_path: pageName, + page_title: pageName, + }); + } +}; 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(), + }); +}; From 3f2c4aa8ded7b8646f5dce0c1f7003715ae533af Mon Sep 17 00:00:00 2001 From: FrankDev Date: Wed, 4 Mar 2026 19:25:17 -0500 Subject: [PATCH 15/18] =?UTF-8?q?fix:=20Corregir=20rastreo=20de=20p=C3=A1g?= =?UTF-8?q?inas=20en=20Google=20Analytics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/googleAnalytics.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/utils/googleAnalytics.js b/src/utils/googleAnalytics.js index 57d3770..b3ab265 100644 --- a/src/utils/googleAnalytics.js +++ b/src/utils/googleAnalytics.js @@ -1,5 +1,11 @@ +// 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; @@ -33,10 +39,10 @@ export const trackEvent = (eventName, eventParams = {}) => { // Función para rastrear cambios de página export const trackPageView = pageName => { - if (window.gtag) { - window.gtag('config', '', { + if (window.gtag && MEASUREMENT_ID) { + window.gtag('config', MEASUREMENT_ID, { page_path: pageName, - page_title: pageName, + page_title: document.title, }); } }; From 0f45bc718af21d8e91f4016f968310ac266fdff6 Mon Sep 17 00:00:00 2001 From: FrankDev Date: Wed, 4 Mar 2026 20:09:05 -0500 Subject: [PATCH 16/18] =?UTF-8?q?fix:=20Corregir=20rastreo=20de=20p=C3=A1g?= =?UTF-8?q?inas=20en=20SPA=20con=20evento=20page=5Fview=20de=20GA4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/usePageTracking.js | 10 +++++++--- src/utils/googleAnalytics.js | 10 ++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/hooks/usePageTracking.js b/src/hooks/usePageTracking.js index 06cba5a..602a301 100644 --- a/src/hooks/usePageTracking.js +++ b/src/hooks/usePageTracking.js @@ -6,7 +6,11 @@ export const usePageTracking = () => { const location = useLocation(); useEffect(() => { - // Rastrear cada vez que cambia la ruta - trackPageView(location.pathname); - }, [location.pathname]); + // Pequeño delay para asegurar que gtag esté listo + const timeout = setTimeout(() => { + trackPageView(location.pathname + location.search); + }, 100); + + return () => clearTimeout(timeout); + }, [location.pathname, location.search]); }; diff --git a/src/utils/googleAnalytics.js b/src/utils/googleAnalytics.js index b3ab265..a10d86c 100644 --- a/src/utils/googleAnalytics.js +++ b/src/utils/googleAnalytics.js @@ -22,8 +22,9 @@ export const initializeGoogleAnalytics = measurementId => { } gtag('js', new Date()); + // Desactivar el page_view automático para manejarlo manualmente en la SPA gtag('config', measurementId, { - page_path: window.location.pathname, + send_page_view: false, }); // Guardar gtag en window para usarla en toda la app @@ -37,11 +38,12 @@ export const trackEvent = (eventName, eventParams = {}) => { } }; -// Función para rastrear cambios de página +// Función para rastrear cambios de página (GA4 - evento page_view) export const trackPageView = pageName => { - if (window.gtag && MEASUREMENT_ID) { - window.gtag('config', MEASUREMENT_ID, { + if (window.gtag) { + window.gtag('event', 'page_view', { page_path: pageName, + page_location: window.location.origin + pageName, page_title: document.title, }); } From 8f446188ff564b3dab120366b4ef3f2ab22f380c Mon Sep 17 00:00:00 2001 From: FrankDev Date: Wed, 18 Mar 2026 21:22:33 -0500 Subject: [PATCH 17/18] firebase auth, firestore tracking y registro de botones exitosos --- .env.example | 11 + .gitignore | 5 + index.html | 1 + package-lock.json | 1576 ++++++++++++++++- package.json | 2 + src/App.jsx | 5 +- .../components/DoubleListControls.jsx | 26 + .../components/LinearSearchControls.jsx | 7 + .../queue/components/QueueControls.jsx | 6 + .../components/SimpleListControls.jsx | 26 + .../stack/components/StackControls.jsx | 6 + src/animations/views/AnimationView.jsx | 2 + src/components/Auth/LoginButton.jsx | 83 + src/components/Auth/UserProfile.jsx | 115 ++ src/components/HomeAprende.jsx | 2 + src/components/MobileMenu.jsx | 23 +- src/components/NavBar.jsx | 19 +- src/config/firebaseConfig.js | 42 + src/hooks/useActivity.js | 47 + src/hooks/useAuth.js | 53 + src/hooks/usePageActivity.js | 47 + src/hooks/useSessionClose.js | 55 + src/quiz/pages/DataStructuresPage.jsx | 2 + src/quiz/pages/SearchMethodsPage.jsx | 2 + src/quiz/pages/SortingMethodsPage.jsx | 2 + src/quiz/views/QuizView.jsx | 7 + src/services/authService.js | 39 + src/services/firestoreService.js | 138 ++ src/utils/clickRegistry.js | 21 + 29 files changed, 2304 insertions(+), 66 deletions(-) create mode 100644 .env.example create mode 100644 src/components/Auth/LoginButton.jsx create mode 100644 src/components/Auth/UserProfile.jsx create mode 100644 src/config/firebaseConfig.js create mode 100644 src/hooks/useActivity.js create mode 100644 src/hooks/useAuth.js create mode 100644 src/hooks/usePageActivity.js create mode 100644 src/hooks/useSessionClose.js create mode 100644 src/services/authService.js create mode 100644 src/services/firestoreService.js create mode 100644 src/utils/clickRegistry.js 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/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 a5f4384..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", @@ -2530,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", @@ -3420,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", @@ -3649,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", @@ -4073,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", @@ -4301,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" } @@ -5094,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", @@ -5521,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", @@ -5698,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" } @@ -6184,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", @@ -6268,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", @@ -6412,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.*" } @@ -6674,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", @@ -6763,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", @@ -6995,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" } @@ -9550,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", @@ -9702,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", @@ -10599,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", @@ -10948,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" } @@ -11188,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", @@ -11462,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", @@ -11554,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" }, @@ -11903,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", @@ -12303,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", @@ -12653,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", @@ -12670,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" }, @@ -12685,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" }, @@ -12696,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", @@ -12758,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" } @@ -12782,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", @@ -12800,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" } @@ -14450,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", @@ -15126,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", @@ -15244,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", @@ -15582,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", @@ -15775,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", @@ -16325,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", @@ -16653,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", @@ -16798,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", @@ -17158,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", @@ -17229,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", @@ -17336,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", @@ -17524,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", @@ -17577,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", @@ -17738,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", @@ -19569,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", @@ -19666,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", @@ -20282,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", @@ -20518,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", @@ -20681,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", @@ -20881,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", @@ -20952,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" } @@ -21217,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", @@ -21468,6 +22928,21 @@ "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", @@ -21781,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", @@ -21792,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" } @@ -21801,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" } @@ -21809,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==" } } }, @@ -21852,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", @@ -21871,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", @@ -21885,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 0a3cee9..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", diff --git a/src/App.jsx b/src/App.jsx index 9eb7534..82b0d96 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -22,10 +22,13 @@ 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'; function AppContent() { - // Rastrear cambios de página automáticamente usePageTracking(); + const { loginTime } = useAuth(); + useSessionClose(loginTime); return (
    diff --git a/src/animations/doubleList/components/DoubleListControls.jsx b/src/animations/doubleList/components/DoubleListControls.jsx index eeefe78..31bd47c 100644 --- a/src/animations/doubleList/components/DoubleListControls.jsx +++ b/src/animations/doubleList/components/DoubleListControls.jsx @@ -20,6 +20,7 @@ import { svgRepeat, svgForward, } from '../../../assets/svg/SvgConstans'; +import { registrarClick } from '../../../utils/clickRegistry'; export const DoubleListControls = () => { const doubleListState = useSelector(state => state.doubleList); @@ -252,6 +253,10 @@ export const DoubleListControls = () => { } executeAnimation('insertarAlInicio', numValue); + registrarClick( + 'Insertar al inicio', + `Insertar ${numValue} al inicio de la lista doble` + ); setValue(''); }; @@ -269,6 +274,10 @@ export const DoubleListControls = () => { } executeAnimation('insertarAlFinal', numValue); + registrarClick( + 'Insertar al final', + `Insertar ${numValue} al final de la lista doble` + ); setValue(''); }; @@ -298,6 +307,10 @@ export const DoubleListControls = () => { } executeAnimation('insertarEnPosicion', numValue, pos); + registrarClick( + 'Insertar en posición', + `Insertar ${numValue} en posición ${pos} en lista doble` + ); setValue(''); setPosition(''); }; @@ -309,6 +322,10 @@ export const DoubleListControls = () => { } executeAnimation('eliminarDelInicio', null); + registrarClick( + 'Eliminar del inicio', + 'Eliminar primer elemento de la lista doble' + ); }; const handleEliminarDelFinal = () => { @@ -318,6 +335,10 @@ export const DoubleListControls = () => { } executeAnimation('eliminarDelFinal', null); + registrarClick( + 'Eliminar del final', + 'Eliminar último elemento de la lista doble' + ); }; const handleEliminarEnPosicion = () => { @@ -340,6 +361,10 @@ export const DoubleListControls = () => { } executeAnimation('eliminarEnPosicion', null, pos); + registrarClick( + 'Eliminar en posición', + `Eliminar elemento en posición ${pos} de lista doble` + ); setDeletePosition(''); }; @@ -357,6 +382,7 @@ export const DoubleListControls = () => { } executeAnimation('buscar', numValue); + registrarClick('Buscar', `Buscar ${numValue} en la lista doble`); setSearchValue(''); }; diff --git a/src/animations/linearSearch/components/LinearSearchControls.jsx b/src/animations/linearSearch/components/LinearSearchControls.jsx index 4da1135..ea23144 100644 --- a/src/animations/linearSearch/components/LinearSearchControls.jsx +++ b/src/animations/linearSearch/components/LinearSearchControls.jsx @@ -22,6 +22,7 @@ import { svgRepeat, svgForward, } from '../../../assets/svg/SvgConstans'; +import { registrarClick } from '../../../utils/clickRegistry'; export const LinearSearchControls = () => { const linearSearchState = useSelector(state => state.linearSearch); @@ -183,6 +184,7 @@ export const LinearSearchControls = () => { dispatch(setArrayLinearSearch(values)); dispatch(resetLinearSearch()); clearActiveTimeouts(); + registrarClick('Cargar arreglo', `Cargar arreglo: [${values.join(', ')}]`); setToast('Arreglo cargado correctamente'); setOpen(true); }; @@ -197,6 +199,10 @@ export const LinearSearchControls = () => { dispatch(setArrayLinearSearch(randomArray)); dispatch(resetLinearSearch()); clearActiveTimeouts(); + registrarClick( + 'Arreglo aleatorio', + `Generar arreglo aleatorio: [${randomArray.join(', ')}]` + ); setToast('Arreglo aleatorio generado'); setOpen(true); }; @@ -208,6 +214,7 @@ export const LinearSearchControls = () => { return; } executeSearch(numValue); + registrarClick('Buscar', `Buscar ${numValue} en el arreglo`); setSearchValue(''); }; diff --git a/src/animations/queue/components/QueueControls.jsx b/src/animations/queue/components/QueueControls.jsx index 8f5f3b1..1f86bda 100644 --- a/src/animations/queue/components/QueueControls.jsx +++ b/src/animations/queue/components/QueueControls.jsx @@ -13,6 +13,7 @@ import { 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, @@ -183,6 +184,7 @@ export const QueueControls = () => { } executeAnimation('enqueue', numValue); + registrarClick('Enqueue', `Insertar ${numValue} en la cola`); setValue(''); }; @@ -194,6 +196,7 @@ export const QueueControls = () => { const frontValue = queueState.head.getValue(); executeAnimation('dequeue', frontValue); + registrarClick('Dequeue', `Eliminar ${frontValue} del frente de la cola`); }; const handleEliminarFinal = () => { @@ -203,6 +206,7 @@ export const QueueControls = () => { } executeAnimation('eliminarFinal', null); + registrarClick('Eliminar Final', 'Eliminar último elemento de la cola'); }; const handleInsert = () => { @@ -236,6 +240,7 @@ export const QueueControls = () => { } executeAnimation('insertar', numValue, pos); + registrarClick('Insertar', `Insertar ${numValue} en posición ${pos}`); setValue(''); setInsertPosition(''); }; @@ -261,6 +266,7 @@ export const QueueControls = () => { } executeAnimation('extraer', null, pos); + registrarClick('Extraer', `Extraer elemento en posición ${pos}`); setExtractPosition(''); }; diff --git a/src/animations/simpleList/components/SimpleListControls.jsx b/src/animations/simpleList/components/SimpleListControls.jsx index e413756..2111b68 100644 --- a/src/animations/simpleList/components/SimpleListControls.jsx +++ b/src/animations/simpleList/components/SimpleListControls.jsx @@ -20,6 +20,7 @@ import { svgRepeat, svgForward, } from '../../../assets/svg/SvgConstans'; +import { registrarClick } from '../../../utils/clickRegistry'; export const SimpleListControls = () => { const simpleListState = useSelector(state => state.simpleList); @@ -254,6 +255,10 @@ export const SimpleListControls = () => { } executeAnimation('insertarAlInicio', numValue); + registrarClick( + 'Insertar al inicio', + `Insertar ${numValue} al inicio de la lista` + ); setValue(''); }; @@ -271,6 +276,10 @@ export const SimpleListControls = () => { } executeAnimation('insertarAlFinal', numValue); + registrarClick( + 'Insertar al final', + `Insertar ${numValue} al final de la lista` + ); setValue(''); }; @@ -300,6 +309,10 @@ export const SimpleListControls = () => { } executeAnimation('insertarEnPosicion', numValue, pos); + registrarClick( + 'Insertar en posición', + `Insertar ${numValue} en posición ${pos}` + ); setValue(''); setPosition(''); }; @@ -311,6 +324,10 @@ export const SimpleListControls = () => { } executeAnimation('eliminarDelInicio', null); + registrarClick( + 'Eliminar del inicio', + 'Eliminar primer elemento de la lista' + ); }; const handleEliminarDelFinal = () => { @@ -320,6 +337,10 @@ export const SimpleListControls = () => { } executeAnimation('eliminarDelFinal', null); + registrarClick( + 'Eliminar del final', + 'Eliminar último elemento de la lista' + ); }; const handleEliminarEnPosicion = () => { @@ -342,6 +363,10 @@ export const SimpleListControls = () => { } executeAnimation('eliminarEnPosicion', null, pos); + registrarClick( + 'Eliminar en posición', + `Eliminar elemento en posición ${pos}` + ); setDeletePosition(''); }; @@ -359,6 +384,7 @@ export const SimpleListControls = () => { } executeAnimation('buscar', numValue); + registrarClick('Buscar', `Buscar ${numValue} en la lista`); setSearchValue(''); }; diff --git a/src/animations/stack/components/StackControls.jsx b/src/animations/stack/components/StackControls.jsx index 0bedfbc..530f5a6 100644 --- a/src/animations/stack/components/StackControls.jsx +++ b/src/animations/stack/components/StackControls.jsx @@ -13,6 +13,7 @@ import { 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, @@ -211,6 +212,7 @@ export const StackControls = () => { } executeAnimation('push', numValue); + registrarClick('Push', `Insertar ${numValue} en la pila`); setValue(''); }; @@ -222,6 +224,7 @@ export const StackControls = () => { const topValue = stackState.head.getValue(); executeAnimation('pop', topValue); + registrarClick('Pop', `Eliminar ${topValue} de la pila`); }; const handleSumergirButton = () => { @@ -237,6 +240,7 @@ export const StackControls = () => { const topValue = stackState.head.getValue(); executeAnimation('sumergir', topValue); + registrarClick('Sumergir', `Sumergir ${topValue} en la pila`); }; const handleInsertarButton = () => { @@ -270,6 +274,7 @@ export const StackControls = () => { } executeAnimation('insertar', numValue, position); + registrarClick('Insertar', `Insertar ${numValue} en posición ${position}`); setValue(''); setInsertPosition(''); }; @@ -296,6 +301,7 @@ export const StackControls = () => { } executeAnimation('extraer', null, position); + registrarClick('Extraer', `Extraer elemento en posición ${position}`); setExtractPosition(''); }; diff --git a/src/animations/views/AnimationView.jsx b/src/animations/views/AnimationView.jsx index 694a469..7a7a1e1 100644 --- a/src/animations/views/AnimationView.jsx +++ b/src/animations/views/AnimationView.jsx @@ -44,12 +44,14 @@ import { 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 = diff --git a/src/components/Auth/LoginButton.jsx b/src/components/Auth/LoginButton.jsx new file mode 100644 index 0000000..223cc77 --- /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/HomeAprende.jsx b/src/components/HomeAprende.jsx index e62fe2c..49a8049 100644 --- a/src/components/HomeAprende.jsx +++ b/src/components/HomeAprende.jsx @@ -5,10 +5,12 @@ import HomeAprendeTabla from './HomeAprendeTabla'; import { useDispatch } from 'react-redux'; import { resetStateForQuiz } from '../store'; import { useTranslation } from 'react-i18next'; +import usePageActivity from '../hooks/usePageActivity'; export const HomeAprende = () => { const dispatch = useDispatch(); const { t } = useTranslation(); + usePageActivity('pagina_visitada', 'inicio'); useEffect(() => { dispatch(resetStateForQuiz()); diff --git a/src/components/MobileMenu.jsx b/src/components/MobileMenu.jsx index 5f716df..59655f6 100644 --- a/src/components/MobileMenu.jsx +++ b/src/components/MobileMenu.jsx @@ -1,11 +1,15 @@ import PropTypes from 'prop-types'; -import UserLogo from '../assets/user.png'; import { svgClose } from '../assets/svg/SvgConstans'; import { NavTab } from './NavTab'; import { useTranslation } from 'react-i18next'; +import { Box, CircularProgress } from '@mui/material'; +import useAuth from '../hooks/useAuth'; +import LoginButton from './Auth/LoginButton'; +import UserProfile from './Auth/UserProfile'; export const MobileMenu = ({ isHamburgerMenuVisible, hideHamburgerMenu }) => { const { t } = useTranslation(); + const { user, loading } = useAuth(); return (
    { linkTo="/mini-juegos" onClick={hideHamburgerMenu} /> - + + {/* Auth area — mobile */} + + {loading ? ( + + ) : user ? ( + + ) : ( + + )} + + {/* Auth area — desktop */} +
    + {loading ? ( + + ) : user ? ( + + ) : ( + + )} +
    {error && ( diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx index 687885d..68b7148 100644 --- a/src/components/Footer.jsx +++ b/src/components/Footer.jsx @@ -1,7 +1,7 @@ export const Footer = () => { return ( ); };