Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#266 implement search functionality fe #271

Merged
merged 18 commits into from
Oct 21, 2023
2 changes: 2 additions & 0 deletions FrontEnd/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AdminPage from './components/adminPage/AdminPage';
import BasicPage from './components/basicPage/BasicPage';
import { AuthContext } from './context';
import { useProvideAuth } from './hooks';
import { Search } from './components/SearchPage/Search';

function App() {
const auth = useProvideAuth();
Expand All @@ -16,6 +17,7 @@ function App() {
<Routes>
<Route path="/*" element={<BasicPage />} />
<Route path="/admin/*" element={<AdminPage />} />
<Route path="/search" element={<Search isAuthorized={false} />} />
</Routes>
</div>
</BrowserRouter>
Expand Down
35 changes: 17 additions & 18 deletions FrontEnd/src/components/HeaderFooter/header/navbar/Menu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,26 @@ import css from './Menu.module.css';
import { HashLink } from 'react-router-hash-link';

const MENU_LINKS = [
{
id: 'm0',
title: 'Головна',
link: '/'
},
{
title: 'Компанії',
link: '/profiles/companies'
},
{
title: 'Стартапи',
link: '/profiles/startups'
},
{
title: 'Про нас',
link: '/#about-us'
},
{
id: 'm0',
title: 'Головна',
link: '/',
},
{
title: 'Компанії',
link: '/profiles/companies',
},
{
title: 'Стартапи',
link: '/profiles/startups',
},
{
title: 'Про нас',
link: '/#about-us',
},
];

function Menu() {
console.log(MENU_LINKS.map((element) => (element.title.startsWith('/#'))));
return (
<div className={css['header-menu-section']}>
{MENU_LINKS.map((element) => (
Expand Down
44 changes: 34 additions & 10 deletions FrontEnd/src/components/HeaderFooter/header/navbar/SearchBox.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
import css from './SearchBox.module.css';
import { useState } from 'react';
import icon_search from './search-icon.svg';
import { useNavigate } from 'react-router-dom';
import css from './SearchBox.module.css';

function SearchBox() {
const navigate = useNavigate();

const [searchTerm, setSearchTerm] = useState('');
const searchPage = 'search';

function SearchBox () {
return (
<div className={css['header-search-box']}>
<div className={css['header-search-form']}>
<input className={css['header-search-form__input']} placeholder="Пошук"></input>
</div>
<span className={css['header-search-form__addon']}><img src={icon_search} alt=""/></span>
</div>
);
const handleSearch = (searchTerm, searchPage) => {
if (searchTerm.trim() !== '') {
navigate(`/${searchPage}/?name=${searchTerm}`);
}
};
return (
<div className={css['header-search-box']}>
<div className={css['header-search-form']}>
<input
className={css['header-search-form__input']}
value={searchTerm}
type="text"
placeholder="Пошук"
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<button
onClick={() => handleSearch(searchTerm, searchPage)}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also would expect to run a search from the keyboard on the enter press

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain it, please?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for feedback. Done.

Copy link
Collaborator

@popovycholeg popovycholeg Oct 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean when I heat Enter button from the keyboard I would expect to run a search

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

className={css['header-search-form__addon']}
disabled={!searchTerm.trim()}
>
<img src={icon_search} alt="" />
</button>
</div>
);
}

export default SearchBox;
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.header-search-box {
/* .header-search-box {
display: flex;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uncomment or remove

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for feedback. Done.

align-items: center;
flex-shrink: 0;
Expand Down Expand Up @@ -53,4 +53,60 @@
textarea:focus, .header-search-form__input:focus{
border: none;
outline: none;
} */

.header-search-box {
display: flex;
align-items: center;
flex-shrink: 0;
width: 264px;

border-radius: 2px;
border: 1px solid #d9d9d9;
background: #fff;
}

.header-search-form {
display: flex;
padding: 4px 12px;
align-items: flex-start;
gap: 10px;
flex: 1 0 0;
}

.header-search-form__input {
display: flex;
padding: 1px 0px;
align-items: flex-start;
gap: 10px;
flex: 1 0 0;
border: none;
}

.header-search-form__addon {
background-color: transparent;
cursor: pointer;
border: 1px solid rgba(0, 0, 0, 0.25);
}

.header-search-form__addon img {
width: 20px;
height: 20px;
}

.header-search-form__input::placeholder {
color: rgba(0, 0, 0, 0.25);
font-feature-settings: 'calt' off;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
letter-spacing: -0.14px;
}

textarea:focus,
.header-search-form__input:focus {
border: none;
outline: none;
}
142 changes: 142 additions & 0 deletions FrontEnd/src/components/SearchPage/Search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import axios from 'axios';
import { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import BreadCrumbs from '../BreadCrumbs/BreadCrumbs';
import SearchResults from './search_field/SearchResults';
import frame42 from './img/frame42.png';
import link_to_left from './img/link_to_left.svg';
import link_to_right from './img/link_to_right.svg';
import './search_page.css';

const ITEMS_PER_PAGE = 6;

export function Search(props) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prop types

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for feedback. Done

const [searchResults, setSearchResults] = useState([]);
const [searchPerformed, setSearchPerformed] = useState(false);
const [error, setError] = useState(null);

const location = useLocation();
const searchParams = new URLSearchParams(location.search);
const searchTerm = searchParams.get('name');
const servedAddress = process.env.REACT_APP_BASE_API_URL;
const searchUrl = 'search';

useEffect(() => {
if (searchTerm) {
// Make an AJAX request to Django API to get search results
axios
.get(`${servedAddress}/api/search/?name=${searchTerm}`)
.then((response) => {
setSearchResults(response.data);
setSearchPerformed(true);
setError(null); // Clear error on successful response
})
.catch((error) => {
console.error('Error fetching search results:', error);
setError(error.response ? error.response.data : 'An error occurred');
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can handle it using swr for the consistency

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for feedback. Done

}
}, [searchTerm, servedAddress, searchUrl]);

const [currentPage, setCurrentPage] = useState(1);
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);

const handlePageChange = (newPage) => {
setCurrentPage(newPage);
};

return (
<div className="main_block_outer">
<BreadCrumbs currentPage="Пошук" />
<div className="main_block">
<img className="frame-img-right" src={frame42} alt="frame" />
<div className="new-companies-search_count">
{searchResults && (
<h3 className="search_results_text">
РЕЗУЛЬТАТІВ ЗА ПОШУКОМ{' '}
<span className="search_field_entered_value">{searchTerm}</span> :{' '}
{searchResults.length > 0 ? searchResults.length : 0}
</h3>
)}
<br />
</div>
<div className="new-companies-main">
{!error && searchResults.length > 0 ? (
<>
<SearchResults
results={searchResults}
searchPerformed={searchPerformed}
displayedResults={displayedResults}
isAuthorized={props.isAuthorized}
/>
<br />
</>
) : (
<p className="search_result_error">
Пошук не дав результатів: компанії з іменем{' '}
<span className=".search_result_error search_result_error_search_value">
{searchTerm}
</span>{' '}
не було виявлено на даний момент
</p>
)}
</div>
<div className="new-companies-result_pages">
{totalPages > 1 && (
<div className="pagination">
{currentPage > 1 && (
<button onClick={() => handlePageChange(currentPage - 1)}>
<img src={link_to_left} alt="Link to Left" />
</button>
)}
{currentPage > 1 && (
<>
<button onClick={() => handlePageChange(1)}>1</button>
{currentPage > 2 && <span className="ellipsis">...</span>}
</>
)}
{Array.from({ length: totalPages }, (_, i) => {
if (
i === 2 ||
i === totalPages ||
(i >= currentPage - 1 && i <= currentPage)
) {
return (
<button
key={i}
onClick={() => handlePageChange(i + 1)}
className={currentPage === i + 1 ? 'active' : ''}
>
{i + 1}
</button>
);
}
return null;
})}
{currentPage < totalPages - 1 && (
<>
{currentPage < totalPages - 1 && (
<span className="ellipsis">...</span>
)}
<button onClick={() => handlePageChange(totalPages)}>
{totalPages}
</button>
</>
)}
{currentPage < totalPages && (
<button onClick={() => handlePageChange(currentPage + 1)}>
<img src={link_to_right} alt="Link to Right" />
</button>
)}
</div>
)}
</div>
</div>
</div>
);
}
Binary file added FrontEnd/src/components/SearchPage/img/frame42.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions FrontEnd/src/components/SearchPage/img/link_to_left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions FrontEnd/src/components/SearchPage/img/link_to_right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions FrontEnd/src/components/SearchPage/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to create a new react root for the SearchPage?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for feedback. Done.

root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
33 changes: 33 additions & 0 deletions FrontEnd/src/components/SearchPage/search_field/SearchResults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import MainCompanies from './companies/Companies';
import './Text.css';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use module css

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for feedback. Done.


const SearchResults = ({ results, displayedResults, isAuthorized }) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prop types

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for feedback. Done

let error = null;

if (results && results.error) {
error = results.error;
}

return (
<div>
{!error && (
<>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the fragment if you have only one child

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for feedback. Done.

<div className="new-companies-block">
<div className="row">
{displayedResults.map((result, resultIndex) => (
<div key={resultIndex} className="col-md-4">
<MainCompanies
companyData={result}
isAuthorized={isAuthorized}
/>
</div>
))}
</div>
</div>
</>
)}
</div>
);
};

export default SearchResults;
Loading
Loading