From 4c55e8741696cda52f09f6300e40c9d2ecd755a9 Mon Sep 17 00:00:00 2001 From: afds4567 <33995840+afds4567@users.noreply.github.com> Date: Fri, 6 Oct 2023 11:09:24 +0900 Subject: [PATCH] =?UTF-8?q?[FE]=20Feat/#554=20search=20=EC=A7=80=EB=8F=84?= =?UTF-8?q?=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#555)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: SearchBar 컴포넌트 구현 * feat: Search 페이지 구현 * feat: 홈페이지에 searchBar 적용 * refactor: 불필요한 memo 제거 --- frontend/src/assets/search.svg | 2 + .../src/components/SearchBar/SearchBar.tsx | 71 +++++++++ frontend/src/hooks/useNavigator.ts | 1 + frontend/src/pages/Home.tsx | 7 +- frontend/src/pages/Search.tsx | 138 ++++++++++++++++++ frontend/src/router.tsx | 6 + 6 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 frontend/src/assets/search.svg create mode 100644 frontend/src/components/SearchBar/SearchBar.tsx create mode 100644 frontend/src/pages/Search.tsx diff --git a/frontend/src/assets/search.svg b/frontend/src/assets/search.svg new file mode 100644 index 00000000..0c9648b3 --- /dev/null +++ b/frontend/src/assets/search.svg @@ -0,0 +1,2 @@ + + diff --git a/frontend/src/components/SearchBar/SearchBar.tsx b/frontend/src/components/SearchBar/SearchBar.tsx new file mode 100644 index 00000000..5df898bd --- /dev/null +++ b/frontend/src/components/SearchBar/SearchBar.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; +import styled from 'styled-components'; +import useNavigator from '../../hooks/useNavigator'; +import SearchIcon from '../../assets/search.svg'; + +const SearchBar = () => { + const { routingHandlers } = useNavigator(); + + const [searchTerm, setSearchTerm] = useState(''); + + const onInputChange = (e: React.ChangeEvent) => { + setSearchTerm(e.target.value); + }; + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + routingHandlers.search(searchTerm); + }; + + return ( + + + + + ); +}; +export default SearchBar; + +const SearchBarWrapper = styled.form` + display: flex; + padding-left: 20px; + position: relative; + border-radius: 5px; + border: 1px solid #ccc; + box-shadow: 0px 1px 5px 3px rgba(0, 0, 0, 0.12); +`; + +const SearchInput = styled.input` + height: 45px; + width: 100%; + outline: none; + border: none; + border-radius: 5px; + padding: 0 60px 0 20px; + font-size: 18px; + font-weight: 500; + &:focus { + outline: none !important; + box-shadow: + inset -1px -1px rgba(0, 0, 0, 0.075), + inset -1px -1px rgba(0, 0, 0, 0.075), + inset -3px -3px rgba(255, 255, 255, 0.6), + inset -4px -4px rgba(255, 255, 255, 0.6), + inset rgba(255, 255, 255, 0.6), + inset rgba(64, 64, 64, 0.15); + } +`; + +const StyledSearchIcon = styled(SearchIcon)` + position: absolute; + top: 50%; + left: 10px; + transform: translateY(-50%); + width: 20px; + height: 20px; + fill: #ccc; +`; diff --git a/frontend/src/hooks/useNavigator.ts b/frontend/src/hooks/useNavigator.ts index 77f76cde..b6021d52 100644 --- a/frontend/src/hooks/useNavigator.ts +++ b/frontend/src/hooks/useNavigator.ts @@ -27,6 +27,7 @@ const useNavigator = () => { routePage('/new-pin', topicId); closeModal('addMapOrPin'); }, + search: (searchTerm: string) => routePage(`/search?${searchTerm}`), goToPopularTopics: () => routePage('see-all/popularity'), goToNearByMeTopics: () => routePage('see-all/near'), goToLatestTopics: () => routePage('see-all/latest'), diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 5ca139b9..f8c32e76 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,6 +1,6 @@ import Space from '../components/common/Space'; import useNavigator from '../hooks/useNavigator'; -import { css, styled } from 'styled-components'; +import { styled } from 'styled-components'; import useSetLayoutWidth from '../hooks/useSetLayoutWidth'; import { FULLSCREEN } from '../constants'; import useSetNavbarHighlight from '../hooks/useSetNavbarHighlight'; @@ -8,6 +8,7 @@ import { Suspense, lazy, useContext, useEffect } from 'react'; import { MarkerContext } from '../context/MarkerContext'; import TopicCardContainerSkeleton from '../components/Skeletons/TopicListSkeleton'; import { setFullScreenResponsive } from '../constants/responsive'; +import SearchBar from '../components/SearchBar/SearchBar'; const TopicListContainer = lazy( () => import('../components/TopicCardContainer'), @@ -33,7 +34,9 @@ const Home = () => { return ( - + + + }> { + const { fetchGet } = useGet(); + + const [originalTopics, setOriginalTopics] = useState( + null, + ); + const [displayedTopics, setDisplayedTopics] = useState< + TopicCardProps[] | null + >(null); + const searchQuery = decodeURIComponent(useLocation().search.substring(1)); + + const getTopicsFromServer = async () => { + fetchGet( + '/topics', + '지도를 가져오는데 실패했습니다.', + (response) => { + setOriginalTopics(response); + const searchResult = response.filter((topic) => + topic.name.includes(searchQuery), + ); + setDisplayedTopics(searchResult); + }, + ); + }; + + useEffect(() => { + getTopicsFromServer(); + }, []); + + useEffect(() => { + if (originalTopics) { + const searchResult = originalTopics.filter((topic) => + topic.name.includes(searchQuery), + ); + setDisplayedTopics(searchResult); + } + }, [searchQuery]); + + return ( + + + + + + + + 찾았을 지도? + + + + 검색한 지도를 확인해보세요. + + + + + + {displayedTopics?.length === 0 ? ( + // 검색 결과가 없을 때의 UI + + + + + '{searchQuery}'에 대한 + {'검색 결과가 없습니다.'} + + + + + + ) : ( + + {displayedTopics?.map((topic) => ( + + + + ))} + + )} + + ); +}; + +export default Search; + +const Wrapper = styled.article` + width: 1036px; + margin: 0 auto; + position: relative; + + ${setFullScreenResponsive()} +`; + +const CardListWrapper = styled.ul` + display: flex; + flex-wrap: wrap; + gap: 20px; +`; + +const EmptyWrapper = styled.section` + height: 240px; + display: flex; + flex-direction: column; + align-items: center; +`; diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 7f5833bd..55204a63 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -5,6 +5,7 @@ import RootPage from './pages/RootPage'; import { ReactNode } from 'react'; import AuthLayout from './components/Layout/AuthLayout'; import NotFound from './pages/NotFound'; +import Search from './pages/Search'; const SelectedTopic = lazy(() => import('./pages/SelectedTopic')); const NewPin = lazy(() => import('./pages/NewPin')); @@ -147,6 +148,11 @@ const routes: routeElement[] = [ ), withAuth: false, }, + { + path: '/search', + element: , + withAuth: false, + }, ], }, ];