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

Feature: Search page 구현 #23

Merged
merged 11 commits into from
Aug 7, 2024
19 changes: 19 additions & 0 deletions src/assets/icons/SmallStar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const SmallStar = () => {
return (
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="12" height="12" fill="white" />
<path
d="M10.6418 4.13731L7.6664 3.70489L6.33632 1.0084C6.29999 0.934575 6.24023 0.874809 6.1664 0.838481C5.98124 0.747075 5.75624 0.823247 5.66366 1.0084L4.33358 3.70489L1.35819 4.13731C1.27616 4.14903 1.20116 4.1877 1.14374 4.24629C1.07432 4.31764 1.03607 4.41364 1.03739 4.51318C1.0387 4.61272 1.07949 4.70767 1.15077 4.77715L3.30351 6.87598L2.79491 9.83965C2.78299 9.90859 2.79062 9.9795 2.81694 10.0443C2.84325 10.1092 2.88721 10.1653 2.94382 10.2064C3.00043 10.2475 3.06743 10.272 3.13722 10.2769C3.20701 10.2819 3.27679 10.2672 3.33866 10.2346L5.99999 8.83536L8.66132 10.2346C8.73398 10.2732 8.81835 10.2861 8.89921 10.2721C9.10312 10.2369 9.24023 10.0436 9.20507 9.83965L8.69648 6.87598L10.8492 4.77715C10.9078 4.71973 10.9465 4.64473 10.9582 4.5627C10.9898 4.35762 10.8469 4.16778 10.6418 4.13731Z"
fill="#FC6B02"
/>
</svg>
);
};

export default SmallStar;
18 changes: 9 additions & 9 deletions src/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const Map: React.FC<MapProps> = ({ onClick }) => {
if (!isMapLoaded) {
return <Loading />;
}

return (
<MapDiv
style={{
Expand All @@ -38,14 +37,15 @@ const Map: React.FC<MapProps> = ({ onClick }) => {
defaultCenter={new navermaps.LatLng(37.3595704, 127.105399)}
defaultZoom={15}
>
{restaurants.map((restaurantData, index) => (
<CustomMarker
key={index}
navermaps={navermaps}
lat={restaurantData.restaurant.latitude}
lng={restaurantData.restaurant.longitude}
/>
))}
{restaurants &&
restaurants.map((restaurant, index) => (
<CustomMarker
key={index}
navermaps={navermaps}
lat={restaurant.latitude}
lng={restaurant.longitude}
/>
))}
</NaverMap>
</MapDiv>
);
Expand Down
13 changes: 5 additions & 8 deletions src/components/bottomSheet/BottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type BottomSheetProps = {
onClose: () => void;
};

const BottomSheetWrapper = styled.div<{ translatey: number }>`
const BottomSheetWrapper = styled.div<{ $translateY: number }>`
position: fixed;
bottom: 0;
z-index: 100;
Expand All @@ -22,7 +22,7 @@ const BottomSheetWrapper = styled.div<{ translatey: number }>`
max-height: 80vh;

overflow-y: auto;
transform: translateY(${({ translatey: translateY }) => translateY}px);
transform: translateY(${({ $translateY }) => $translateY}px);
`;

const Handle = styled.div`
Expand All @@ -45,15 +45,12 @@ const BottomSheet: React.FC<BottomSheetProps> = ({ onClose }) => {
const [restaurants] = useAtom(restaurantAtom);

return (
<BottomSheetWrapper translatey={translateY}>
<BottomSheetWrapper $translateY={translateY}>
<Handle onMouseDown={handleMouseDown} />
<BottomSheetContent>
<ul>
{restaurants.map((userRestaurant) => (
<RestaurantSummary
key={userRestaurant.id}
restaurant={userRestaurant.restaurant}
/>
{restaurants.map((restaurant) => (
<RestaurantSummary key={restaurant.id} restaurant={restaurant} />
))}
</ul>
</BottomSheetContent>
Expand Down
94 changes: 70 additions & 24 deletions src/components/map/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { useState } from 'react';
import { useSetAtom } from 'jotai';
import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { styled } from 'styled-components';
import NavBackIcon from '~/assets/icons/NavBackIcon';
import QuestionIcon from '~/assets/icons/QuestionIcon';
import XIcon from '~/assets/icons/XIcon';
import SearchContents from '~/components/map/SearchContents';
import usePostSearch from '~/hooks/api/search/usePostSearch';
import { searchRestaurantAtom } from '~/store/restaurants';

type SearchBarProps = {
bottomSheetClose: () => void;
Expand All @@ -29,7 +32,23 @@ const SearchContainer = styled.div`
filter: drop-shadow(0 4px 20px rgba(0, 0, 0, 0.1));
`;

const ClearButton = styled.button`
const SearchForm = styled.form`
width: 100%;

display: flex;
align-items: center;
`;

const SubmitButton = styled.button`
border: none;
cursor: pointer;
background-color: ${({ theme }) => theme.colors.white};
color: ${({ theme }) => theme.colors.black};
padding: 5px 10px;
border-radius: 5px;
`;

const ClearButton = styled.div`
border: none;
cursor: pointer;
background-color: transparent;
Expand All @@ -47,25 +66,37 @@ const Input = styled.input`
outline: none;
}
`;
const NavBackButton = styled.button`
const NavBackLink = styled.a`
border: none;
cursor: pointer;
background-color: transparent;
`;

const SearchBar: React.FC<SearchBarProps> = ({ bottomSheetClose }) => {
const [searchText, setSearchText] = useState<string>('');
const location = useLocation();
const [searchText, setSearchText] = useState<string>('');
const [isOverlayVisible, setOverlayVisible] = useState<boolean>(
location.pathname === '/search',
);
const setSearchRestaurants = useSetAtom(searchRestaurantAtom);

const { data, refetch } = usePostSearch({ query: searchText });
useEffect(() => {
if (data) {
setSearchRestaurants(data.results);
}
}, [data, setSearchRestaurants]);

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchText(e.target.value);
if (e.target.value === '') {
clearInput();
}
};

const handleBackButtonClick = () => {
const handleBackLinkClick = () => {
setOverlayVisible(false);
clearInput();
window.history.pushState(null, '', '/');
};

Expand All @@ -77,31 +108,46 @@ const SearchBar: React.FC<SearchBarProps> = ({ bottomSheetClose }) => {

const clearInput = () => {
setSearchText('');
setSearchRestaurants([]);
};

const handleSubmit = (e?: React.FormEvent<HTMLFormElement>) => {
e && e.preventDefault();
// searchText가 비어 있는 경우 refetch를 하지 않음
if (searchText.trim() !== '') {
refetch();
}
};

return (
<>
<SearchContainer>
{isOverlayVisible ? (
<NavBackButton onClick={handleBackButtonClick}>
<NavBackIcon />
</NavBackButton>
) : (
<QuestionIcon />
)}
<Input
type="text"
value={searchText}
onChange={handleInputChange}
onFocus={handeInputFocus}
/>
{searchText && (
<ClearButton onClick={clearInput}>
<XIcon />
</ClearButton>
)}
<SearchForm onSubmit={handleSubmit}>
{isOverlayVisible && (
<NavBackLink onClick={handleBackLinkClick}>
<NavBackIcon />
</NavBackLink>
)}
<Input
type="text"
value={searchText}
onChange={handleInputChange}
onFocus={handeInputFocus}
/>
{searchText && (
<ClearButton onClick={clearInput}>
<XIcon />
</ClearButton>
)}
<SubmitButton type="submit">
<QuestionIcon />
</SubmitButton>
</SearchForm>
</SearchContainer>
<SearchContents isVisible={isOverlayVisible} />
<SearchContents
$isVisible={isOverlayVisible}
textSetter={(historyQuery: string) => setSearchText(historyQuery)}
/>
</>
);
};
Expand Down
96 changes: 91 additions & 5 deletions src/components/map/SearchContents.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,109 @@
import { useEffect, useState } from 'react';
import { styled } from 'styled-components';
import HistoryLine from '~/components/search/HistoryLine';
import useGetSearch from '~/hooks/api/search/useGetSearch';
import { History } from '~/types/search';
import { useAtom } from 'jotai';
import SearchResultLine from '~/components/search/SearchResultLine';
import { searchRestaurantAtom } from '~/store/restaurants';

type SearchContentsProps = {
isVisible: boolean;
$isVisible: boolean;
textSetter: (text: string) => void;
};

const Overlay = styled.div<{ isVisible: boolean }>`
const Overlay = styled.div<{ $isVisible: boolean }>`
z-index: 99;
position: fixed;
top: 0;
left: 50%;
transform: translate(-50%, 0);
width: 400px;
height: calc(100% - 56px);
background-color: ${({ theme }) => theme.colors.whitegray};
display: ${({ $isVisible }) => ($isVisible ? 'block' : 'none')};
`;

const HistoryWrapper = styled.ul`
margin-top: 100px;
padding: 40px 0;
height: 100%;

display: flex;
flex-direction: column;
align-items: center;

gap: 12px;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
background-color: ${({ theme }) => theme.colors.white};
display: ${({ isVisible }) => (isVisible ? 'block' : 'none')};
`;

const SearchContents: React.FC<SearchContentsProps> = ({ isVisible }) => {
return <Overlay isVisible={isVisible}>SearchContents</Overlay>;
const OverflowBox = styled.div`
width: 100%;
padding-bottom: 56px;
overflow-y: auto;
::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
`;

const EmptyHistory = styled.div`
padding: 20px;
text-align: center;
font-size: 14px;
font-weight: ${({ theme }) => theme.fontWeights.Regular};
`;

const SearchContents: React.FC<SearchContentsProps> = ({
$isVisible,
textSetter,
}) => {
const [histories, setHistories] = useState<History[]>([]);
const [searchRestaurants] = useAtom(searchRestaurantAtom);
const { data } = useGetSearch();
useEffect(() => {
if (data) {
setHistories(data.histories);
}
}, [data, setHistories]);

const removeHistory = (id: number) => {
setHistories(histories.filter((history) => history.id !== id));
};

return (
<Overlay $isVisible={$isVisible}>
<HistoryWrapper>
<OverflowBox>
{searchRestaurants.length > 0 ? (
searchRestaurants.map((result) => (
<SearchResultLine key={result.id} {...result} />
))
) : histories.length > 0 ? (
// restaurant가 비어있고 histories가 있으면 이 컴포넌트를 보여줍니다.
<>
{histories.map((history) => (
<HistoryLine
key={history.id}
setFn={() => {
textSetter(history.query);
}}
removeHistory={removeHistory}
{...history}
/>
))}
</>
) : (
// 둘 다 없으면 이 메시지를 보여줍니다.
<EmptyHistory>검색 기록이 없습니다.</EmptyHistory>
)}
</OverflowBox>
</HistoryWrapper>
</Overlay>
);
};

export default SearchContents;
2 changes: 1 addition & 1 deletion src/components/navBar/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const NavBar = () => {
<Nav>
<NavList>
{navItems.map(({ path, label, Icon }) => (
<NavItemWrapper key={path} isActive={pathname === path} link={path}>
<NavItemWrapper key={path} $isActive={pathname === path} link={path}>
<Icon />
{label}
</NavItemWrapper>
Expand Down
Loading
Loading