diff --git a/package-lock.json b/package-lock.json
index 8cfd5fa..5a23802 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,7 +11,6 @@
"@hookform/resolvers": "3.3.2",
"@reduxjs/toolkit": "1.9.7",
"axios": "1.6.2",
- "axios-mock-adapter": "^1.22.0",
"classnames": "2.5.1",
"i18next": "23.6.0",
"i18next-browser-languagedetector": "7.1.0",
@@ -27,8 +26,8 @@
},
"devDependencies": {
"@tailwindcss/typography": "0.5.10",
- "@testing-library/jest-dom": "5.16.5",
- "@testing-library/react": "14.2.2",
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^14.2.2",
"@types/js-cookie": "3.0.6",
"@types/react": "18.2.15",
"@types/react-dom": "18.2.7",
@@ -124,9 +123,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.22.20",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
- "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+ "version": "7.24.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
+ "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -210,9 +209,9 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.24.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz",
- "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==",
+ "version": "7.24.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz",
+ "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -233,13 +232,13 @@
}
},
"node_modules/@babel/types": {
- "version": "7.24.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
- "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
+ "version": "7.24.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz",
+ "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==",
"dev": true,
"dependencies": {
- "@babel/helper-string-parser": "^7.23.4",
- "@babel/helper-validator-identifier": "^7.22.20",
+ "@babel/helper-string-parser": "^7.24.1",
+ "@babel/helper-validator-identifier": "^7.24.5",
"to-fast-properties": "^2.0.0"
},
"engines": {
diff --git a/package.json b/package.json
index ac0d66b..88e089d 100644
--- a/package.json
+++ b/package.json
@@ -14,7 +14,6 @@
"@hookform/resolvers": "3.3.2",
"@reduxjs/toolkit": "1.9.7",
"axios": "1.6.2",
- "axios-mock-adapter": "^1.22.0",
"classnames": "2.5.1",
"i18next": "23.6.0",
"i18next-browser-languagedetector": "7.1.0",
@@ -30,8 +29,8 @@
},
"devDependencies": {
"@tailwindcss/typography": "0.5.10",
- "@testing-library/jest-dom": "5.16.5",
- "@testing-library/react": "14.2.2",
+ "@testing-library/jest-dom": "^5.16.5",
+ "@testing-library/react": "^14.2.2",
"@types/js-cookie": "3.0.6",
"@types/react": "18.2.15",
"@types/react-dom": "18.2.7",
diff --git a/src/__tests__/components/studentFilters/ApiGetUserDetail.test.tsx b/src/__tests__/components/studentFilters/ApiGetUserDetail.test.tsx
new file mode 100644
index 0000000..225ac52
--- /dev/null
+++ b/src/__tests__/components/studentFilters/ApiGetUserDetail.test.tsx
@@ -0,0 +1,95 @@
+import React, { useContext } from 'react'
+import axios from 'axios'
+import { fireEvent, render } from '@testing-library/react'
+// eslint-disable-next-line import/no-extraneous-dependencies
+import MockAdapter from 'axios-mock-adapter'
+import { FetchStudentsListHome } from '../../../api/FetchStudentsList'
+import {
+ StudentFiltersProvider,
+ StudentFiltersContext,
+} from '../../../context/StudentFiltersContext'
+import StudentFiltersContent from '../../../components/studentFilters/StudentFiltersContent'
+
+const mockAxios = new MockAdapter(axios)
+
+describe('FetchStudentsListHome function', () => {
+ afterEach(() => {
+ mockAxios.reset()
+ })
+
+ it('should fetch student list for home', async () => {
+ const selectedRoles = ['role1', 'role2']
+
+ const expectedUrl =
+ 'https://itaperfils.eurecatacademy.org/api/v1/student/list/for-home?specialization=role1,role2'
+
+ const mockData = [
+ { id: 1, name: 'Student 1' },
+ { id: 2, name: 'Student 2' },
+ ]
+
+ mockAxios.onGet(expectedUrl).reply(200, mockData)
+
+ const result = await FetchStudentsListHome(selectedRoles)
+
+ expect(result).toEqual(mockData)
+ })
+
+ it('should handle errors', async () => {
+ const selectedRoles: string[] = []
+
+ const expectedUrl =
+ 'https://itaperfils.eurecatacademy.org/api/v1/student/list/for-home'
+
+ mockAxios.onGet(expectedUrl).reply(500)
+
+ await expect(FetchStudentsListHome(selectedRoles)).rejects.toThrow()
+ })
+})
+
+describe('StudentFiltersContent component', () => {
+ it('renders StudentFiltersContent and handles user events', () => {
+ const { getByTestId } = render(
+
+
+ ,
+ )
+
+ // Verifies that the component renders correctly
+ expect(getByTestId('student-filters-content')).toBeDefined()
+ })
+
+ it('should add and remove role', () => {
+ const TestComponent = () => {
+ const { selectedRoles, addRole, removeRole } =
+ useContext(StudentFiltersContext) || {}
+
+ if (!selectedRoles || !addRole || !removeRole) {
+ throw new Error('Context is undefined')
+ }
+ return (
+
+
+
+
{selectedRoles.join(',')}
+
+ )
+ }
+
+ const { getByText, queryByText } = render(
+
+
+ ,
+ )
+
+ fireEvent.click(getByText('Add role'))
+ expect(getByText('test')).toBeInTheDocument()
+
+ fireEvent.click(getByText('Remove role'))
+ expect(queryByText('test')).not.toBeInTheDocument()
+ })
+})
diff --git a/src/__tests__/components/studentFilters/Landing.test.tsx b/src/__tests__/components/studentFilters/Landing.test.tsx
new file mode 100644
index 0000000..3325295
--- /dev/null
+++ b/src/__tests__/components/studentFilters/Landing.test.tsx
@@ -0,0 +1,24 @@
+import { render, screen } from '@testing-library/react'
+import { Provider } from 'react-redux' // Import Provider from react-redux
+import Landing from '../../../components/landing/Landing'
+
+import { store } from '../../../store/store'
+
+describe('StudentDetailsLayout', () => {
+ it('should render the studentDetailsLayout component correctly', () => {
+ const { container } = render(
+
+
+ ,
+ )
+ expect(container).toBeInTheDocument()
+ })
+
+ test('renders all the different cards', () => {
+ expect(screen.queryByTestId('MenuNavbar')).toBeDefined()
+ expect(screen.queryByTestId('UserNavbar')).toBeDefined()
+ expect(screen.queryByTestId('StudentFiltersLayout')).toBeDefined()
+ expect(screen.queryByTestId('StudentLayout')).toBeDefined()
+ expect(screen.queryByTestId('StudentDetailsLayout')).toBeDefined()
+ })
+})
diff --git a/src/__tests__/components/studentFilters/StudenFtiltersContext.test.tsx b/src/__tests__/components/studentFilters/StudenFtiltersContext.test.tsx
new file mode 100644
index 0000000..f91e89c
--- /dev/null
+++ b/src/__tests__/components/studentFilters/StudenFtiltersContext.test.tsx
@@ -0,0 +1,43 @@
+import { renderHook, act } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+import { ReactNode, useContext } from 'react'
+import {
+ StudentFiltersProvider,
+ StudentFiltersContext,
+} from '../../../context/StudentFiltersContext'
+
+describe('StudentFiltersProvider', () => {
+ it('should add role to selectedRoles', () => {
+ const wrapper = (
+ { children }: { children: ReactNode }, // Explicitly type children prop
+ ) => {children}
+
+ const { result } = renderHook(() => useContext(StudentFiltersContext), {
+ wrapper,
+ })
+
+ act(() => {
+ result.current?.addRole('Role1') // Guard against result.current being undefined
+ })
+
+ expect(result.current?.selectedRoles).toEqual(['Role1']) // Guard against result.current being undefined
+ })
+
+ it('should remove role from selectedRoles', () => {
+ const wrapper = (
+ { children }: { children: ReactNode }, // Explicitly type children prop
+ ) => {children}
+
+ const { result } = renderHook(() => useContext(StudentFiltersContext), {
+ wrapper,
+ })
+
+ act(() => {
+ result.current?.addRole('Role1') // Guard against result.current being undefined
+ result.current?.addRole('Role2') // Guard against result.current being undefined
+ result.current?.removeRole('Role1') // Guard against result.current being undefined
+ })
+
+ expect(result.current?.selectedRoles).toEqual(['Role2']) // Guard against result.current being undefined
+ })
+})
diff --git a/src/__tests__/components/studentFilters/StudentFiltersContent.test.tsx b/src/__tests__/components/studentFilters/StudentFiltersContent.test.tsx
new file mode 100644
index 0000000..90f30e2
--- /dev/null
+++ b/src/__tests__/components/studentFilters/StudentFiltersContent.test.tsx
@@ -0,0 +1,69 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { render, RenderResult, waitFor } from '@testing-library/react';
+import { act } from 'react-dom/test-utils';
+import StudentFiltersProvider from '../../../components/studentFilters/StudentFiltersContent';
+import { StudentFiltersContext } from '../../../context/StudentFiltersContext';
+
+describe('StudentFiltersProvider', () => {
+ let mock: MockAdapter;
+
+ beforeAll(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.reset();
+ });
+
+ afterAll(() => {
+ mock.restore();
+ });
+
+ const selectedRoles: string[] = [];
+ const addRole = () => { };
+ const removeRole = () => { };
+
+ const value = {
+ selectedRoles,
+ addRole,
+ removeRole,
+ };
+
+ const rolesData: string[] | undefined = [];
+ const developmentData: string[] | undefined = [];
+
+ test('renders student filters correctly', async () => {
+ // Mock API responses
+ mock
+ .onGet('https://itaperfils.eurecatacademy.org/api/v1/specialization/list')
+ .reply(200, rolesData);
+
+ mock
+ .onGet('https://itaperfils.eurecatacademy.org/api/v1/development/list')
+ .reply(200, developmentData);
+
+ let getByText: RenderResult['getByText'];
+
+ // Render the component
+ await act(async () => {
+ const renderResult = render(
+
+
+
+ );
+ getByText = renderResult.getByText;
+
+ // Wait for data to be fetched and rendered
+ await waitFor(() => {
+ rolesData.forEach((role) => {
+ expect(getByText(role)).toBeInTheDocument();
+ });
+
+ developmentData.forEach((development) => {
+ expect(getByText(development)).toBeInTheDocument();
+ });
+ });
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/api/FetchStudentsList.ts b/src/api/FetchStudentsList.ts
index d2e8b6b..e23954f 100644
--- a/src/api/FetchStudentsList.ts
+++ b/src/api/FetchStudentsList.ts
@@ -1,15 +1,26 @@
-import axios from 'axios'
-import { IStudentList } from '../interfaces/interfaces'
+// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars
+import axios, { AxiosError } from 'axios';
+import { IStudentList } from '../interfaces/interfaces';
// eslint-disable-next-line consistent-return
-export const FetchStudentsListHome = async () => {
+export const FetchStudentsListHome = async (selectedRoles:Array= []) => {
+
try {
- const response = await axios.get(
- 'https://itaperfils.eurecatacademy.org/api/v1/student/list/for-home',
- )
- return response.data
- } catch (e) {
+ let queryParams = '';
+
+ // Construir la cadena de consulta para roles seleccionados
+ if (selectedRoles.length > 0) {
+ queryParams += `specialization=${selectedRoles.join(',')}`;
+ }
+
+ // Construir la URL completa con la cadena de consulta
+ const url = `https://itaperfils.eurecatacademy.org/api/v1/student/list/for-home${queryParams ? `?${queryParams}` : ''}`;
+
+ const response = await axios.get(url);
+ return response.data;
+ // @ts-expect-error throws AxiosError exception
+ } catch (e: AxiosError) {
// eslint-disable-next-line no-console
- console.error(e)
+ throw new DOMException(e.message, 'ConnectionFailed');
}
-}
+ };
\ No newline at end of file
diff --git a/src/components/landing/Landing.tsx b/src/components/landing/Landing.tsx
index a0da1e4..44c4861 100644
--- a/src/components/landing/Landing.tsx
+++ b/src/components/landing/Landing.tsx
@@ -3,19 +3,25 @@ import UserNavbar from '../userNavBar/UserNavbar'
import StudentDetailsLayout from '../studentDetail/StudentDetailsLayout'
import StudentsLayout from '../students/StudentsLayout'
import StudentFiltersLayout from '../studentFilters/StudentFiltersLayout'
+import { StudentFiltersProvider } from '../../context/StudentFiltersContext'
const Landing = () => (
-
+
{/* Added data-testid */}
-
-
-
-
-
+
{/* Added data-testid */}
+
+
+
+ {' '}
+ {/* Added data-testid */}
+ {' '}
+ {/* Added data-testid */}
+
+
{' '}
+ {/* Added data-testid */}
-
-
+
)
diff --git a/src/components/studentFilters/StudentFiltersContent.tsx b/src/components/studentFilters/StudentFiltersContent.tsx
index a9f42a8..d89fe7b 100644
--- a/src/components/studentFilters/StudentFiltersContent.tsx
+++ b/src/components/studentFilters/StudentFiltersContent.tsx
@@ -1,83 +1,115 @@
import axios from 'axios'
-import { useEffect, useState } from 'react'
+import React, { useContext, useEffect, useMemo, useState } from 'react'
+import { StudentFiltersContext } from '../../context/StudentFiltersContext'
-const StudentFiltersContent: React.FC = () => {
+const StudentFiltersProvider: React.FC = () => {
const [roles, setRoles] = useState
([])
const [development, setDevelopment] = useState([])
+ const context = useContext(StudentFiltersContext)
+
+ if (!context) {
+ throw new Error('StudentFiltersContext must be provided')
+ }
+
+ const { selectedRoles, addRole, removeRole } = context
+
+ const value = useMemo(
+ () => ({
+ selectedRoles,
+ addRole,
+ removeRole,
+ }),
+ [selectedRoles, addRole, removeRole],
+ )
+
const urlRoles =
'https://itaperfils.eurecatacademy.org/api/v1/specialization/list'
const urlDevelopment =
'https://itaperfils.eurecatacademy.org/api/v1/development/list'
- const fetchData = (
+ const fetchData = async (
url: string,
+
setData: React.Dispatch>,
) => {
- axios
- .get(url)
- .then((response) => {
- setData(response.data)
- })
- .catch((error) => {
- // eslint-disable-next-line no-console
- console.error(error)
- })
+ try {
+ const response = await axios.get(url)
+ setData(response.data)
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error('Error fetching data:', error)
+ // Handle error gracefully, e.g., show a message to the user
+ }
}
useEffect(() => {
fetchData(urlRoles, setRoles)
- }, [urlRoles])
-
- useEffect(() => {
fetchData(urlDevelopment, setDevelopment)
- }, [urlDevelopment])
+ }, [urlRoles, urlDevelopment])
+
+ const toggleRole = (role: string) => {
+ if (selectedRoles.includes(role)) {
+ removeRole(role)
+ } else {
+ addRole(role)
+ }
+ }
return (
-
-
Filtros
-
-
-
Roles
-
- {roles.map((role) => (
-
- ))}
+
+
+
Filtros
+
+
+
Roles
+
+ {roles.map((role) => (
+
+ ))}
+
-
-
-
+
)
}
-export default StudentFiltersContent
+export default StudentFiltersProvider
diff --git a/src/components/studentFilters/StudentFiltersLayout.tsx b/src/components/studentFilters/StudentFiltersLayout.tsx
index 34dada3..e4126f1 100644
--- a/src/components/studentFilters/StudentFiltersLayout.tsx
+++ b/src/components/studentFilters/StudentFiltersLayout.tsx
@@ -2,8 +2,8 @@ import StudentFiltersContent from './StudentFiltersContent'
const StudentFiltersLayout: React.FC = () => (
-
+
)
-export default StudentFiltersLayout
+export default StudentFiltersLayout
\ No newline at end of file
diff --git a/src/components/students/StudentsList.tsx b/src/components/students/StudentsList.tsx
index 1743f50..63c32a6 100644
--- a/src/components/students/StudentsList.tsx
+++ b/src/components/students/StudentsList.tsx
@@ -1,28 +1,31 @@
-import { useEffect, useState } from 'react'
+import { useContext, useEffect, useState } from 'react'
import StudentCard from './StudentCard'
import { useAppSelector } from '../../hooks/ReduxHooks'
import { IStudentList } from '../../interfaces/interfaces'
import { FetchStudentsListHome } from '../../api/FetchStudentsList'
+import { StudentFiltersContext } from '../../context/StudentFiltersContext';
const StudentsList: React.FC = () => {
const isPanelOpen = useAppSelector(
(state) => state.ShowUserReducer.isUserPanelOpen,
- )
+ );
+
+ const studentFilterContext = useContext(StudentFiltersContext)
- const [students, setStudents] = useState
()
+ const [students, setStudents] = useState()
useEffect(() => {
const fetchStudents = async () => {
try {
- const studentsList = await FetchStudentsListHome()
- setStudents(studentsList)
+ const studentsList = await FetchStudentsListHome( studentFilterContext?.selectedRoles || []);
+ setStudents(studentsList);
} catch (error) {
// eslint-disable-next-line no-console
- console.error('Error fetching students:', error)
+ console.error('Error fetching students:', error);
}
- }
- fetchStudents()
- }, [])
+ };
+ fetchStudents();
+ }, [studentFilterContext?.selectedRoles]);
return (
void;
+ removeRole: (role: string) => void;
+}
+
+export const StudentFiltersContext = createContext(undefined);
+
+interface StudentFiltersProviderProps {
+ children: ReactNode;
+}
+
+export const StudentFiltersProvider: React.FC = ({ children }) => {
+ const [selectedRoles, setSelectedRoles] = useState([]);
+
+ const addRole = useCallback((role: string) => {
+ setSelectedRoles(prevRoles => [...prevRoles, role]);
+ }, []);
+
+ const removeRole = useCallback((role: string) => {
+ setSelectedRoles(prevRoles => prevRoles.filter(r => r !== role));
+ }, []);
+
+ const value = useMemo(() => ({ selectedRoles, addRole, removeRole }), [selectedRoles, addRole, removeRole]);
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/src/store/reducers/getUserDetail/apiGetUserDetail.ts b/src/store/reducers/getUserDetail/apiGetUserDetail.ts
index 55c33ae..5d2fac0 100644
--- a/src/store/reducers/getUserDetail/apiGetUserDetail.ts
+++ b/src/store/reducers/getUserDetail/apiGetUserDetail.ts
@@ -1,21 +1,29 @@
-/* eslint-disable no-param-reassign */
-import { createSlice } from '@reduxjs/toolkit'
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
const initialState = {
- isUserPanelOpen: false,
+ isUserPanelOpen: false,
+ filteredStudents: ''
}
-const studentDetailPanel = createSlice({
- name: 'userDetail',
- initialState,
- reducers: {
- openUserPanel: (state) => {
- state.isUserPanelOpen = true
+const showUserInfo = createSlice({
+ name: "showUserReducer",
+ initialState,
+ reducers: {
+ openUserPanel: (state) => {
+ // eslint-disable-next-line no-param-reassign
+ state.isUserPanelOpen = true;
+ },
+ closeUserPanel: (state) => {
+ // eslint-disable-next-line no-param-reassign
+ state.isUserPanelOpen = false;
+ },
+ setFilteredStudents: (state, action: PayloadAction) => {
+ // eslint-disable-next-line no-param-reassign
+ state.filteredStudents = action.payload;
+ },
},
- closeUserPanel: (state) => {
- state.isUserPanelOpen = false
- },
- },
-})
-export const { openUserPanel, closeUserPanel } = studentDetailPanel.actions
-export default studentDetailPanel.reducer
+});
+
+export const { openUserPanel, closeUserPanel, setFilteredStudents } = showUserInfo.actions;
+
+export default showUserInfo.reducer;
\ No newline at end of file