diff --git a/FrontEnd/src/components/Loader/Loader.module.css b/FrontEnd/src/components/Loader/Loader.module.css index c24dedb7a..f8ed507fc 100644 --- a/FrontEnd/src/components/Loader/Loader.module.css +++ b/FrontEnd/src/components/Loader/Loader.module.css @@ -1,4 +1,5 @@ .loader__container { padding-top: 30%; min-height: var(--min-height-block-main); + width: 100%; } diff --git a/FrontEnd/src/pages/CustomThemes/customTheme.js b/FrontEnd/src/pages/CustomThemes/customTheme.js index 94ba1b9c0..652dca796 100644 --- a/FrontEnd/src/pages/CustomThemes/customTheme.js +++ b/FrontEnd/src/pages/CustomThemes/customTheme.js @@ -50,6 +50,7 @@ const customTheme = { }, List: { fontFamily: 'Geologica', + emptyTextPadding: 0, } }, token: { diff --git a/FrontEnd/src/pages/ProfileList/ProfileList.jsx b/FrontEnd/src/pages/ProfileList/ProfileList.jsx index ade6f33df..a53206a6a 100644 --- a/FrontEnd/src/pages/ProfileList/ProfileList.jsx +++ b/FrontEnd/src/pages/ProfileList/ProfileList.jsx @@ -5,6 +5,7 @@ import CompanyCard from '../../components/CompanyCard/CompanyCard'; export default function ProfileList({ isAuthorized, + emptyText, current, items, profiles, @@ -34,7 +35,7 @@ export default function ProfileList({ grid={{ justify: 'center', align: 'stretch', - gutter: [32, 24], + gutter: [24, 24], xs: 1, md: 2, xl: 3, @@ -53,9 +54,13 @@ export default function ProfileList({ }} dataSource={profiles} split={false} - locale={{emptyText: 'Жодна компанія не відповідає обраному фільтру.'}} + locale={{emptyText: emptyText}} renderItem={(item) => ( - + { - const response = await axios.get(url); - setSearchResults(response.data); - }; + async function fetcher(url) { + return axios.get(url) + .then(res => res.data); + } - const { data: companylist, error } = useSWR( - `${servedAddress}/api/search/?name=${searchTerm}&ordering=name`, - fetcher + const { data: companylist, isLoading } = useSWR( + `${servedAddress}/api/search/?name=${searchTerm}&ordering=name&page_size=${pageSize}&page=${currentPage}`, + fetcher, + {onSuccess: (data) => setSearchResults(data.results)} ); const changeCompanies = (id, isSaved) => { @@ -42,116 +40,90 @@ export function Search({ isAuthorized }) { setSearchResults(newCompanies); }; - useEffect(() => { - if (searchTerm) { - setSearchPerformed(true); - } - }, [searchTerm, servedAddress, searchUrl, companylist]); + const windowWidth = useWindowWidth(); useEffect(() => { - setCurrentPage(pageNumber); - }, [pageNumber]); - - const [currentPage, setCurrentPage] = useState(pageNumber); - const totalItems = searchResults.length; - const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE); - - const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; - const endIndex = startIndex + ITEMS_PER_PAGE; - const displayedResults = searchResults.slice(startIndex, endIndex); + if (windowWidth < 768) { + setPageSize(4); + } else if (windowWidth >= 768 && windowWidth < 1200) { + setPageSize(16); + } else if (windowWidth >= 1200 && windowWidth < 1512) { + setPageSize(12); + } else if (windowWidth >= 1512) { + setPageSize(16); + } + }, [windowWidth]); - const handlePageChange = (newPage) => { - setCurrentPage(newPage); + const updateQueryParams = (newPage) => { searchParams.set('page', newPage); - navigate(`?${searchParams.toString()}`); + setSearchParams(searchParams); }; + const handlePageChange = (page) => { + setCurrentPage(page); + updateQueryParams(page); + }; + + useEffect(() => { + if (companylist?.total_items === 0) { + setCurrentPage(1); + searchParams.delete('page'); + setSearchParams(searchParams); + } else { + const totalPages = Math.ceil(companylist?.total_items / pageSize); + if (currentPage > totalPages) { + setCurrentPage(totalPages); + updateQueryParams(totalPages); + } + } + }, [companylist?.total_items, pageSize, currentPage]); + return ( -
-
-
- {searchResults && ( -
-

- Результати пошуку - - {` “${searchTerm}” `} - - : {searchResults.length > 0 ? searchResults.length : 0} -

-
- )} -
- {!error && searchResults.length > 0 && ( +
+
+ {isLoading ? + : ( <> - -
- {totalPages > 1 && ( -
- {currentPage > 1 && ( - - )} - {currentPage > 1 && ( - <> - - {currentPage > 2 && ( - ... - )} - - )} - {Array.from({ length: totalPages }, (_, i) => { - if ( - i === 2 || - i === totalPages || - (i >= currentPage - 1 && i <= currentPage) - ) { - return ( - - ); - } - return null; - })} - {currentPage < totalPages - 1 && ( - <> - {currentPage < totalPages - 1 && ( - ... - )} - - - )} - {currentPage < totalPages && ( - - )} +
+ {searchResults && ( +
+

+ Результати пошуку + + {` “${searchTerm}” `} + + : {companylist?.total_items || 0} +

)}
+
+ +
)}
- {searchResults.length === 0 && -
-

+ {companylist?.total_items === 0 && +

+

Пошук не дав результатів

-
} +
+ }
); } diff --git a/FrontEnd/src/pages/SearchPage/search.module.scss b/FrontEnd/src/pages/SearchPage/Search.module.scss similarity index 53% rename from FrontEnd/src/pages/SearchPage/search.module.scss rename to FrontEnd/src/pages/SearchPage/Search.module.scss index 7ed26a476..737cb8cf9 100644 --- a/FrontEnd/src/pages/SearchPage/search.module.scss +++ b/FrontEnd/src/pages/SearchPage/Search.module.scss @@ -1,46 +1,44 @@ -.main_block_outer { +.search-page__outer { display: flex; flex-direction: column; position: relative; } -.new-companies-result_pages { - padding: 40px; +.search-page { + display: flex; + align-self: center; + align-items: flex-start; + flex-direction: column; + background: var(--new-companies-background-color); + box-sizing: border-box; + width: 375px; + padding: 40px 15px; + gap: 32px; } -.new-companies-main__error { - width: 100%; - background-color: rgba(249, 245, 236, 1); - display: flex; - justify-content: center; - align-items: center; - min-height: 288px; - flex-grow: 1; +.search-page__empty { + height: 102px; } -.new-companies-main { +.search-list__content--items { display: flex; - align-self: center; - align-items: flex-start; flex-direction: column; - background: var(--new-companies-background-color); - min-width: 375px; + width: 100%; } -.new-companies-search_count { +.search-page__results-count { display: flex; flex-direction: row; - padding: 40px 16px; } -.search_field_entered_value { +.search-field__entered-value { color: #707070; font: var(--font-main); font-weight: 300; font-size: 18px ; } -.search_results_text { +.search-results__text { font: normal normal bold 18px/2 var(--font-main); font-size: 18px; font-weight: 700; @@ -48,72 +46,78 @@ line-height: 22px; } -.search_result_error { +.search-page__error { width: 100%; - color: var(--search-text-color); - font: normal normal bold 24px/2 var(--font-main); - text-align: center; - white-space: nowrap; -} - -.pagination { + background-color: rgba(249, 245, 236, 1); display: flex; justify-content: center; align-items: center; - margin-top: 20px; -} - -.pagination button { - all: unset; - font-size: 16px; - font-weight: 500; - padding: 8px 16px; - margin: 0 4px; - cursor: pointer; + min-height: 288px; + flex-grow: 1; } -.pagination button.active { - background-color: transparent; +.search_result__error { + width: 100%; color: var(--search-text-color); - border: 1px solid var(--search-text-color); - border-radius: 5px; - transition: background-color 0.3s, color 0.3s; + font: normal normal bold 24px/2 var(--font-main); + text-align: center; + white-space: nowrap; } - - - @media only screen and (min-width: 768px){ - .search_results_text { + .search-results__text { font-size: 34px; line-height: 41px; } - .search_field_entered_value{ + .search-field__entered-value{ font-size: 34px; } - .search_result_error { + .search-result__error { font: normal normal bold 40px/2 var(--font-main); } - .new-companies-search_count { - padding: 40px 24px; + .search-page { + width: 768px; + padding: 40px 27px; } - .new-companies-main { - width: 768px; + .search-page__empty { + height: 121px; } } -@media only screen and (min-width: 1512px){ - .new-companies-search_count { - padding: 40px 14px; +@media only screen and (min-width: 1200px){ + .search-results__text { + font-size: 34px; + line-height: 41px; + } + + .search-field__entered-value{ + font-size: 34px; + } + + .search-result__error { + font: normal normal bold 40px/2 var(--font-main); } - .new-companies-main { - width: 1304px; + .search-page { + width: 1200px; + padding: 40px 35px; + } + + .search-page__error { + min-height: 480px; + } + +} + +@media only screen and (min-width: 1512px){ + .search-page { + width: 1512px; + padding: 40px 107px; } } diff --git a/FrontEnd/src/pages/SearchPage/SearchField/SearchResults.jsx b/FrontEnd/src/pages/SearchPage/SearchField/SearchResults.jsx deleted file mode 100644 index 8871d4fac..000000000 --- a/FrontEnd/src/pages/SearchPage/SearchField/SearchResults.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { Row, Col} from 'antd'; -import CompanyCard from '../../../components/CompanyCard/CompanyCard'; -import styles from './SearchResults.module.css'; -import PropTypes from 'prop-types'; - -const SearchResults = ({ - results, - displayedResults, - isAuthorized, - changeCompanies, -}) => { - let error = null; - - if (results && results.error) { - error = results.error; - } - - return ( -
- {!error && ( - - {displayedResults.map((result, resultIndex) => ( - - - - ))} - - )} -
- ); -}; - -export default SearchResults; - -SearchResults.propTypes = { - results: PropTypes.array, - displayedResults: PropTypes.array, - isAuthorized: PropTypes.bool, - changeCompanies: PropTypes.func, -}; diff --git a/FrontEnd/src/pages/SearchPage/SearchField/SearchResults.module.css b/FrontEnd/src/pages/SearchPage/SearchField/SearchResults.module.css deleted file mode 100644 index ad8067862..000000000 --- a/FrontEnd/src/pages/SearchPage/SearchField/SearchResults.module.css +++ /dev/null @@ -1,18 +0,0 @@ -.new-companies-block { - width: 375px; - padding: 18px 0px 38px 0px; -} - - -@media only screen and (min-width: 768px) { - .new-companies-block { - width: 768px; - } -} - -@media only screen and (min-width: 1512px) { - .new-companies-block { - width: 1304px; - } -} - diff --git a/FrontEnd/src/pages/SearchPage/SearchField/tests/SearchResults.test.js b/FrontEnd/src/pages/SearchPage/SearchField/tests/SearchResults.test.js deleted file mode 100644 index 2ec9fbc7f..000000000 --- a/FrontEnd/src/pages/SearchPage/SearchField/tests/SearchResults.test.js +++ /dev/null @@ -1,138 +0,0 @@ -import { render, screen, cleanup } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; - -import SearchResults from '../SearchResults'; - -const mockedUser = { - email: 'test@test.com', - id: 1, - name: 'Test', - profile_id: 1, - surname: 'Test' -}; -jest.mock('../../../../hooks/useAuth', () => ({ - useAuth: () => { - return mockedUser; - }, -})); - -afterEach(cleanup); - -describe('SearchResults component unit tests', () => { - test('renders search results', () => { - Object.defineProperty(window, 'matchMedia', { - writable: true, - value: jest.fn().mockImplementation(query => ({ - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - dispatchEvent: jest.fn(), - })), - }); - const results = [ - { - id: 1, - name: 'saleonline', - categories: [ - { - id: 1, - name: 'trade', - }, - { - id: 2, - name: 'transport', - }, - ], - region: 'Dnipro', - founded: 1980, - service_info: null, - address: 'Kyiv', - banner: null, - is_saved: true, - }, - { - id: 2, - name: 'sale', - categories: [ - { - id: 1, - name: 'trade', - }, - ], - region: 'Dnipro', - founded: 2007, - service_info: null, - address: 'Dnipro', - banner: null, - is_saved: false, - }, - { - id: 3, - name: 'PizzaHousesale', - categories: [], - region: 'Charkiv', - founded: null, - service_info: null, - address: 'Zaporija', - banner: null, - }, - { - id: 4, - name: 'salefruits', - categories: [], - region: null, - founded: null, - service_info: null, - address: null, - banner: null, - }, - { - id: 5, - name: 'salevegetables', - categories: [], - region: null, - founded: null, - service_info: null, - address: null, - banner: null, - }, - { - id: 6, - name: 'salebushes', - categories: [], - region: null, - founded: null, - service_info: null, - address: null, - banner: null, - }, - { - id: 7, - name: 'GGGsale', - categories: [], - region: null, - founded: null, - service_info: null, - address: null, - banner: null, - }, - ]; - - const displayedResults = results.slice(0, 7); - render( - - - - ); - const nameElement = screen.getAllByText(/sale/i, { exact: false }); - expect(nameElement).toHaveLength(results.length); - }); -}); diff --git a/FrontEnd/src/pages/SearchPage/img/link_to_left.svg b/FrontEnd/src/pages/SearchPage/img/link_to_left.svg deleted file mode 100644 index 849379319..000000000 --- a/FrontEnd/src/pages/SearchPage/img/link_to_left.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/FrontEnd/src/pages/SearchPage/img/link_to_right.svg b/FrontEnd/src/pages/SearchPage/img/link_to_right.svg deleted file mode 100644 index 450b2ecdc..000000000 --- a/FrontEnd/src/pages/SearchPage/img/link_to_right.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/FrontEnd/src/pages/SearchPage/tests/Search.test.js b/FrontEnd/src/pages/SearchPage/tests/Search.test.js index 57ad370df..15e66ec70 100644 --- a/FrontEnd/src/pages/SearchPage/tests/Search.test.js +++ b/FrontEnd/src/pages/SearchPage/tests/Search.test.js @@ -1,95 +1,246 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, cleanup, act } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; +import axios from 'axios'; import Search from '../Search'; +import ProfileList from '../../ProfileList/ProfileList'; -jest.mock('../../../hooks', () => ({ - useAuth: () => ({isStaff: false}), +const mockedUser = { + email: 'test@test.com', + id: 1, + name: 'Test', + profile_id: 1, + surname: 'Test' +}; + +jest.mock('../../../hooks/useAuth', () => ({ + useAuth: () => { + return mockedUser; + }, })); -afterEach(() => { - jest.resetAllMocks(); +jest.mock('axios'); + +beforeEach(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); }); +afterEach(cleanup); + describe('Search component unit tests', () => { - test('renders sale search page', () => { - render( - - - - ); + test('renders sale search page', async () => { + await act(async () => { + render( + + + + ); + }); const counterElement = screen.getByText(/Результати пошуку/i, { exact: false, }); expect(counterElement).toBeInTheDocument(); }); - test('testing search', () => { - jest.mock('axios'); - - const axios = require('axios', () => { - jest.fn().mockResolvedValue({}); - - () => { - axios.get.mockResolvedValue({ - data: [ - { - id: 1, - name: 'saleonline', - categories: [ - { - id: 1, - name: 'trade', - }, - { - id: 2, - name: 'transport', - }, - ], - region: 'Dnipro', - founded: 1980, - service_info: null, - address: 'Kyiv', - banner: null, - }, - { - id: 2, - name: 'sale', - categories: [ - { - id: 1, - name: 'trade', - }, - ], - region: 'Dnipro', - founded: 2007, - service_info: null, - address: 'Dnipro', - banner: null, - }, - { - id: 3, - name: 'PizzaHousesale', - categories: [], - region: 'Charkiv', - founded: null, - service_info: null, - address: 'Zaporija', - banner: null, - }, - ], - }); - }; + test('testing search', async () => { + axios.get.mockResolvedValue({ + data: { + results: [ + { + id: 1, + name: 'saleonline', + categories: [ + { + id: 1, + name: 'trade', + }, + { + id: 2, + name: 'transport', + }, + ], + region: 'Dnipro', + founded: 1980, + service_info: null, + address: 'Kyiv', + banner: null, + }, + { + id: 2, + name: 'sale', + categories: [ + { + id: 1, + name: 'trade', + }, + ], + region: 'Dnipro', + founded: 2007, + service_info: null, + address: 'Dnipro', + banner: null, + }, + { + id: 3, + name: 'PizzaHousesale', + categories: [], + region: 'Charkiv', + founded: null, + service_info: null, + address: 'Zaporija', + banner: null, + }, + ], + total_items: 3, + }, + }); + await act(async () => { render( ); - expect(axios.get).toBeCalled(); - expect(screen.getByText(/назад/i, { exact: false })).toBeInTheDocument(); - expect(screen.getByRole('link')).toHaveAttribute('href', '/'); - expect(screen.getByText(/3/i, { exact: false })).toBeInTheDocument(); }); + + expect(axios.get).toBeCalled(); + const links = screen.getAllByRole('link'); + links.forEach(link => {expect(link).toHaveAttribute('href');}); + expect(screen.getByText(/Результати пошуку/i, { exact: false })).toBeInTheDocument(); + expect(screen.getByText(/3/i, { exact: false })).toBeInTheDocument(); + }); +}); + + +describe('Search results unit tests', () => { + test('renders search results', async () => { + const results = [ + { + id: 1, + name: 'saleonline', + categories: [ + { + id: 1, + name: 'trade', + }, + { + id: 2, + name: 'transport', + }, + ], + region: 'Dnipro', + founded: 1980, + service_info: null, + address: 'Kyiv', + banner: null, + is_saved: true, + }, + { + id: 2, + name: 'sale', + categories: [ + { + id: 1, + name: 'trade', + }, + ], + region: 'Dnipro', + founded: 2007, + service_info: null, + address: 'Dnipro', + banner: null, + is_saved: false, + }, + { + id: 3, + name: 'PizzaHousesale', + categories: [], + region: 'Charkiv', + founded: null, + service_info: null, + address: 'Zaporija', + banner: null, + }, + { + id: 4, + name: 'salefruits', + categories: [], + region: null, + founded: null, + service_info: null, + address: null, + banner: null, + }, + { + id: 5, + name: 'salevegetables', + categories: [], + region: null, + founded: null, + service_info: null, + address: null, + banner: null, + }, + { + id: 6, + name: 'salebushes', + categories: [], + region: null, + founded: null, + service_info: null, + address: null, + banner: null, + }, + { + id: 7, + name: 'GGGsale', + categories: [], + region: null, + founded: null, + service_info: null, + address: null, + banner: null, + }, + ]; + + const totalItems = 7; + + axios.get.mockResolvedValue({ + data: { + results: results, + total_items: totalItems, + }, + }); + + await act(async () => { + render( + + + + ); + }); + + const nameElements = screen.getAllByText(/sale/i, { exact: false }); + expect(nameElements).toHaveLength(results.length); }); });