diff --git a/src/components/Detail/Main/Swiper/Swiper.module.scss b/src/components/Detail/Main/Swiper/Swiper.module.scss new file mode 100644 index 00000000..1ef3a55e --- /dev/null +++ b/src/components/Detail/Main/Swiper/Swiper.module.scss @@ -0,0 +1,10 @@ +.container { + width: 100%; + height: 24.8rem; + + display: flex; + justify-content: center; + align-items: center; + + background-color: #efefef; +} diff --git a/src/components/Detail/Main/Swiper/Swiper.tsx b/src/components/Detail/Main/Swiper/Swiper.tsx new file mode 100644 index 00000000..36964a60 --- /dev/null +++ b/src/components/Detail/Main/Swiper/Swiper.tsx @@ -0,0 +1,7 @@ +import styles from "./Swiper.module.scss"; + +function Swiper() { + return
Swiper
; +} + +export default Swiper; diff --git a/src/components/Home/RecommendedItemList/RecommendedItem/RecommendedItem.tsx b/src/components/Home/RecommendedItemList/RecommendedItem/RecommendedItem.tsx index 2dd42b14..956af031 100644 --- a/src/components/Home/RecommendedItemList/RecommendedItem/RecommendedItem.tsx +++ b/src/components/Home/RecommendedItemList/RecommendedItem/RecommendedItem.tsx @@ -10,32 +10,27 @@ interface PropsType { } function RecommendedItem(data: PropsType) { - const linkURL = `/detail/${data.data.id}`; - const imageAlt = `${data.data.title}의 사진`; - return ( -
- - {imageAlt} -
- {data.data.title} - {data.data.location} -
-

- - - - {data.data.score} -

- ({data.data.reviewNumber}) -
+ + {`${data.data.title}의 +
+ {data.data.title} + {data.data.location} +
+

+ + + + {data.data.score} +

+ ({data.data.reviewNumber})
- -
+
+ ); } diff --git a/src/components/Home/SearchBarAtHome/SearchBarAtHome.tsx b/src/components/Home/SearchBarAtHome/SearchBarAtHome.tsx index 4f994c91..cab6819e 100644 --- a/src/components/Home/SearchBarAtHome/SearchBarAtHome.tsx +++ b/src/components/Home/SearchBarAtHome/SearchBarAtHome.tsx @@ -6,7 +6,7 @@ import styles from "./SearchBarAtHome.module.scss"; function SearchBarAtHome() { return (
- + 어디로 떠나시나요?

diff --git a/src/components/Home/TabBar/TabBar.tsx b/src/components/Home/TabBar/TabBar.tsx index 065f7cb6..c28f647b 100644 --- a/src/components/Home/TabBar/TabBar.tsx +++ b/src/components/Home/TabBar/TabBar.tsx @@ -8,7 +8,7 @@ function TabBar() { return (

- + diff --git a/src/components/Review/Review.module.scss b/src/components/Review/Review.module.scss new file mode 100644 index 00000000..7b74f1d8 --- /dev/null +++ b/src/components/Review/Review.module.scss @@ -0,0 +1,47 @@ +@use "@/sass" as *; + +.container { + display: flex; + gap: 8px; + padding: 24px 20px; + border-bottom: 1px solid $neutral100; + + &__contentsBox { + width: 100%; + + display: flex; + flex-direction: column; + gap: 4px; + + &__name { + @include typography(button); + } + + &__secondItems { + display: flex; + align-items: center; + + &__star { + display: inline; + font-size: 1.6rem; + color: $etc0; + } + + &__point { + margin-left: 2px; + @include typography(captionSmall); + } + + &__visitedAt { + margin-left: 9px; + color: $primary300; + @include typography(captionSmall); + } + } + + &__content { + @include typography(bodySmall); + text-overflow: ellipsis; + } + } +} diff --git a/src/components/Review/Review.tsx b/src/components/Review/Review.tsx new file mode 100644 index 00000000..dae9dccc --- /dev/null +++ b/src/components/Review/Review.tsx @@ -0,0 +1,52 @@ +import { Avatar } from "@chakra-ui/react"; +import { GoStarFill } from "react-icons/go"; + +import styles from "./Review.module.scss"; + +import ReviewImageSlider from "./ReviewImageSlider/ReviewImageSlider"; + +import { ReviewPropsTypes } from "@/types/detail"; + +function Review({ + name, + isGoogle = false, + point, + visitedAt, + content, + images, +}: ReviewPropsTypes) { + return ( +
+
+ +
+
+
+ {name} + {isGoogle && 구글} +
+
+ + + {point} + + + {visitedAt} + +
+
{content}
+ {images && } +
+
+ ); +} + +export default Review; diff --git a/src/components/Review/ReviewImageSlider/ReviewImageSlider.module.scss b/src/components/Review/ReviewImageSlider/ReviewImageSlider.module.scss new file mode 100644 index 00000000..0f7c7303 --- /dev/null +++ b/src/components/Review/ReviewImageSlider/ReviewImageSlider.module.scss @@ -0,0 +1,56 @@ +.container { + overflow: hidden; + position: relative; + + &__imgWrapper { + position: relative; + display: flex; + gap: 8px; + + transition: all 1s; + img { + width: 7.6rem; + height: 7.6rem; + } + } + + &__leftBtn { + position: absolute; + top: 50%; + left: 10px; + transform: translate(-10px, -50%); + + width: 2.4rem; + height: 2.2rem; + border-radius: 1.2rem; + + background-color: #ffffff; + z-index: 2; + &__icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } + + &__rightBtn { + position: absolute; + top: 50%; + right: 10px; + transform: translate(-10px, -50%); + + width: 2.4rem; + height: 2.4rem; + border-radius: 1.2rem; + + background-color: #ffffff; + z-index: 2; + &__icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + } +} diff --git a/src/components/Review/ReviewImageSlider/ReviewImageSlider.tsx b/src/components/Review/ReviewImageSlider/ReviewImageSlider.tsx new file mode 100644 index 00000000..07db63a9 --- /dev/null +++ b/src/components/Review/ReviewImageSlider/ReviewImageSlider.tsx @@ -0,0 +1,50 @@ +import { useState } from "react"; +import { AiOutlineLeft, AiOutlineRight } from "react-icons/ai"; + +import styles from "./ReviewImageSlider.module.scss"; + +import useComponentSize from "@/hooks/useComponetSize"; + +function ReviewImageSlider({ images }: { images: string[] }) { + const [slideLocation, setSlideLocation] = useState(0); + const [componentRef, size] = useComponentSize(); + + return ( +
+
{ + setSlideLocation(slideLocation + 84); + }} + > + +
+
= 84 * images.length - 1 - size.width + ? "none" + : "block", + }} + onClick={() => { + setSlideLocation(slideLocation - 84); + }} + > + +
+
+ {images.map((data) => ( + + ))} +
+
+ ); +} + +export default ReviewImageSlider; diff --git a/src/components/SearchFromHome/SearchBar/SearchBar.module.scss b/src/components/SearchFromHome/SearchBar/SearchBar.module.scss new file mode 100644 index 00000000..c0da7272 --- /dev/null +++ b/src/components/SearchFromHome/SearchBar/SearchBar.module.scss @@ -0,0 +1,66 @@ +@use "@/sass" as *; + +@mixin icon { + position: absolute; + top: calc(50% - 1.5px); + + width: 24px; + height: 24px; + + font-size: 24px; + + transform: translateY(-50%); +} + +.container { + width: 100%; + height: 4.8rem; + + padding: 0 20px 8px 20px; + + .search_container { + position: relative; + + padding: 5px 32px 8px 32px; + + .bottom_line { + position: absolute; + bottom: 0; + left: 0; + + width: 100%; + height: 2px; + + background-color: $primary300; + } + } + + .searchBar { + position: relative; + + width: 100%; + height: 100%; + + @include typography(subTitle); + + &__input { + width: 100%; + &:focus { + outline: none; + } + } + } + + .return { + left: 0; + + @include icon; + } + .enter { + right: 0; + + @include icon; + + cursor: pointer; + } +} diff --git a/src/components/SearchFromHome/SearchBar/SearchBar.tsx b/src/components/SearchFromHome/SearchBar/SearchBar.tsx new file mode 100644 index 00000000..86e0017f --- /dev/null +++ b/src/components/SearchFromHome/SearchBar/SearchBar.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; + +import styles from "./SearchBar.module.scss"; + +interface PropsType { + set: React.Dispatch>; + keyword: string | undefined; +} + +function SearchBar({ set, keyword }: PropsType) { + const [inputValue, setInputValue] = useState(""); + + useEffect(() => { + if (keyword) { + setInputValue(keyword); + } + }, [keyword]); + + function handleInputValue(e: React.ChangeEvent) { + setInputValue(e.target.value); + } + + function search() { + set(inputValue); + } + + return ( +
+
+
+ { + if (e.key === "Enter") { + search(); + } + }} + /> +
+ + + + + +

+ + + +

+
+
+
+ ); +} + +export default SearchBar; diff --git a/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.module.scss b/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.module.scss new file mode 100644 index 00000000..ab073fac --- /dev/null +++ b/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.module.scss @@ -0,0 +1,25 @@ +@use "@/sass" as *; + +.container { + display: flex; + flex-direction: column; + gap: 8px; + + img { + max-width: 10.6rem; + max-height: 10.6rem; + + border-radius: 1.6rem; + } + .text_box { + display: flex; + flex-direction: column; + } + .title { + @include typography(button); + } + .location { + color: $neutral400; + @include typography(captionSmall); + } +} diff --git a/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.tsx b/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.tsx new file mode 100644 index 00000000..55a67f4d --- /dev/null +++ b/src/components/SearchFromHome/SearchHome/HotItems/HotItem/HotItem.tsx @@ -0,0 +1,23 @@ +import { Link } from "react-router-dom"; + +import styles from "./HotItem.module.scss"; + +import { SearchHotItemType } from "@/types/home"; + +interface PropsData { + data: SearchHotItemType; +} + +function HotItem({ data }: PropsData) { + return ( + + {`${data.title}의 +

+ {data.title} + {data.location} +

+ + ); +} + +export default HotItem; diff --git a/src/components/SearchFromHome/SearchHome/HotItems/HotItems.module.scss b/src/components/SearchFromHome/SearchHome/HotItems/HotItems.module.scss new file mode 100644 index 00000000..ba1ada99 --- /dev/null +++ b/src/components/SearchFromHome/SearchHome/HotItems/HotItems.module.scss @@ -0,0 +1,10 @@ +@use "@/sass" as *; + +.container { + @include slide_button_container; + + .slide_box { + @include slide_list_container; + gap: 8px; + } +} diff --git a/src/components/SearchFromHome/SearchHome/HotItems/HotItems.tsx b/src/components/SearchFromHome/SearchHome/HotItems/HotItems.tsx new file mode 100644 index 00000000..fcb7c47f --- /dev/null +++ b/src/components/SearchFromHome/SearchHome/HotItems/HotItems.tsx @@ -0,0 +1,55 @@ +import { useEffect, useState } from "react"; + +import styles from "./HotItems.module.scss"; + +import useComponentSize from "@/hooks/useComponetSize"; + +import SlideButton from "@/components/SlideButton/SlideButton"; + +import { getData } from "@/mocks/handlers/home"; + +import HotItem from "./HotItem/HotItem"; + +import { SearchHotItemType } from "@/types/home"; + +interface PropsType { + type: string; +} + +function HotItems({ type }: PropsType) { + const [data, setData] = useState(); + const [slideLocation, setSlideLocation] = useState(0); + const [componentRef, size] = useComponentSize(); + + useEffect(() => { + getData(`home/search/hot${type}`, setData); + }, [type]); + + return ( +
+ {data && ( + + )} +
+ {data && + data.map((data, i) => )} +
+
+ ); +} + +export default HotItems; diff --git a/src/components/SearchFromHome/SearchHome/SearchHome.module.scss b/src/components/SearchFromHome/SearchHome/SearchHome.module.scss new file mode 100644 index 00000000..6e9732e5 --- /dev/null +++ b/src/components/SearchFromHome/SearchHome/SearchHome.module.scss @@ -0,0 +1,26 @@ +@use "@/sass" as *; + +.lists_box { + display: flex; + flex-direction: column; + gap: 48px; + + .title { + @include typography(titleMedium); + color: #1d2433; + + padding: 8px 20px; + } + + .column_4px { + display: flex; + flex-direction: column; + gap: 4px; + } + + .column_8px { + display: flex; + flex-direction: column; + gap: 8px; + } +} diff --git a/src/components/SearchFromHome/SearchHome/SearchHome.tsx b/src/components/SearchFromHome/SearchHome/SearchHome.tsx new file mode 100644 index 00000000..85c1784c --- /dev/null +++ b/src/components/SearchFromHome/SearchHome/SearchHome.tsx @@ -0,0 +1,29 @@ +import styles from "./SearchHome.module.scss"; + +import HotItems from "./HotItems/HotItems"; +import SearchKeyword from "./SearchKeyword/SearchKeyword"; + +interface PropsType { + set: React.Dispatch>; +} + +function SearchHome({ set }: PropsType) { + return ( +
+
+

인기 검색 키워드

+ +
+
+

최근 30일간 인기 장소

+ +
+
+

최근 30일간 인기 장소

+ +
+
+ ); +} + +export default SearchHome; diff --git a/src/components/SearchFromHome/SearchHome/SearchKeyword/SearchKeyword.module.scss b/src/components/SearchFromHome/SearchHome/SearchKeyword/SearchKeyword.module.scss new file mode 100644 index 00000000..e50c9a10 --- /dev/null +++ b/src/components/SearchFromHome/SearchHome/SearchKeyword/SearchKeyword.module.scss @@ -0,0 +1,27 @@ +@use "@/sass" as *; + +.container { + @include slide_button_container; + + .slide_box { + @include slide_list_container; + gap: 8px; + + p { + height: 3.8rem; + + padding: 8px 16px; + + border-radius: 48px; + + color: $primary300; + background-color: $primary100; + + white-space: nowrap; + + cursor: pointer; + + @include typography(button); + } + } +} diff --git a/src/components/SearchFromHome/SearchHome/SearchKeyword/SearchKeyword.tsx b/src/components/SearchFromHome/SearchHome/SearchKeyword/SearchKeyword.tsx new file mode 100644 index 00000000..4e84c883 --- /dev/null +++ b/src/components/SearchFromHome/SearchHome/SearchKeyword/SearchKeyword.tsx @@ -0,0 +1,89 @@ +import { useEffect, useState } from "react"; + +import styles from "./SearchKeyword.module.scss"; + +import useComponentSize from "@/hooks/useComponetSize"; + +import SlideButton from "@/components/SlideButton/SlideButton"; + +import { getData } from "@/mocks/handlers/home"; + +interface PropsType { + set: React.Dispatch>; +} + +function SearchKeyword({ set }: PropsType) { + const [data, setData] = useState(); + const [listWidth, setListWidth] = useState(0); + const [slideLocation, setSlideLocation] = useState(0); + const [componentRef, size] = useComponentSize(); + + useEffect(() => { + getData("home/search/keyword", setData); + }, []); + + // 각 키워드의 너비를 모두 더한 값을 구함 + useEffect(() => { + // 정확한 이유를 찾지 못하였으나 setTimeout을 걸지 않으면 각 p태그의 width가 실제보다 다소 큰 수가 반영됨 + setTimeout(() => { + if ( + data && + componentRef.current && + componentRef.current?.childNodes.length === data.length + ) { + const pTags = componentRef.current.querySelectorAll("p"); + const widths = Array.from(pTags).map((pTag) => { + const rect = pTag.getBoundingClientRect(); + return rect.width; + }); + const width = widths.reduce((acc, width) => acc + width, 0); + setListWidth(width); + } + }, 100); + }, [data, componentRef]); + + function searchKeyword(keyword: string) { + set(keyword); + } + + return ( +
+ {data && ( + + )} +
+ {data ? ( + data.map((keyword, i) => ( +

{ + searchKeyword(keyword); + }} + > + {keyword} +

+ )) + ) : ( +

키워드가 없습니다.

+ )} +
+
+ ); +} + +export default SearchKeyword; diff --git a/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.module.scss b/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.module.scss new file mode 100644 index 00000000..e90ca855 --- /dev/null +++ b/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.module.scss @@ -0,0 +1,20 @@ +@use "@/sass" as *; + +.container { + width: 7.9rem; + height: 2.4rem; + + display: flex; + align-items: center; + + padding: 4px 8px; + + cursor: pointer; + + @include typography(button); + + .icon { + width: 2.4rem; + height: 2.4rem; + } +} diff --git a/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.tsx b/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.tsx new file mode 100644 index 00000000..ccae0fe7 --- /dev/null +++ b/src/components/SearchFromHome/SearchList/DateFilter/DateFilter.tsx @@ -0,0 +1,14 @@ +import { BsFilterLeft } from "react-icons/bs"; + +import styles from "./DateFilter.module.scss"; + +function DateFilter() { + return ( +
+ + 등록순 +
+ ); +} + +export default DateFilter; diff --git a/src/components/SearchFromHome/SearchList/LocationFilter/LocationFilter.module.scss b/src/components/SearchFromHome/SearchList/LocationFilter/LocationFilter.module.scss new file mode 100644 index 00000000..036adae2 --- /dev/null +++ b/src/components/SearchFromHome/SearchList/LocationFilter/LocationFilter.module.scss @@ -0,0 +1,28 @@ +@use "@/sass" as *; + +.container { + width: 9.1rem; + height: 3rem; + + display: flex; + align-items: center; + gap: 2px; + + padding: 4px 4px 4px 10px; + + border: 1px solid $neutral300; + border-radius: 16px; + + @include typography(tabLabel); + + cursor: pointer; + + .icon { + width: 2rem; + height: 2rem; + + display: flex; + justify-content: center; + align-items: center; + } +} diff --git a/src/components/SearchFromHome/SearchList/LocationFilter/LocationFilter.tsx b/src/components/SearchFromHome/SearchList/LocationFilter/LocationFilter.tsx new file mode 100644 index 00000000..e4b8e946 --- /dev/null +++ b/src/components/SearchFromHome/SearchList/LocationFilter/LocationFilter.tsx @@ -0,0 +1,14 @@ +import { MdOutlineKeyboardArrowDown } from "react-icons/md"; + +import styles from "./LocationFilter.module.scss"; + +function LocationFilter() { + return ( +
+ 전체 지역 + +
+ ); +} + +export default LocationFilter; diff --git a/src/components/SearchFromHome/SearchList/MapButton/MapButton.module.scss b/src/components/SearchFromHome/SearchList/MapButton/MapButton.module.scss new file mode 100644 index 00000000..e7ef1ead --- /dev/null +++ b/src/components/SearchFromHome/SearchList/MapButton/MapButton.module.scss @@ -0,0 +1,34 @@ +@use "@/sass" as *; + +.container { + position: fixed; + left: 50%; + bottom: 24px; + transform: translateX(-50%); + + width: 18.3rem; + height: 5.4rem; + + padding: 12px 32px; + + border-radius: 4.8rem; + + display: flex; + justify-content: center; + align-items: center; + + background-color: $primary300; + color: $neutral0; + + @include typography(button); + + .text { + display: flex; + gap: 4px; + + .icon { + width: 2rem; + height: 2rem; + } + } +} diff --git a/src/components/SearchFromHome/SearchList/MapButton/MapButton.tsx b/src/components/SearchFromHome/SearchList/MapButton/MapButton.tsx new file mode 100644 index 00000000..73539784 --- /dev/null +++ b/src/components/SearchFromHome/SearchList/MapButton/MapButton.tsx @@ -0,0 +1,16 @@ +import { MdOutlineMap } from "react-icons/md"; + +import styles from "./MapButton.module.scss"; + +function MapButton() { + return ( + + ); +} + +export default MapButton; diff --git a/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.module.scss b/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.module.scss new file mode 100644 index 00000000..0018e0d9 --- /dev/null +++ b/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.module.scss @@ -0,0 +1,32 @@ +@use "@/sass" as *; + +.container { + width: 100%; + max-height: 70px; + + display: flex; + gap: 12px; + + padding: 16px 0; + + img { + width: 4rem; + height: 4rem; + + border-radius: 0.8rem; + } + + .text { + display: flex; + flex-direction: column; + + .title { + @include typography(titleSmall); + } + .info { + color: $neutral400; + + @include typography(captionSmall); + } + } +} diff --git a/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.tsx b/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.tsx new file mode 100644 index 00000000..e23fae02 --- /dev/null +++ b/src/components/SearchFromHome/SearchList/SearchItem/SearchItem.tsx @@ -0,0 +1,25 @@ +import { Link } from "react-router-dom"; + +import styles from "./SearchItem.module.scss"; + +import { SearchItemType } from "@/types/home"; + +interface PropsType { + data: SearchItemType; +} + +function SearchItem({ data }: PropsType) { + return ( + + {`${data.title}의 +

+ {data.title} + + {data.category}·{data.location} + +

+ + ); +} + +export default SearchItem; diff --git a/src/components/SearchFromHome/SearchList/SearchList.module.scss b/src/components/SearchFromHome/SearchList/SearchList.module.scss new file mode 100644 index 00000000..74ab4d7b --- /dev/null +++ b/src/components/SearchFromHome/SearchList/SearchList.module.scss @@ -0,0 +1,22 @@ +@use "@/sass" as *; + +.container { + width: 100%; + + display: flex; + flex-direction: column; + gap: 24px; + + @include typography(button); + + .filter { + display: flex; + justify-content: space-between; + align-items: center; + + padding: 0 20px; + } + ul { + padding: 0 20px; + } +} diff --git a/src/components/SearchFromHome/SearchList/SearchList.tsx b/src/components/SearchFromHome/SearchList/SearchList.tsx new file mode 100644 index 00000000..d60829ac --- /dev/null +++ b/src/components/SearchFromHome/SearchList/SearchList.tsx @@ -0,0 +1,52 @@ +import { useEffect, useState } from "react"; + +import styles from "./SearchList.module.scss"; + +import { getData } from "@/mocks/handlers/home"; + +import DateFilter from "./DateFilter/DateFilter"; +import LocationFilter from "./LocationFilter/LocationFilter"; +import MapButton from "./MapButton/MapButton"; +import SearchItem from "./SearchItem/SearchItem"; +import Tabs from "./Tabs/Tabs"; + +import { SearchItemType } from "@/types/home"; + +function SearchList({ keyword }: { keyword: string }) { + const [data, setData] = useState(); + const [filterData, setFilterData] = useState(); + const [category, setCategory] = useState("전체"); + + useEffect(() => { + setCategory("전체"); + getData("home/search/search", setData); + }, [keyword]); + + useEffect(() => { + if (data && category !== "전체") { + const filterData = data.filter((data) => data.category === category); + setFilterData(filterData); + } else { + setFilterData(data); + } + }, [data, category]); + + return ( +
+ +
+ + +
+
    + {filterData && + filterData.map((data, i) => ( + + ))} +
+ +
+ ); +} + +export default SearchList; diff --git a/src/components/SearchFromHome/SearchList/Tabs/Tab/Tab.module.scss b/src/components/SearchFromHome/SearchList/Tabs/Tab/Tab.module.scss new file mode 100644 index 00000000..08882f61 --- /dev/null +++ b/src/components/SearchFromHome/SearchList/Tabs/Tab/Tab.module.scss @@ -0,0 +1,13 @@ +@use "@/sass" as *; +.container { + min-width: 90px; + height: 33px; + + transition: 0.5s color; + + display: flex; + justify-content: center; + align-items: center; + + cursor: pointer; +} diff --git a/src/components/SearchFromHome/SearchList/Tabs/Tab/Tab.tsx b/src/components/SearchFromHome/SearchList/Tabs/Tab/Tab.tsx new file mode 100644 index 00000000..7946fa0a --- /dev/null +++ b/src/components/SearchFromHome/SearchList/Tabs/Tab/Tab.tsx @@ -0,0 +1,31 @@ +import styles from "./Tab.module.scss"; + +interface PropsType { + set: React.Dispatch>; + category: string; + thisCategory: string; +} + +function Tab({ set, category, thisCategory }: PropsType) { + function handleCategory(key: string) { + set(key); + } + + return ( +

{ + handleCategory(thisCategory); + }} + > + {thisCategory} +

+ ); +} + +export default Tab; diff --git a/src/components/SearchFromHome/SearchList/Tabs/Tabs.module.scss b/src/components/SearchFromHome/SearchList/Tabs/Tabs.module.scss new file mode 100644 index 00000000..909e2ec1 --- /dev/null +++ b/src/components/SearchFromHome/SearchList/Tabs/Tabs.module.scss @@ -0,0 +1,11 @@ +@use "@/sass" as *; + +.container { + color: $neutral300; + + @include slide_button_container; + + .tabs { + @include slide_list_container; + } +} diff --git a/src/components/SearchFromHome/SearchList/Tabs/Tabs.tsx b/src/components/SearchFromHome/SearchList/Tabs/Tabs.tsx new file mode 100644 index 00000000..67eedf15 --- /dev/null +++ b/src/components/SearchFromHome/SearchList/Tabs/Tabs.tsx @@ -0,0 +1,61 @@ +import { useState } from "react"; + +import styles from "./Tabs.module.scss"; + +import useComponentSize from "@/hooks/useComponetSize"; + +import SlideButton from "@/components/SlideButton/SlideButton"; + +import Tab from "./Tab/Tab"; + +interface PropsType { + set: React.Dispatch>; + category: string; +} + +function Tabs({ set, category }: PropsType) { + const [slideLocation, setSlideLocation] = useState(0); + const [componentRef, size] = useComponentSize(); + const thisCategory = [ + "전체", + "맛집", + "숙소", + "관광지", + "문화시설", + "레포츠", + "쇼핑", + ]; + + return ( +
+ +
+ {thisCategory.map((thisCategory) => ( + + ))} +
+
+ ); +} + +export default Tabs; diff --git a/src/components/SlideButton/LeftButton/LeftButton.tsx b/src/components/SlideButton/LeftButton/LeftButton.tsx index 02693d78..71790084 100644 --- a/src/components/SlideButton/LeftButton/LeftButton.tsx +++ b/src/components/SlideButton/LeftButton/LeftButton.tsx @@ -9,7 +9,11 @@ function LeftButton({ setSlideLocation, itemWidth, flexGap, + buttonSize, }: LeftButtonPropsType) { + const buttonsSize = buttonSize ? buttonSize : 40; + const iconSize = buttonSize ? buttonSize / 3 : undefined; + function handleButton() { if (-itemWidth < slideLocation) { setSlideLocation(0); @@ -21,7 +25,12 @@ function LeftButton({ return ( +
+ ); +}; + +export default VoteDetailsBottomButton; diff --git a/src/components/Vote/VoteDetailsField/VoteDetailsField.module.scss b/src/components/Vote/VoteDetailsField/VoteDetailsField.module.scss new file mode 100644 index 00000000..fa7c3a1f --- /dev/null +++ b/src/components/Vote/VoteDetailsField/VoteDetailsField.module.scss @@ -0,0 +1,32 @@ +@use "@/sass" as *; + +.container { + width: 100%; + padding: 20px; + + display: flex; + flex-direction: column; + flex-grow: 1; + + &__stateBar { + margin-bottom: 15px; + display: flex; + justify-content: space-between; + align-items: center; + + &__state { + @include typography(captionSmall); + color: $primary200; + //color:$neutral400; + } + &__addCandidate { + @include typography(button); + color: $neutral900; + } + } + &__candidateList { + display: flex; + flex-direction: column; + gap: 16px; + } +} diff --git a/src/components/Vote/VoteDetailsField/VoteDetailsField.tsx b/src/components/Vote/VoteDetailsField/VoteDetailsField.tsx new file mode 100644 index 00000000..d4076471 --- /dev/null +++ b/src/components/Vote/VoteDetailsField/VoteDetailsField.tsx @@ -0,0 +1,35 @@ +import { Icon } from "@chakra-ui/react"; +import { GoDotFill } from "react-icons/go"; + +import styles from "./VoteDetailsField.module.scss"; + +import VoteCandidateItem from "../VoteCandidateItem/VoteCandidateItem"; + +// import VoteDetailsFieldZero from "../VoteDetailsFieldZero/VoteDetailsFieldZero"; + +const VoteDetailsField = () => { + // if(CandidateList.length===0) { + // //지도 색 neutral300 + // return + // } + + return ( +
+
+
+ + 진행 중 +
+
+ + 후보 추가(1/15) +
+
+
+ + + +
+
+ ); +}; +export default VoteDetailsField; diff --git a/src/components/Vote/VoteDetailsFieldZero/VoteDetailsFieldZero.module.scss b/src/components/Vote/VoteDetailsFieldZero/VoteDetailsFieldZero.module.scss new file mode 100644 index 00000000..3cc2caba --- /dev/null +++ b/src/components/Vote/VoteDetailsFieldZero/VoteDetailsFieldZero.module.scss @@ -0,0 +1,20 @@ +@use "@/sass" as *; + +.container { + display: flex; + flex-direction: column; + flex-grow: 1; + + &__zeroTextBox { + text-align: center; + margin-top: 25vh; + &__title { + @include typography(titleSmall); + color: $neutral400; + } + &__text { + @include typography(captionSmall); + color: $neutral300; + } + } +} diff --git a/src/components/Vote/VoteDetailsFieldZero/VoteDetailsFieldZero.tsx b/src/components/Vote/VoteDetailsFieldZero/VoteDetailsFieldZero.tsx new file mode 100644 index 00000000..9f1d2d40 --- /dev/null +++ b/src/components/Vote/VoteDetailsFieldZero/VoteDetailsFieldZero.tsx @@ -0,0 +1,18 @@ +import styles from "./VoteDetailsFieldZero.module.scss"; + +const VoteDetailsFieldZero = () => { + return ( +
+
+

+ 투표 후보가 없습니다. +

+

+ 하단 버튼을 눌러 후보를 추가해보세요! +

+
+
+ ); +}; + +export default VoteDetailsFieldZero; diff --git a/src/components/Vote/VoteDetailsHeader/VoteDetailsHeader.module.scss b/src/components/Vote/VoteDetailsHeader/VoteDetailsHeader.module.scss new file mode 100644 index 00000000..58776061 --- /dev/null +++ b/src/components/Vote/VoteDetailsHeader/VoteDetailsHeader.module.scss @@ -0,0 +1,36 @@ +@use "@/sass" as *; + +.container { + height: 4rem; + width: 45rem; + min-width: 36rem; + position: fixed; + top: 0; + left: 50%; + transform: translateX(-50%); + + padding: 0 16px; + + background-color: $neutral0; + + display: flex; + justify-content: space-between; + align-items: center; + + font-size: 2.4rem; + + z-index: 100; +} + +.title { + @include typography(tabLabel); + height: 2.2rem; + margin-left: 40px; +} + +.iconBox { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; +} diff --git a/src/components/Vote/VoteDetailsHeader/VoteDetailsHeader.tsx b/src/components/Vote/VoteDetailsHeader/VoteDetailsHeader.tsx new file mode 100644 index 00000000..0eecd11b --- /dev/null +++ b/src/components/Vote/VoteDetailsHeader/VoteDetailsHeader.tsx @@ -0,0 +1,28 @@ +import { BsThreeDots } from "react-icons/bs"; +import { FaRegMap } from "react-icons/fa"; +import { MdOutlineArrowBackIosNew } from "react-icons/md"; + +import styles from "./VoteDetailsHeader.module.scss"; + +const VoteDetailsHeader = () => { + const voteTitle = "카페 어디로 갈래?"; + + //상태에 따른 아이콘 disabled + // 또는 없애기 + + return ( +
+
+ +
+

{voteTitle}

+ +
+ + +
+
+ ); +}; + +export default VoteDetailsHeader; diff --git a/src/mocks/handlers/home.ts b/src/mocks/handlers/home.ts index a6b64c09..1b797ece 100644 --- a/src/mocks/handlers/home.ts +++ b/src/mocks/handlers/home.ts @@ -3,6 +3,7 @@ import { http, HttpResponse } from "msw"; import { Dispatch } from "react"; const recommendedItem = [ + // 홈 [ { title: "호텔 loft", @@ -175,28 +176,27 @@ const recommendedLocation = [ "https://img-cf.kurly.com/shop/data/goodsview/20210218/gv30000159355_1.jpg", }, ]; - const userVoteData = [ { title: "부산, 여수 여행", date: "1.17-1.19", profile: "https://avatars.githubusercontent.com/u/154430298?s=48&v=4", discussion: "첫째 날 카페 어디갈래?", - voteURL: "/voteDetail", + voteURL: "/vote", }, { title: "부산, 여수 여행", date: "1.17-1.19", profile: "https://avatars.githubusercontent.com/u/154430298?s=48&v=4", discussion: "둘째 날 카페 어디갈래?", - voteURL: "/voteDetail", + voteURL: "/vote", }, { title: "부산, 여수 여행", date: "1.17-1.19", profile: "https://avatars.githubusercontent.com/u/154430298?s=48&v=4", discussion: "셋째 날 카페 어디갈래?", - voteURL: "/voteDetail", + voteURL: "/vote", }, ]; const tripSpaceData = [ @@ -222,8 +222,215 @@ const tripSpaceData = [ dDay: "D-34", }, ]; +// 홈 검색 +const searchKeywordData = [ + "감자", + "강릉 감자", + "강릉 감자유원지", + "부산", + "울산 맛집", + "해운대 카페", + "광안리 횟집", + "깡통시장 깡돼후", +]; +const hotPlaces = [ + { + title: "대전 성심당", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "맛집 · 대전", + id: "1", + }, + { + title: "대전 성심당", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "맛집 · 대전", + id: "1", + }, + { + title: "대전 성심당", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "맛집 · 대전", + id: "1", + }, + { + title: "대전 성심당", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "맛집 · 대전", + id: "1", + }, + { + title: "대전 성심당", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "맛집 · 대전", + id: "1", + }, + { + title: "대전 성심당", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "맛집 · 대전", + id: "1", + }, +]; +const hotHotels = [ + { + title: "호텔 loft", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "호텔 · 제주", + id: "1", + }, + { + title: "호텔 loft", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "호텔 · 제주", + id: "1", + }, + { + title: "호텔 loft", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "호텔 · 제주", + id: "1", + }, + { + title: "호텔 loft", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "호텔 · 제주", + id: "1", + }, + { + title: "호텔 loft", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "호텔 · 제주", + id: "1", + }, + { + title: "호텔 loft", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "호텔 · 제주", + id: "1", + }, +]; +const searchItemData = [ + { + title: "아시안 누들 서비스", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "맛집", + }, + { + title: "더 타코부스", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "맛집", + }, + { + title: "콴안다오", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "맛집", + }, + { + title: "신라호텔", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "숙소", + }, + { + title: "조선호텔", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "숙소", + }, + { + title: "그랜드하얏트", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "숙소", + }, + { + title: "더 현대", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "관광지", + }, + { + title: "서울숲", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "관광지", + }, + { + title: "롯데월드", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "관광지", + }, + { + title: "국립중앙박물관", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "문화시설", + }, + { + title: "디큐브아트센터", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "문화시설", + }, + { + title: "인사아트프로젝트", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "문화시설", + }, + { + title: "한강 레저", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "레포츠", + }, + { + title: "서울 레이스 파크", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "레포츠", + }, + { + title: "남산 케이블카", + imageURL: + "https://m.eejmall.com/web/product/big/201708/211_shop1_627935.jpg", + location: "서울", + category: "레포츠", + }, +]; export const home = [ + // 홈 http.get("/api/home/recommendedItem/1", () => { return HttpResponse.json(recommendedItem[0], { status: 200, @@ -249,6 +456,27 @@ export const home = [ status: 200, }); }), + // 홈 검색 + http.get("/api/home/search/keyword", () => { + return HttpResponse.json(searchKeywordData, { + status: 200, + }); + }), + http.get("/api/home/search/hotplace", () => { + return HttpResponse.json(hotPlaces, { + status: 200, + }); + }), + http.get("/api/home/search/hothotel", () => { + return HttpResponse.json(hotHotels, { + status: 200, + }); + }), + http.get("/api/home/search/search", () => { + return HttpResponse.json(searchItemData, { + status: 200, + }); + }), ]; // 추후 api폴더가 생기면 함수를 옮기겠습니다. diff --git a/src/pages/SearchFromHome/SearchFromHome.module.scss b/src/pages/SearchFromHome/SearchFromHome.module.scss new file mode 100644 index 00000000..39ebfd03 --- /dev/null +++ b/src/pages/SearchFromHome/SearchFromHome.module.scss @@ -0,0 +1,21 @@ +@use "@/sass" as *; + +.container { + width: 100%; + + display: flex; + flex-direction: column; + gap: 24px; + + padding-top: 16px; + + font-family: "suit", sans-serif; + + color: $neutral900; + + -ms-overflow-style: none; + scrollbar-width: none; + ::-webkit-scrollbar { + display: none; + } +} diff --git a/src/pages/SearchFromHome/SearchFromHome.tsx b/src/pages/SearchFromHome/SearchFromHome.tsx new file mode 100644 index 00000000..25faaa8f --- /dev/null +++ b/src/pages/SearchFromHome/SearchFromHome.tsx @@ -0,0 +1,24 @@ +import { useState } from "react"; + +import styles from "./SearchFromHome.module.scss"; + +import SearchBar from "@/components/SearchFromHome/SearchBar/SearchBar"; +import SearchHome from "@/components/SearchFromHome/SearchHome/SearchHome"; +import SearchList from "@/components/SearchFromHome/SearchList/SearchList"; + +function SearchFromHome() { + const [keyword, setKeyword] = useState(); + + return ( +
+ + {!keyword ? ( + + ) : ( + + )} +
+ ); +} + +export default SearchFromHome; diff --git a/src/pages/Trip/Trip.tsx b/src/pages/Trip/Trip.tsx index 5d17f70a..b2c36612 100644 --- a/src/pages/Trip/Trip.tsx +++ b/src/pages/Trip/Trip.tsx @@ -39,7 +39,6 @@ function Trip() { onOpen: onBottomSlideOpen, onClose: onBottomSlideClose, } = useDisclosure(); - const { isOpen: isSlideBarOpen, onOpen: onSlideBarOpen, diff --git a/src/pages/VoteDetail/VoteDetail.module.scss b/src/pages/VoteDetail/VoteDetail.module.scss new file mode 100644 index 00000000..e1ddeb1b --- /dev/null +++ b/src/pages/VoteDetail/VoteDetail.module.scss @@ -0,0 +1,11 @@ +@use "@/sass" as *; + +.container { + height: 100vh; + width: 100%; + margin-top: 4rem; + + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/src/pages/VoteDetail/VoteDetail.tsx b/src/pages/VoteDetail/VoteDetail.tsx new file mode 100644 index 00000000..78c5bbf6 --- /dev/null +++ b/src/pages/VoteDetail/VoteDetail.tsx @@ -0,0 +1,21 @@ +import styles from "./VoteDetail.module.scss"; + +// import CreatVoteTitleModal from "@/components/Vote/CreatVoteTitleModal/CreatVoteTitleModal"; +import VoteDetailsBottomButton from "@/components/Vote/VoteDetailsBottomButton/VoteDetailsBottomButton"; +import VoteDetailsField from "@/components/Vote/VoteDetailsField/VoteDetailsField"; +import VoteDetailsHeader from "@/components/Vote/VoteDetailsHeader/VoteDetailsHeader"; + +const VoteDetail = () => { + return ( +
+ + + + {/* 나중에 여행스페이스 메인에 옮겨야함 */} + {/* */} + +
+ ); +}; + +export default VoteDetail; diff --git a/src/routes/MainRouter/MainRouter.tsx b/src/routes/MainRouter/MainRouter.tsx index 7e7002d2..e7f1f1e7 100644 --- a/src/routes/MainRouter/MainRouter.tsx +++ b/src/routes/MainRouter/MainRouter.tsx @@ -1,14 +1,15 @@ import { Route, Routes } from "react-router-dom"; -import Login from "@/pages/Login/Login"; import Alarm from "@/pages/Alarm/Alarm"; import Detail from "@/pages/Detail/Detail"; import Home from "@/pages/Home/Home"; +import Login from "@/pages/Login/Login"; +import SearchFromHome from "@/pages/SearchFromHome/SearchFromHome"; import AgreePrivacy from "@/pages/Signup/Agree/AgreePrivacy"; import AgreeService from "@/pages/Signup/Agree/AgreeService"; -import Dashboard from "@/routes/Dashboard/Dashboard"; -import Vote from "@/pages/Vote/Vote"; import Trip from "@/pages/Trip/Trip"; +import Vote from "@/pages/Vote/Vote"; +import Dashboard from "@/routes/Dashboard/Dashboard"; function MainRouter() { return ( @@ -25,6 +26,7 @@ function MainRouter() { } /> } /> } /> + } /> ); } diff --git a/src/types/home.ts b/src/types/home.ts index 1bd496f2..e03e6600 100644 --- a/src/types/home.ts +++ b/src/types/home.ts @@ -1,3 +1,5 @@ +// 홈 타입 + export interface RecommendedItemDataType { title: string; imageURL: string; @@ -32,6 +34,7 @@ export interface LeftButtonPropsType { setSlideLocation: React.Dispatch>; itemWidth: number; flexGap: number; + buttonSize?: number; } interface ComponentSize { @@ -43,3 +46,18 @@ export interface SlideButtonPropsType extends LeftButtonPropsType { slideSize: ComponentSize; itemNumber: number; } + +// 홈 검색 타입 +export interface SearchHotItemType { + title: string; + imageURL: string; + location: string; + id: number; +} + +export interface SearchItemType { + title: string; + imageURL: string; + location: string; + category: string; +}