diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..3792e412d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "workbench.colorCustomizations": { + "activityBar.activeBackground": "#93e6fc", + "activityBar.background": "#93e6fc", + "activityBar.foreground": "#15202b", + "activityBar.inactiveForeground": "#15202b99", + "activityBarBadge.background": "#fa45d4", + "activityBarBadge.foreground": "#15202b", + "commandCenter.border": "#15202b99", + "sash.hoverBorder": "#93e6fc", + "statusBar.background": "#61dafb", + "statusBar.foreground": "#15202b", + "statusBarItem.hoverBackground": "#2fcefa", + "statusBarItem.remoteBackground": "#61dafb", + "statusBarItem.remoteForeground": "#15202b", + "titleBar.activeBackground": "#61dafb", + "titleBar.activeForeground": "#15202b", + "titleBar.inactiveBackground": "#61dafb99", + "titleBar.inactiveForeground": "#15202b99" + }, + "peacock.color": "#61dafb" +} \ No newline at end of file diff --git a/package.json b/package.json index bba447542..d0831f133 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "Sebastián González Riffo " ], "scripts": { - "dev": "vite", + "dev": "VITE_APP_MODE=playground vite", + "docu": "vite", "build": "tsc && vite build && npm run build:types && npm run copy-dts", "build:types": "tsc -p tsconfig.types.json", "copy-dts": "copyfiles -u 1 \"src/**/*.d.ts\" dist", diff --git a/src/documentation/components/Organisms/ModalAlertDoc/ModalAlertDoc.tsx b/src/documentation/components/Organisms/ModalAlertDoc/ModalAlertDoc.tsx index 4301d8e4d..b2577c58d 100644 --- a/src/documentation/components/Organisms/ModalAlertDoc/ModalAlertDoc.tsx +++ b/src/documentation/components/Organisms/ModalAlertDoc/ModalAlertDoc.tsx @@ -1,69 +1,16 @@ -import { useState } from 'react' -import { ModalAlert } from '@/organisms' -import { BtnPrimary } from '@/molecules' -import { ModalAlertProps } from '@/organisms/ModalAlert/types' +// import { useState } from 'react' +// // import { ModalAlert } from '@/organisms' +// import { BtnPrimary } from '@/molecules' +// import { ModalAlertProps } from '@/organisms/ModalAlert/types' export function ModalAlertDoc(): JSX.Element { - const [isOpen, setIsOpen] = useState(false) - const [isOpenInfo, setIsOpenInfo] = useState(false) - const optionsModal: ModalAlertProps = { - showModal: isOpen, - typeAlert: 'success', - title: '¿Seguro que deseas borrar esta pregunta?', - description: 'Por favor escoge otro horario.', - optionsButton: [ - { - id: '1', - label: 'Opcion 1', - action: () => { - setIsOpen(false) - }, - }, - { - id: '2', - label: 'Opcion 2', - action: () => { - setIsOpen(false) - }, - }, - ], - } - - const optionsModaInfol: ModalAlertProps = { - showModal: isOpenInfo, - typeAlert: 'info', - title: 'Información sobre tu nota', - description: ( - <> - La razón por la que tu nota bajó en la evaluación podría ser que no entendiste completamente - el tema. Esto sucede porque si no comprendes bien el contenido, es difícil responder - correctamente a las preguntas. Es importante dedicar tiempo a estudiar y practicar para - asegurarte de tener un buen entendimiento del tema antes de la evaluación.
-
- La razón para la disminución de tu nota podría ser que se detectó que copiaste en tus - respuestas. Copiar no solo es deshonesto, sino que también muestra una falta de comprensión - y habilidad para resolver problemas por tu cuenta. Es importante recordar que la integridad - académica es fundamental y que es mejor esforzarse por entender y resolver los problemas por - uno mismo, incluso si eso significa obtener una nota más baja en el corto plazo. - - ), - optionsButton: [ - { - id: '1', - label: 'Entendido', - action: () => { - setIsOpenInfo(false) - }, - }, - ], - } return ( <> - setIsOpen(true)}>Ejemplo modal -
+

Ejemplo modal

+ {/*
setIsOpenInfo(true)}>Ejemplo modal Info - + */} ) } diff --git a/src/documentation/components/Organisms/Modals.tsx b/src/documentation/components/Organisms/Modals.tsx new file mode 100644 index 000000000..59f147c72 --- /dev/null +++ b/src/documentation/components/Organisms/Modals.tsx @@ -0,0 +1,126 @@ +import { useDisclosure } from '@chakra-ui/react' +import { BtnPrimary } from '@/molecules' +import { Modal, ModalAlertNew } from '@/organisms' + +export const ModalDemo = ({ + type, +}: { + type?: + | 'withoutButtons' + | 'buttonsCenter' + | 'buttonsColumn' + | 'buttonsInside' + | 'fixedSubtitle' + | 'withoutMargin' + | 'closeOnOverlayClick' +}): JSX.Element => { + const { isOpen, onOpen, onClose } = useDisclosure() + + const name = { + default: 'Open Modal', + withoutButtons: 'Sin botones (withoutButtons)', + buttonsCenter: 'Con boton centrado (buttonsCenter)', + buttonsColumn: 'Botones en mobile no pasan a columas (buttonsColumn)', + buttonsInside: 'Botones dentro del contenido (buttonsInside)', + fixedSubtitle: 'Subtitulo fijo (fixedSubtitle="")', + withoutMargin: 'Sin margin (withoutMargin)', + closeOnOverlayClick: 'Cerrar solo en botones (closeOnOverlayClick)', + } + + return ( + <> + {name[type ?? 'default']} + onClose(), text: 'Guardar' }] + : [ + { action: () => onClose(), text: 'Guardar' }, + { type: 'secondary', action: () => onClose(), text: 'Cancelar' }, + ] + } + > +

+ alumnos, además de definir el uso de la plataforma de estudio. 1) El material del programa + de estudio estará disponible en una plataforma tecnológica a la que cada alumno podrá + acceder en la siguiente dirección: http://cursos.eclass.cl 2) Las credenciales de acceso + que recibe el alumno para ingresar a su programa de estudio, son personales e + intransferibles. 3) Al aceptar estos términos y condiciones, el alumno se compromete a + realizar el programa en el que se encuentra inscrito y a revisar los siguientes + documentos: Reglamento Académico, Manual del Alumno e Información del Programa.Estos + documentos estarán disponibles en la plataforma de estudio, y contienen toda la + información académica del programa. 4) El alumno se compromete a utilizar los materiales y + medios tecnológicos de eClass exclusivamente con fines educativos en el marco del programa + de estudio. Se deja constancia que la alumnos, además de definir el uso de la plataforma + de estudio. 5) El material del programa de estudio estará disponible en una plataforma + tecnológica a la que cada alumno podrá acceder en la siguiente dirección: + http://cursos.eclass.cl 2) Las credenciales de acceso que recibe el alumno para ingresar a + su programa de estudio, son personales e intransferibles. 3) Al aceptar estos términos y + condiciones, el alumno se compromete a realizar el programa en el que se encuentra + inscrito y a revisar los siguientes documentos: Reglamento Académico, Manual del Alumno e + Información del Programa.Estos documentos estarán disponibles en la plataforma de estudio, + y contienen toda la información académica del programa. 4) El alumno se compromete a + utilizar los materiales y medios tecnológicos de eClass exclusivamente con fines + educativos en el marco del programa de estudio. Se deja constancia que la +

+
+ + ) +} + +export const ModalAlertDemo = ({ + button, + type, + status, + withoutTitle, + withoutDescription, +}: { + button?: string + type?: 'loading' | 'info' + status?: 'success' | 'error' | 'warning' | 'info' + withoutTitle?: boolean + withoutDescription?: boolean +}): JSX.Element => { + const { isOpen, onOpen, onClose } = useDisclosure() + + const text = { + info: { + button: 'ModalAlertNew', + title: '¿Seguro que deseas borrar esta pregunta?', + description: 'Por favor escoge otro horario.', + }, + loading: { + button: 'ModalAlertNew Loading', + title: 'Descargando documento', + description: 'Se está preparando el archivo. Esto podría tardar un momento.', + }, + } + return ( + <> + {status ?? button} + onClose(), text: 'Opciones más largas' }, + { action: () => onClose(), text: 'Cancelar' }, + ]} + /> + + ) +} diff --git a/src/documentation/pages/Home.tsx b/src/documentation/pages/Home.tsx index 85ce7b5b2..54b32a40b 100644 --- a/src/documentation/pages/Home.tsx +++ b/src/documentation/pages/Home.tsx @@ -1,4 +1,6 @@ import { MyHeading, MyText, MyTitle, Code } from '../components' +import { Box } from '@chakra-ui/react' +import { CalendarDropdown } from '@/organisms' export const Home = (): JSX.Element => { return ( @@ -19,6 +21,38 @@ export const Home = (): JSX.Element => { todo lo que se promete. + + {}} + text={{ + empty: 'hola', + header: 'hola', + tooltip: 'hola', + course: 'hola', + events: { + today: 'hola', + tomorrow: 'hola', + next: 'hola', + }, + seeMore: { + see: 'hola', + link: 'hola', + }, + loading: 'hola', + buttonCalendar: 'hola', + }} + /> + + Para trabajar de manera local Ejecuta los test con jest @@ -27,4 +61,199 @@ export const Home = (): JSX.Element => { ) } +export const dataFake = [ + { + id: 8874, + student_id: 17347601, + course_id: 44195, + class_id: 6, + status: 'SCHEDULED', + start: '2025-03-04T03:01:00.000Z', + end: '2025-03-04T04:01:00.000Z', + duration_in_minutes: 60, + type: 'evaluation-release', + associated_resource: { + id: 6, + name: 'Liberación Evaluación sumativa 2', + description: 'Unidad 2.3: Valores ciudadanos en la era digital', + }, + course: { + id: 44195, + name: 'Formación Ciudadana_FOFC03 (TESTING TECNOLOGIA NO USAR)', + }, + formatedDate: { + start: 'mar 4 mar', + hours: '0:01 hrs.', + }, + }, + { + id: 8882, + student_id: 17347601, + course_id: 44195, + class_id: 6, + status: 'SCHEDULED', + start: '2025-03-04T03:01:00.000Z', + end: '2025-03-04T04:01:00.000Z', + duration_in_minutes: 60, + type: 'evaluation-release', + associated_resource: { + id: 6, + name: 'Liberación Evaluación acumulativa 6', + description: 'Unidad 2.3: Valores ciudadanos en la era digital', + }, + course: { + id: 44195, + name: 'Formación Ciudadana_FOFC03 (TESTING TECNOLOGIA NO USAR)', + }, + formatedDate: { + start: 'mar 4 mar', + hours: '0:01 hrs.', + }, + }, + { + id: 29304, + student_id: 17347668, + course_id: 44196, + class_id: 6, + status: 'SCHEDULED', + start: '2025-03-07T03:01:00.000Z', + end: '2025-03-07T04:01:00.000Z', + duration_in_minutes: 60, + type: 'work-release', + associated_resource: { + id: 6, + name: 'Liberación Trabajo Final', + description: 'Evaluación Final', + }, + course: { + id: 44196, + name: 'Creatividad en los negocios_AOMO01(TESTING TECNOLOGIA NO USAR)', + }, + formatedDate: { + start: 'vie 7 mar', + hours: '0:01 hrs.', + }, + }, + { + id: 1526, + student_id: 17347669, + course_id: 44197, + class_id: 832159, + status: 'SCHEDULED', + start: '2025-03-07T13:00:00.000Z', + end: '2025-03-07T13:40:00.000Z', + duration_in_minutes: 40, + type: 'in-person', + associated_resource: { + id: 832159, + name: 'Ayudantía 6 NO USAR', + description: null, + }, + course: { + id: 44197, + name: 'Contabilidad_AOC404 (TESTING TECNOLOGIA NO USAR)', + }, + formatedDate: { + start: 'vie 7 mar', + hours: '10:00 hrs.', + }, + }, + { + id: 1527, + student_id: 17347669, + course_id: 44197, + class_id: 832160, + status: 'SCHEDULED', + start: '2025-03-07T18:00:00.000Z', + end: '2025-03-07T18:40:00.000Z', + duration_in_minutes: 40, + type: 'in-person', + associated_resource: { + id: 832160, + name: 'Ayudantía 7 ESTA NO', + description: null, + }, + course: { + id: 44197, + name: 'Contabilidad_AOC404 (TESTING TECNOLOGIA NO USAR)', + }, + formatedDate: { + start: 'vie 7 mar', + hours: '15:00 hrs.', + }, + }, + { + id: 29329, + student_id: 17347601, + course_id: 44195, + class_id: 6, + status: 'SCHEDULED', + start: '2025-03-08T02:59:00.000Z', + end: '2025-03-08T03:59:00.000Z', + duration_in_minutes: 60, + type: 'work-delivery-deadline', + associated_resource: { + id: 6, + name: 'Fin plazo para entregar Evaluación sumativa 1', + description: 'Unidad 1.3: Diálogo para una ciudadanía crítica y responsable', + }, + course: { + id: 44195, + name: 'Formación Ciudadana_FOFC03 (TESTING TECNOLOGIA NO USAR)', + }, + formatedDate: { + start: 'vie 7 mar', + hours: '23:59 hrs.', + }, + }, + { + id: 16643, + student_id: 17348364, + course_id: 22487, + class_id: 1, + status: 'SCHEDULED', + start: '2026-01-01T02:59:00.000Z', + end: '2026-01-01T03:59:00.000Z', + duration_in_minutes: 60, + type: 'answers-schedule-deadline', + associated_resource: { + id: 1, + name: 'Fin plazo para responder Pregunta tipo archivo 2', + description: 'Unidad 1', + }, + course: { + id: 22487, + name: 'Coaching en la Era Digital', + }, + formatedDate: { + start: 'mié 31 dic', + hours: '23:59 hrs.', + }, + }, + { + id: 19448, + student_id: 17242931, + course_id: 40711, + class_id: 1, + status: 'SCHEDULED', + start: '2026-01-01T02:59:00.000Z', + end: '2026-01-01T03:59:00.000Z', + duration_in_minutes: 60, + type: 'answers-schedule-deadline', + associated_resource: { + id: 1, + name: 'Fin plazo para responder Control Mayo 5', + description: 'Pruebas Cloze Inacap', + }, + course: { + id: 40711, + name: '[Pruebas TI] - Herramientas para la Gestión Estratégica de Procesos', + }, + formatedDate: { + start: 'mié 31 dic', + hours: '23:59 hrs.', + }, + }, +] + export default Home diff --git a/src/documentation/pages/Organisms/Modals.tsx b/src/documentation/pages/Organisms/Modals.tsx new file mode 100644 index 000000000..feab43a8e --- /dev/null +++ b/src/documentation/pages/Organisms/Modals.tsx @@ -0,0 +1,136 @@ +import { MyHeading, MyText, MyTittle, Code, ListComponent } from '@/documentation/components' +import { ModalAlertDemo, ModalDemo } from '@/documentation/components/Organisms/Modals' + +export const ViewModals = (): JSX.Element => { + return ( + <> + Modales + + Para los modales, tenemos dos tipos de componentes: Modal y ModalAlert. Cada uno tiene sus{' '} + variantes que definen su apariencia y funcionalidad, y también tienen + tamaños y paddings predefinidos. + + + + + + + Tipo Modal + + Es el Modal tradicional para mostrar contenido, que incluye una cabecera, contenido y una + botonera con acciones. + + El componente se importa de la siguiente manera: + + + + + + + onClose(), text: 'Guardar' }, + { type: 'secondary', action: () => onClose(), text: 'Cancelar' }, + ]} + > +

Contenido del modal...

+
+ ) +}`} + /> + Variantes del tipo Modal + + Variantes del modal dependiendo de sus props. El valor de los props va + definido en sus tipos. + + + + + + + + + + + + Tipo ModalAlert + + Es el modal que se utiliza a modo de alerta para el usuario, el cual posee información + reducida y también puede contar con botones. Este componente tiene dos visualizaciones + principales. + + El componente se importa de la siguiente manera: + + + + + + + + onClose(), text: 'Guardar' }, + { type: 'secondary', action: () => onClose(), text: 'Cancelar' }, + ]} + > +

Contenido de la alerta...

+
+ ) +}`} + /> + Variantes del tipo ModalAlert + + Las variantes del modal son dos, como ejemplificamos más arriba. Sin embargo, cada una + también puede variar; por ejemplo, se pueden omitir los textos pasados como + props, y en el ModalAlert normal el icono varía según los estados definidos. + + + + + + + + + + + + + ) +} + +export default ViewModals diff --git a/src/documentation/utils/routes.tsx b/src/documentation/utils/routes.tsx index 189fb46b2..c227301c8 100644 --- a/src/documentation/utils/routes.tsx +++ b/src/documentation/utils/routes.tsx @@ -29,9 +29,10 @@ const FlashNotification = React.lazy( ) const CalendarDropdown = React.lazy(async () => await import('../pages/Organisms/CalendarDropdown')) const CourseList = React.lazy(async () => await import('../pages/Organisms/CourseList')) +const ModalAlert = React.lazy(async () => await import('../pages/Organisms/ModalAlert')) +const Modals = React.lazy(async () => await import('../pages/Organisms/Modals')) const Events = React.lazy(async () => await import('../pages/Organisms/Events')) const EventsList = React.lazy(async () => await import('../pages/Organisms/EventsList')) -const ModalAlert = React.lazy(async () => await import('../pages/Organisms/ModalAlert')) const Resources = React.lazy(async () => await import('../pages/Organisms/Resources')) /** @@ -159,6 +160,16 @@ export const routes: IRoute[] = [ label: 'Course List', component: , }, + { + path: '/organisms/modalalert', + label: 'ModalAlert', + component: , + }, + { + path: '/organisms/modals', + label: 'Modals', + component: , + }, { path: '/organisms/events', label: 'Events', diff --git a/src/main.tsx b/src/main.tsx index f5b1c6d74..9c5bbeb7b 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,16 +3,17 @@ import * as ReactDOM from 'react-dom' import { ChakraProvider } from '@chakra-ui/react' import { Documentation } from './documentation' -// import { Playground } from './Playground' +import { Playground } from './Playground' import { theme } from './theme' +const isPlaygroundMode = import.meta.env.VITE_APP_MODE === 'playground' + if (import.meta.env.VITE_REACT_DEPLOY_DOCUMENTATION === 'DOCUMENTATION') { ReactDOM.render( - {/* */} - + {isPlaygroundMode ? : } , document.getElementById('root') diff --git a/src/molecules/Buttons/Btn.tsx b/src/molecules/Buttons/Btn.tsx index 2e72a95cf..e51fb9a68 100644 --- a/src/molecules/Buttons/Btn.tsx +++ b/src/molecules/Buttons/Btn.tsx @@ -80,6 +80,7 @@ export function Btn({ { + const originalModule = jest.requireActual('@chakra-ui/react') + return { + ...originalModule, + useMediaQuery: jest.fn(() => [true]), // Por defecto, simula un entorno de escritorio + } +}) + +const renderWithChakra = (ui: React.ReactElement): any => { + return render({ui}) +} + +// Props por defecto para reducir repetición +const defaultProps = { + isOpen: true, + onClose: jest.fn(), + title: 'Test Title', + children:
Test Content
, +} + +describe('Modal Component', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('Rendering', () => { + it('renders with title and children content', () => { + renderWithChakra() + + expect(screen.getByText('Test Title')).toBeInTheDocument() + expect(screen.getByText('Test Content')).toBeInTheDocument() + expect(screen.getByRole('dialog')).toBeInTheDocument() + }) + + it('renders fixedSubtitle when provided', () => { + renderWithChakra() + + expect(screen.getByText('Fixed Subtitle')).toBeInTheDocument() + }) + + it('does not render when isOpen is false', () => { + renderWithChakra() + + expect(screen.queryByText('Test Title')).not.toBeInTheDocument() + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) + + it('renders multiple buttons with correct types', () => { + const buttons = [ + { text: 'Primary Button', action: jest.fn(), type: 'primary' as const }, + { text: 'Secondary Button', action: jest.fn(), type: 'secondary' as const }, + { text: 'Button Without Type', action: jest.fn() }, // type es opcional + ] + + renderWithChakra() + + buttons.forEach((button) => { + expect(screen.getByText(button.text)).toBeInTheDocument() + }) + }) + + it('does not render footer when no buttons are provided', () => { + renderWithChakra() + + expect(screen.queryByRole('contentinfo')).not.toBeInTheDocument() + }) + }) + + describe('Interaction', () => { + it('calls onClose when close button is clicked', async () => { + const user = userEvent.setup() + const onCloseMock = jest.fn() + + renderWithChakra() + + await user.click(screen.getByLabelText('Close')) + expect(onCloseMock).toHaveBeenCalledTimes(1) + }) + + it('calls onClose when overlay is clicked by default', async () => { + const user = userEvent.setup() + const onCloseMock = jest.fn() + + renderWithChakra() + + await user.click(screen.getByRole('button', { name: 'Close' })) + expect(onCloseMock).toHaveBeenCalledTimes(1) + }) + + it('does not call onClose when overlay is clicked if closeOnOverlayClick is false', async () => { + const user = userEvent.setup() + const onCloseMock = jest.fn() + + renderWithChakra( + + ) + + expect(screen.queryByLabelText('Close')).not.toBeInTheDocument() + + // eslint-disable-next-line testing-library/no-node-access + const modalContainer = document.querySelector('.chakra-modal__content-container') + if (modalContainer) { + await user.click(modalContainer) + } + expect(onCloseMock).not.toHaveBeenCalled() + }) + + it('calls onClose when Escape key is pressed', async () => { + const user = userEvent.setup() + const onCloseMock = jest.fn() + + renderWithChakra() + + await user.keyboard('{Escape}') + expect(onCloseMock).toHaveBeenCalledTimes(1) + }) + }) + + describe('Button Positioning', () => { + it('renders buttons inside ModalBody when buttonsInside is true', async () => { + const user = userEvent.setup() + const buttonActionMock = jest.fn() + + renderWithChakra( + + ) + + // eslint-disable-next-line testing-library/no-node-access + const modalBody = screen.getByText('Test Content').closest('.chakra-modal__body') + const button = screen.getByText('Inside Button') + + expect(modalBody).toBeInTheDocument() + expect(modalBody).toContainElement(button) + + await user.click(button) + expect(buttonActionMock).toHaveBeenCalledTimes(1) + }) + + it('renders buttons in ModalFooter when buttonsInside is false', async () => { + const user = userEvent.setup() + const buttonActionMock = jest.fn() + + renderWithChakra( + + ) + + // eslint-disable-next-line testing-library/no-node-access + const modalFooter = screen.getByText('Outside Button').closest('.chakra-modal__footer') + const button = screen.getByText('Outside Button') + + expect(modalFooter).toBeInTheDocument() + expect(modalFooter).toContainElement(button) + + await user.click(button) + expect(buttonActionMock).toHaveBeenCalledTimes(1) + }) + + it('handles empty button array', () => { + renderWithChakra() + + expect(screen.queryByRole('contentinfo')).not.toBeInTheDocument() + }) + }) + + describe('Responsive Behavior', () => { + it('applies responsive styles based on useMediaQuery', () => { + const useMediaQuery = jest.requireMock('@chakra-ui/react').useMediaQuery + useMediaQuery.mockReturnValue([false]) // Simulate mobile environment + + renderWithChakra() + + // eslint-disable-next-line testing-library/no-node-access + const modalContent = screen.getByRole('dialog').closest('.chakra-modal__content') + expect(modalContent).toHaveStyle('border-radius: 0px') + expect(modalContent).toHaveStyle('height: 100dvh') + expect(modalContent).toHaveStyle('margin: 0px') + expect(modalContent).toHaveStyle('max-width: 100%') + + useMediaQuery.mockReturnValue([true]) // Reset to desktop for other tests + }) + }) +}) diff --git a/src/organisms/Modals/Modal/Modal.tsx b/src/organisms/Modals/Modal/Modal.tsx new file mode 100644 index 000000000..5b37e125b --- /dev/null +++ b/src/organisms/Modals/Modal/Modal.tsx @@ -0,0 +1,125 @@ +import { + Box, + Modal as ChakraModal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + useMediaQuery, +} from '@chakra-ui/react' + +import { vars } from '@/theme' +import { ModalButtons } from './ModalButtons' +import { IModal } from '../types' + +export const Modal = ({ + buttons, + buttonsCenter, + buttonsColumn = true, + buttonsInside, + children, + closeOnOverlayClick = true, + fixedSubtitle, + isOpen, + onClose, + title, + withoutMargin = false, +}: IModal): JSX.Element => { + const py = '32px' + const px = '24px' + + const [isDesktop] = useMediaQuery('(min-width: 641px)') + + return ( + <> + + + + + {title} + + {closeOnOverlayClick && ( + + )} + {fixedSubtitle?.trim() && ( + + {fixedSubtitle} + + )} + {children && ( + + {children} + + {buttonsInside && buttons && buttons.length > 0 && ( + + )} + + )} + + {!buttonsInside && ( + + )} + + + + ) +} diff --git a/src/organisms/Modals/Modal/ModalButtons.test.tsx b/src/organisms/Modals/Modal/ModalButtons.test.tsx new file mode 100644 index 000000000..96518a54e --- /dev/null +++ b/src/organisms/Modals/Modal/ModalButtons.test.tsx @@ -0,0 +1,87 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { ChakraProvider, Modal as ChakraModal } from '@chakra-ui/react' + +import { ModalButtons } from './ModalButtons' + +const renderWithChakra = (ui: React.ReactElement): any => { + return render( + + {}}> + {ui} + + + ) +} + +describe('ModalButtons Component', () => { + it('renders nothing when no buttons are provided', () => { + const { container } = renderWithChakra( + + ) + expect(container).toBeEmptyDOMElement() + }) + + it('renders a primary button and calls its action on click', async () => { + const actionMock = jest.fn() + renderWithChakra( + + ) + const button = screen.getByText('Primary Button') + expect(button).toBeInTheDocument() + await userEvent.click(button) + expect(actionMock).toHaveBeenCalledTimes(1) + }) + + it('renders a secondary button and calls its action on click', async () => { + const actionMock = jest.fn() + renderWithChakra( + + ) + const button = screen.getByText('Secondary Button') + expect(button).toBeInTheDocument() + await userEvent.click(button) + expect(actionMock).toHaveBeenCalledTimes(1) + }) + + it('renders mixed buttons correctly', () => { + const primaryActionMock = jest.fn() + const secondaryActionMock = jest.fn() + renderWithChakra( + + ) + expect(screen.getByText('Primary')).toBeInTheDocument() + expect(screen.getByText('Secondary')).toBeInTheDocument() + }) +}) diff --git a/src/organisms/Modals/Modal/ModalButtons.tsx b/src/organisms/Modals/Modal/ModalButtons.tsx new file mode 100644 index 000000000..0ac937476 --- /dev/null +++ b/src/organisms/Modals/Modal/ModalButtons.tsx @@ -0,0 +1,46 @@ +import { ModalFooter } from '@chakra-ui/react' + +import { BtnPrimary, BtnSecondary } from '@/molecules' +import { IModalButtons } from '../types' + +export const ModalButtons = ({ + buttons, + buttonsCenter, + buttonsColumn, + buttonsInside, + isDesktop, + px, + py, +}: IModalButtons): JSX.Element => { + if (!buttons || (buttons && buttons.length === 0)) { + return <> + } + + const buttonFull = !isDesktop && buttonsColumn + + return ( + + {buttons.map((button, index) => { + if (button?.type === 'secondary') { + return ( + button.action()} isFullWidth={buttonFull}> + {button.text} + + ) + } + return ( + button.action()} isFullWidth={buttonFull}> + {button.text} + + ) + })} + + ) +} diff --git a/src/organisms/Modals/ModalAlert/Loading.tsx b/src/organisms/Modals/ModalAlert/Loading.tsx new file mode 100644 index 000000000..107cffc53 --- /dev/null +++ b/src/organisms/Modals/ModalAlert/Loading.tsx @@ -0,0 +1,53 @@ +import { vars } from '@/theme' + +export const Loading = ({ + fill = vars('colors-main-deepSkyBlue'), +}: { + fill?: string +}): JSX.Element => { + return ( + + + + + + + ) +} diff --git a/src/organisms/Modals/ModalAlert/ModalAlert.test.tsx b/src/organisms/Modals/ModalAlert/ModalAlert.test.tsx new file mode 100644 index 000000000..9070c5f05 --- /dev/null +++ b/src/organisms/Modals/ModalAlert/ModalAlert.test.tsx @@ -0,0 +1,72 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { ChakraProvider } from '@chakra-ui/react' + +import { ModalAlertNew } from './ModalAlert' + +// Mock de useMediaQuery para controlar el comportamiento responsivo en los tests +jest.mock('@chakra-ui/react', () => { + const originalModule = jest.requireActual('@chakra-ui/react') + return { + ...originalModule, + useMediaQuery: jest.fn(() => [true]), // Por defecto, simula un entorno de escritorio + } +}) + +const renderWithChakra = (ui: React.ReactElement): any => { + return render({ui}) +} + +describe('ModalAlertNew Component', () => { + it('renders with title and description', () => { + renderWithChakra( + {}} + title="Test Title" + description="Test Description" + type="info" + status="info" + /> + ) + expect(screen.getByText('Test Title')).toBeInTheDocument() + expect(screen.getByText('Test Description')).toBeInTheDocument() + }) + + it('renders loading state', () => { + renderWithChakra( {}} type="loading" status="info" />) + expect(screen.getByTestId('loading-svg')).toBeInTheDocument() // Assuming Loading component has data-testid="loading-svg" + }) + + it('renders buttons and calls action on click', async () => { + const actionMock = jest.fn() + renderWithChakra( + {}} + title="Test Title" + type="info" + status="info" + buttons={[{ text: 'Action Button', action: actionMock }]} + /> + ) + const button = screen.getByText('Action Button') + expect(button).toBeInTheDocument() + await userEvent.click(button) + expect(actionMock).toHaveBeenCalledTimes(1) + }) + + it('calls onClose when overlay is clicked', async () => { + const onCloseMock = jest.fn() + renderWithChakra( + + ) + + // eslint-disable-next-line testing-library/no-node-access + const modalContainer = document.querySelector('.chakra-modal__content-container') + + expect(modalContainer).toBeInTheDocument() + await userEvent.click(modalContainer) + expect(onCloseMock).not.toHaveBeenCalled() + }) +}) diff --git a/src/organisms/Modals/ModalAlert/ModalAlert.tsx b/src/organisms/Modals/ModalAlert/ModalAlert.tsx new file mode 100644 index 000000000..0da686e2e --- /dev/null +++ b/src/organisms/Modals/ModalAlert/ModalAlert.tsx @@ -0,0 +1,102 @@ +import { + Box, + Modal as ChakraModal, + ModalBody, + ModalContent, + ModalOverlay, + useMediaQuery, +} from '@chakra-ui/react' + +import { IModalAlert } from '../types' +import { BtnLink } from '@/molecules' +import { Loading } from './Loading' +import { alertColorStates } from '@/organisms/Alerts/utils/alertStates' +import { vars } from '@/theme' + +export const ModalAlertNew = ({ + type, + isOpen, + onClose, + title, + description, + buttons, + status, +}: IModalAlert): JSX.Element => { + const [isDesktop] = useMediaQuery('(min-width: 641px)') + + return ( + <> + + + + + {type === 'loading' ? ( + + ) : ( + + {alertColorStates[status ?? 'info'].icon} + + )} + {title && ( + + {title} + + )} + {description && ( + + {description} + + )} + + {type === 'info' && buttons?.length && ( + + {buttons.map((button, index) => ( + button.action()}> + {button.text} + + ))} + + )} + + + + ) +} diff --git a/src/organisms/Modals/index.ts b/src/organisms/Modals/index.ts new file mode 100644 index 000000000..1f931d99b --- /dev/null +++ b/src/organisms/Modals/index.ts @@ -0,0 +1,2 @@ +export { Modal } from './Modal/Modal' +export { ModalAlertNew } from './ModalAlert/ModalAlert' diff --git a/src/organisms/Modals/resetStyleModal.ts b/src/organisms/Modals/resetStyleModal.ts new file mode 100644 index 000000000..088ed6ba3 --- /dev/null +++ b/src/organisms/Modals/resetStyleModal.ts @@ -0,0 +1,11 @@ +import { radii, shadows } from '@theme/utils' +import { colors } from '@theme/colors' + +export const overlay = { + background: colors.neutral.darkCharcoal, + opacity: '.3!important', +} +export const dialog = { + boxShadow: shadows.lg, + borderRadius: radii.big, +} diff --git a/src/organisms/Modals/types.d.ts b/src/organisms/Modals/types.d.ts new file mode 100644 index 000000000..b950c8c42 --- /dev/null +++ b/src/organisms/Modals/types.d.ts @@ -0,0 +1,39 @@ +export interface IModal { + buttons?: Array<{ type?: 'primary' | 'secondary'; action: () => void; text: string }> + /** Si es un solo botón tiene la opción de centrar este */ + buttonsCenter?: boolean + /** muestran en columna en mobile, por defecto es true */ + buttonsColumn?: boolean + /** Si esta activo los botones se muestran dentro del contenido, le afecta el scroll */ + buttonsInside?: boolean + children: React.ReactNode + /** Por defecto esta activo y permite cerrar el modal haciendo click fuera del modal */ + closeOnOverlayClick?: boolean + /** Si esta activo el subtitulo se fija en la parte superior */ + fixedSubtitle?: string + isOpen: boolean + onClose: () => void + title?: string + /** Si esta activo se quita el margin del contenido */ + withoutMargin?: boolean +} + +export interface IModalButtons { + buttons: IModal['buttons'] + buttonsCenter?: IModal['buttonsCenter'] + buttonsColumn?: IModal['buttonsColumn'] + buttonsInside?: boolean + isDesktop?: boolean + px: string + py: string +} + +export interface IModalAlert { + isOpen: boolean + onClose: () => void + title?: string + description?: string + type: 'info' | 'loading' + status?: 'success' | 'error' | 'warning' | 'info' + buttons?: Array<{ action: () => void; text: string }> +} diff --git a/src/organisms/index.ts b/src/organisms/index.ts index 2875fdb1a..791aeca5b 100644 --- a/src/organisms/index.ts +++ b/src/organisms/index.ts @@ -1,6 +1,7 @@ export * from './CourseList' export * from './Alerts' -export * from './ModalAlert' +// export * from './ModalAlert' export * from './Events' export * from './Resources' export * from './Calendar' +export * from './Modals' diff --git a/src/theme/index.ts b/src/theme/index.ts index e51323807..9832c06c5 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -4,11 +4,20 @@ import { colors } from './colors' import { styles } from './styles' import { utils } from './utils' import { typography } from './typography' +import { dialog, overlay } from '@/organisms/Modals/resetStyleModal' export const theme = extendTheme({ colors, ...utils, ...typography, + components: { + Modal: { + baseStyle: { + overlay, + dialog, + }, + }, + }, styles: styles, }) diff --git a/src/theme/styles.ts b/src/theme/styles.ts index d947c93cc..614939a85 100644 --- a/src/theme/styles.ts +++ b/src/theme/styles.ts @@ -6,45 +6,5 @@ export const styles = { h: '100%', display: 'grid!important', }, - '.chakra-button': { - borderStyle: 'none', - }, - '.chakra-modal__content-container': { - padding: '1.87rem', - zIndex: '1400', - justifyContent: 'center', - alignItems: 'center', - overflow: 'auto', - }, - '.chakra-modal__content': { - borderRadius: '0.5rem', - background: 'white', - color: 'inherit', - zIndex: '1400', - maxWidth: '36.813rem', - boxShadow: '0px 4px 16px rgba(92, 92, 92, 0.2)', - }, - '.chakra-modal__header': { - paddingInlineStart: '1.5rem', - paddingInlineEnd: '1.5rem', - fontSize: '1.25rem', - fontWeight: '700', - textAlign: 'center', - '.chakra-icon': { - width: '13rem', - }, - }, - '.chakra-modal__close-btn': { - width: '2rem', - height: '2rem', - borderRadius: '0.5rem', - fontSize: '0.75rem', - position: 'absolute', - top: '0.5rem', - right: '0.75rem', - color: '#60798E', - backgroundColor: 'transparent', - borderStyle: 'none', - }, }), } diff --git a/tsconfig.json b/tsconfig.json index 57c4bf99e..9c763e568 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,14 +4,14 @@ "lib": ["DOM", "DOM.Iterable", "ESNext"], "allowJs": false, "skipLibCheck": true, - "esModuleInterop": false, + "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "strict": true, + "strict": false, "forceConsistentCasingInFileNames": true, - "module": "ESNext", + "module": "CommonJS", "moduleResolution": "Node", "resolveJsonModule": true, - "isolatedModules": false, + "isolatedModules": true, "incremental": true, "jsx": "react-jsx", "importHelpers": true, @@ -34,6 +34,6 @@ "outDir": "dist", "noEmit": false }, - "include": ["src", "test", "vite.config.ts"], + "include": ["src/**/*", "test/**/*", "vite.config.ts"], "exclude": ["jest.config.js", ".eslintrc.js", "prepare.js"] }