diff --git a/fe/src/App.tsx b/fe/src/App.tsx index 2445291b4..4354be113 100644 --- a/fe/src/App.tsx +++ b/fe/src/App.tsx @@ -4,13 +4,22 @@ import { theme as MuiGlobalTheme } from 'styles/GlobalStyles'; import { theme as ScGlobalTheme } from 'styles/theme'; import Router from 'Router'; +import { PersonnalProvider } from 'contexts/PersonnelContext'; +import { PriceProvider } from 'contexts/PriceContext'; +import { PeriodPriovider } from 'contexts/periodContext'; function App() { return ( - + + + + + + + ); diff --git a/fe/src/components/Calendar.tsx b/fe/src/components/Calendar.tsx new file mode 100644 index 000000000..409eda84b --- /dev/null +++ b/fe/src/components/Calendar.tsx @@ -0,0 +1,92 @@ +import { usePeriodDispatch } from 'contexts/periodContext'; +import { ReactComponent as LeftIcon } from 'images/FE_숙소예약서비스/Property 1=chevron-left.svg'; +import { ReactComponent as RightIcon } from 'images/FE_숙소예약서비스/Property 1=chevron-right.svg'; +import styled from 'styled-components'; + +type CalendarProps = { + month: number; +}; + +function Calendar({ month }: CalendarProps) { + const days: Array = ['일', '월', '화', '수', '목', '금', '토']; + const daysComp = days.map((day) => { + return {day}; + }); + const { setCheckIn, setCheckOut, setMonth } = usePeriodDispatch(); + + function increaseMonth() { + if (month < 10) setMonth(month + 2); + } + function decreaseMonth() { + if (month > 0) setMonth(month - 2); + } + return ( + <> + + { + decreaseMonth(); + }} + /> + {month + 1}월 + {month + 2}월 + { + increaseMonth(); + }} + /> + + + {daysComp} + 다음달 + + + ); +} + +const SlideBtnWrap = styled.div` + width: 100%; + height: 24px; + display: flex; + justify-content: space-between; + position: relative; + margin-bottom: 24px; +`; +const PrevBtn = styled(LeftIcon)` + position: absolute; +`; +const ThisMonthTitle = styled.div` + width: 336px; + display: flex; + justify-content: center; + ${({ theme }) => theme.fontStyles.bold16px}; +`; +const NextMonthTitle = styled(ThisMonthTitle)``; +const NextBtn = styled(RightIcon)` + position: absolute; + right: 0; + cursor: pointer; +`; +const CalendarWrap = styled.div` + width: 100%; + display: flex; + justify-content: space-between; +`; +const ThisMonth = styled.div` + width: 336px; + height: 336px; + display: flex; + justify-content: center; + align-items: center; +`; +const Day = styled.div` + width: 48px; + height: 24px; + display: flex; + justify-content: center; + align-items: center; + ${({ theme }) => theme.fontStyles.normal12px} + color : ${({ theme }) => theme.colors.grey3} +`; +const NextMonth = styled(ThisMonth)``; +export default Calendar; diff --git a/fe/src/components/Header/MiniSearchBar/index.tsx b/fe/src/components/Header/MiniSearchBar/index.tsx index 25b5932e1..bea884f85 100644 --- a/fe/src/components/Header/MiniSearchBar/index.tsx +++ b/fe/src/components/Header/MiniSearchBar/index.tsx @@ -2,18 +2,24 @@ import styled from 'styled-components'; import { Divider } from '@mui/material'; import { StyledSearchIcon } from 'components/Header/SearchBar/searchBar.styled'; +import { usePersonnelState } from 'contexts/PersonnelContext'; +import { usePriceState } from 'contexts/PriceContext'; + type MyProps = { changeSearchBar: () => void; }; function MiniSearchBar({ changeSearchBar }: MyProps) { + const { counterText } = usePersonnelState(); + const { rangeText } = usePriceState(); + return ( 일정 입력 - ₩100,000~1,000,000 + {rangeText} - 인원 입력 + {counterText} diff --git a/fe/src/components/Header/SearchBar/Period.tsx b/fe/src/components/Header/SearchBar/Period.tsx index e9518478c..239b258b3 100644 --- a/fe/src/components/Header/SearchBar/Period.tsx +++ b/fe/src/components/Header/SearchBar/Period.tsx @@ -1,36 +1,57 @@ import { Divider } from '@mui/material'; -import { - CommonContainer, - CommonButton, - Label, - InputState, - StyledCrossIcon, -} from 'components/Header/SearchBar/searchBar.styled'; +import { CommonContainer } from 'components/Header/SearchBar/searchBar.styled'; +import { ClickModal } from '.'; +import InputButton from './common/InputButton'; +import ResetButton from './common/ResetButton'; -function Period() { +function Period({ clickModal, isClicked, focusModal }: ClickModal) { return ( <> - - - - 날짜 입력 - - - - - - - - 날짜 입력 - - - + + {focusModal === '' && } + ); } +function CheckIn({ clickModal, isClicked, focusModal }: ClickModal) { + const FILTER_ID = 'CHECK_IN'; + const BUTTON_INFO = { + id: FILTER_ID, + title: '체크인', + inputText: '날짜 입력', + ariaLabel: '체크인 날짜 입력 버튼', + }; + const RESET_BUTTON_ARIA_LABEL = '날짜 입력 취소 버튼'; + + return ( + + + {isClicked && focusModal === FILTER_ID && } + + ); +} + +function CheckOut({ clickModal, isClicked, focusModal }: ClickModal) { + const FILTER_ID = 'CHECK_OUT'; + const BUTTON_INFO = { + id: FILTER_ID, + title: '체크아웃', + inputText: '날짜 입력', + ariaLabel: '체크아웃 날짜 입력 버튼', + }; + const RESET_BUTTON_ARIA_LABEL = '날짜 입력 취소 버튼'; + + return ( + + + {isClicked && focusModal === FILTER_ID && } + + ); +} + export default Period; diff --git a/fe/src/components/Header/SearchBar/Personnel.tsx b/fe/src/components/Header/SearchBar/Personnel.tsx index 565725a18..3335f7bca 100644 --- a/fe/src/components/Header/SearchBar/Personnel.tsx +++ b/fe/src/components/Header/SearchBar/Personnel.tsx @@ -1,29 +1,31 @@ -import styled from 'styled-components'; -import { - CommonButton, - Label, - InputState, - StyledCrossIcon, -} from 'components/Header/SearchBar/searchBar.styled'; +import { usePersonnelState } from 'contexts/PersonnelContext'; + +import { ClickModal } from '.'; +import ResetButton from './common/ResetButton'; +import InputButton from './common/InputButton'; + +function Personnel({ clickModal, isClicked, focusModal }: ClickModal) { + const { counterText } = usePersonnelState(); + + const FILTER_ID = 'PERSONNEL'; + const BUTTON_INFO = { + id: FILTER_ID, + title: '인원', + inputText: counterText, + ariaLabel: '게스트 추가 버튼', + }; + const RESET_BUTTON_ARIA_LABEL = '게스트 추가 취소 버튼'; -function Personnel() { return ( <> - - - 게스트 2명, 유아 2명 - - + + {isClicked && focusModal === FILTER_ID && } ); } -const PersonnelInputState = styled(InputState)` - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -`; - export default Personnel; diff --git a/fe/src/components/Header/SearchBar/Price.tsx b/fe/src/components/Header/SearchBar/Price.tsx index 469580e48..8e0c01f43 100644 --- a/fe/src/components/Header/SearchBar/Price.tsx +++ b/fe/src/components/Header/SearchBar/Price.tsx @@ -1,21 +1,27 @@ -import { - CommonContainer, - CommonButton, - Label, - InputState, - StyledCrossIcon, -} from 'components/Header/SearchBar/searchBar.styled'; +import { usePriceState } from 'contexts/PriceContext'; + +import { CommonContainer } from 'components/Header/SearchBar/searchBar.styled'; +import ResetButton from './common/ResetButton'; +import InputButton from './common/InputButton'; + +import { ClickModal } from '.'; + +function Price({ clickModal, isClicked, focusModal }: ClickModal) { + const { rangeText } = usePriceState(); + + const FILTER_ID = 'PRICE'; + const BUTTON_INFO = { + id: FILTER_ID, + title: '요금', + inputText: rangeText, + ariaLabel: '요금 입력 버튼', + }; + const RESET_BUTTON_ARIA_LABEL = '요금 입력 취소 버튼'; -function Price() { return ( - - - - ₩100,000~1,000,000 - - + + + {isClicked && focusModal === FILTER_ID && } ); } diff --git a/fe/src/components/Header/SearchBar/SearchButton.tsx b/fe/src/components/Header/SearchBar/SearchButton.tsx index e9a628b15..882a46cf2 100644 --- a/fe/src/components/Header/SearchBar/SearchButton.tsx +++ b/fe/src/components/Header/SearchBar/SearchButton.tsx @@ -2,12 +2,16 @@ import { SearchButton as SearchButtonContainer, StyledSearchIcon, } from 'components/Header/SearchBar/searchBar.styled'; +import { ModalContext } from 'contexts/ModalContext'; +import { useContext } from 'react'; function SearchButton() { + const { focusModal } = useContext(ModalContext); + return ( - + - 검색 + {focusModal !== '' && 검색} ); } diff --git a/fe/src/components/Header/SearchBar/common/InputButton.tsx b/fe/src/components/Header/SearchBar/common/InputButton.tsx new file mode 100644 index 000000000..a04b90102 --- /dev/null +++ b/fe/src/components/Header/SearchBar/common/InputButton.tsx @@ -0,0 +1,38 @@ +import { CommonButton, Label, SelectedOption } from 'components/Header/SearchBar/searchBar.styled'; + +type ButtonInfo = { + id: string | undefined; + title: string; + inputText: string; + ariaLabel: string; +}; + +type Style = { + [key: string]: number | string; +}; + +type InputButtonType = { + clickModal: (e: React.MouseEvent) => void; + buttonInfo: ButtonInfo; + styleOptions?: Style; +}; + +function InputButton({ clickModal, buttonInfo, styleOptions }: InputButtonType) { + return ( + + + {buttonInfo.inputText} + + ); +} + +InputButton.defaultProps = { + styleOptions: undefined, +}; + +export default InputButton; diff --git a/fe/src/components/Header/SearchBar/common/ResetButton.tsx b/fe/src/components/Header/SearchBar/common/ResetButton.tsx new file mode 100644 index 000000000..694021614 --- /dev/null +++ b/fe/src/components/Header/SearchBar/common/ResetButton.tsx @@ -0,0 +1,15 @@ +import { StyledCrossIcon } from 'components/Header/SearchBar/searchBar.styled'; + +type Aria = { + ariaLabel: string; +}; + +function ResetButton({ ariaLabel }: Aria) { + return ( + + ); +} + +export default ResetButton; diff --git a/fe/src/components/Header/SearchBar/index.tsx b/fe/src/components/Header/SearchBar/index.tsx index 6e3b49a4a..6fa979af0 100644 --- a/fe/src/components/Header/SearchBar/index.tsx +++ b/fe/src/components/Header/SearchBar/index.tsx @@ -1,3 +1,5 @@ +import { useModal } from 'hooks/useModal'; + import { Divider } from '@mui/material'; import { SearchBarWrap, @@ -11,27 +13,58 @@ import Price from 'components/Header/SearchBar/Price'; import SearchButton from 'components/Header/SearchBar/SearchButton'; import PeriodModal from 'components/Modals/PeriodModal'; -import PriceModal from 'components/Modals/PriceModal'; import PersonnelModal from 'components/Modals/PersonnelModal'; +import PriceModal from 'components/Modals/PriceModal'; + +export type ClickModal = { + clickModal: (e: React.MouseEvent) => void; + isClicked?: boolean; + focusModal?: string; + id?: string; +}; + +type Focus = { + focus: string; +}; function SearchBar() { + const { focusModal, clickModalFocus, isClicked } = useModal(); + + const clickModal = (e: React.MouseEvent) => { + if (!e.currentTarget.dataset.id) return; + const datasetID: string = e.currentTarget.dataset.id; + clickModalFocus?.(datasetID); + }; + return ( - - - - - - - + + + {focusModal === '' && } + + {focusModal === '' && } + + - - - + {focusModal !== '' && } ); } +function FocusModal({ focus }: Focus) { + switch (focus) { + case 'CHECK_IN': + case 'CHECK_OUT': + return ; + case 'PERSONNEL': + return ; + case 'PRICE': + return ; + default: + throw Error(); + } +} + export default SearchBar; diff --git a/fe/src/components/Header/SearchBar/searchBar.styled.js b/fe/src/components/Header/SearchBar/searchBar.styled.ts similarity index 65% rename from fe/src/components/Header/SearchBar/searchBar.styled.js rename to fe/src/components/Header/SearchBar/searchBar.styled.ts index e23b6cca1..2478e6e4a 100644 --- a/fe/src/components/Header/SearchBar/searchBar.styled.js +++ b/fe/src/components/Header/SearchBar/searchBar.styled.ts @@ -1,22 +1,40 @@ -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { ReactComponent as SearchIcon } from 'images/FE_숙소예약서비스/Property 1=search.svg'; import { ReactComponent as CrossIcon } from 'images/FE_숙소예약서비스/Property 1=x-circle.svg'; +type SearchBarContainerType = { + isClicked: boolean; +}; + export const SearchBarContainer = styled.div` position: relative; `; -export const SearchBarWrap = styled.div` +export const SearchBarWrap = styled.div` display: flex; align-items: center; width: 916px; height: 76px; border: 1px solid ${({ theme }) => theme.colors.grey4}; border-radius: 999px; - background: ${({ theme }) => theme.colors.white}; + + background: ${({ isClicked }) => + isClicked + ? css` + ${({ theme }) => theme.colors.grey5}; + ` + : css` + ${({ theme }) => theme.colors.white} + `}; `; -export const CommonContainer = styled.div` +interface ContainerType { + isClicked?: boolean; + focusModal?: string; + id: string; +} + +export const CommonContainer = styled.div` display: flex; align-items: center; justify-content: space-between; @@ -29,11 +47,13 @@ export const CommonContainer = styled.div` background: ${({ theme }) => theme.colors.grey6}; } - /* props로 넘겨받도록 수정 */ - &.focus { - background: ${({ theme }) => theme.colors.white}; - box-shadow: 0px 0px 13px 2px rgba(51, 51, 51, 0.29); - } + ${({ focusModal, id, isClicked }) => + focusModal === id && + isClicked && + css` + background: ${({ theme }) => theme.colors.white}; + box-shadow: 0px 0px 13px 2px rgba(51, 51, 51, 0.29); + `}; `; export const CommonButton = styled.button` @@ -49,14 +69,18 @@ export const Label = styled.span` ${({ theme }) => theme.fontStyles.bold12px}; `; -export const InputState = styled.span` +export const SelectedOption = styled.span` width: 100%; text-align: left; color: ${({ theme }) => theme.colors.grey2}; ${({ theme }) => theme.fontStyles.normal16px}; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; `; -export const SearchButton = styled.button` +export const SearchButton = styled.a` display: flex; align-items: center; justify-content: center; diff --git a/fe/src/components/Header/SearchWrapper.jsx b/fe/src/components/Header/SearchWrapper.tsx similarity index 52% rename from fe/src/components/Header/SearchWrapper.jsx rename to fe/src/components/Header/SearchWrapper.tsx index 48fee3d85..f5adcc07e 100644 --- a/fe/src/components/Header/SearchWrapper.jsx +++ b/fe/src/components/Header/SearchWrapper.tsx @@ -1,8 +1,17 @@ +import { Dispatch } from 'react'; import styled from 'styled-components'; +import Menu from './Menu'; import MiniSearchBar from './MiniSearchBar'; import SearchBar from './SearchBar'; -function SearchWrapper({ changeSearchBar, miniFocus, setMiniFocus }) { +type SearchWrapperType = { + changeSearchBar: () => void; + miniFocus: boolean; + setMiniFocus: Dispatch>; + path: string; +}; + +function SearchWrapper({ changeSearchBar, miniFocus, setMiniFocus, path }: SearchWrapperType) { function mouseLeave() { if (!miniFocus) { setMiniFocus(true); @@ -11,7 +20,14 @@ function SearchWrapper({ changeSearchBar, miniFocus, setMiniFocus }) { return ( mouseLeave()}> - {miniFocus ? : } + {path === '/' || !miniFocus ? ( + <> + + + + ) : ( + + )} ); } diff --git a/fe/src/components/Header/UserMenu.tsx b/fe/src/components/Header/UserMenu.tsx new file mode 100644 index 000000000..2fdbccd10 --- /dev/null +++ b/fe/src/components/Header/UserMenu.tsx @@ -0,0 +1,43 @@ +import styled from 'styled-components'; +import { Avatar } from '@mui/material'; +import { ReactComponent as HamburgerIcon } from 'images/FE_숙소예약서비스/Property 1=menu.svg'; + +function UserMenu() { + return ( + + + + + ); +} + +const UserWrapper = styled.button` + display: flex; + justify-content: center; + align-items: center; + width: 76px; + height: 40px; + border-radius: 999px; + border: 1px solid ${({ theme }) => theme.colors.grey4}; + background: ${({ theme }) => theme.colors.white}; +`; + +const Hamburger = styled(HamburgerIcon)` + width: 16px; + height: 16px; + margin: 0 8px 0 10px; + + path { + stroke: ${({ theme }) => theme.colors.black}; + } +`; + +const CustomAvatar = styled(Avatar)` + && { + width: 32px; + height: 32px; + background: ${({ theme }) => theme.colors.grey3}; + } +`; + +export default UserMenu; diff --git a/fe/src/components/Header/index.tsx b/fe/src/components/Header/index.tsx index a4b962f6f..f4e804f25 100644 --- a/fe/src/components/Header/index.tsx +++ b/fe/src/components/Header/index.tsx @@ -2,13 +2,13 @@ import { useState } from 'react'; import { useLocation } from 'react-router-dom'; import styled, { css } from 'styled-components'; -import { Avatar, Link } from '@mui/material'; -import { ReactComponent as HamburgerIcon } from 'images/FE_숙소예약서비스/Property 1=menu.svg'; +import { Link } from '@mui/material'; import { ReactComponent as LogoImg } from 'images/logo.svg'; -import Menu from 'components/Header/Menu'; -import SearchBar from 'components/Header/SearchBar/index'; -import SearchWrapper, { SearchWrap } from './SearchWrapper'; +import SearchWrapper from './SearchWrapper'; +import UserMenu from './UserMenu'; + +// type Router = '/' | '/result'; type Position = { position: string; @@ -20,27 +20,20 @@ function Header() { const changeSearchBar = () => setMiniFocus((focus) => !focus); + const path = location.pathname; + return ( - + - {location.pathname === '/' ? ( - - - - - ) : ( - - )} - - - - + + ); } @@ -68,32 +61,4 @@ const HeaderWrap = styled.header` z-index: 1; `; -const UserWrapper = styled.button` - display: flex; - justify-content: center; - align-items: center; - width: 76px; - height: 40px; - border-radius: 999px; - border: 1px solid ${({ theme }) => theme.colors.grey4}; - background: ${({ theme }) => theme.colors.white}; -`; - -const Hamburger = styled(HamburgerIcon)` - width: 16px; - height: 16px; - margin: 0 8px 0 10px; - - path { - stroke: ${({ theme }) => theme.colors.black}; - } -`; - -const CustomAvatar = styled(Avatar)` - && { - width: 32px; - height: 32px; - background: ${({ theme }) => theme.colors.grey3}; - } -`; export default Header; diff --git a/fe/src/components/Modals/PeriodModal.tsx b/fe/src/components/Modals/PeriodModal.tsx index d71311638..c62e64fe4 100644 --- a/fe/src/components/Modals/PeriodModal.tsx +++ b/fe/src/components/Modals/PeriodModal.tsx @@ -1,13 +1,24 @@ +import { usePeriodState } from 'contexts/periodContext'; +import Calendar from 'components/Calendar'; import styled from 'styled-components'; import { ModalWrap } from './styled'; function PeriodModal() { - return 기간모달; + const state = usePeriodState(); + const thisMonth: number = state.month; + return ( + + + + ); } const PeriodModalWrap = styled(ModalWrap)` width: 916px; height: 512px; - display: none; + /* display: none; */ + padding: 64px 88px; + flex-direction: column; `; + export default PeriodModal; diff --git a/fe/src/components/Modals/PersonnelModal.tsx b/fe/src/components/Modals/PersonnelModal.tsx index a8aa6955b..a1118217f 100644 --- a/fe/src/components/Modals/PersonnelModal.tsx +++ b/fe/src/components/Modals/PersonnelModal.tsx @@ -4,18 +4,25 @@ import PersonnelModalWrap from './PersonnelModalWrap'; import { ModalWrap } from './styled'; +export type PersonnselInfo = { + id: number; + title: string; + info: string; + desc: string; +}; + function PersonnelModal() { const PERSONNEL_INFO = [ - { id: 1, title: '성인', info: '만 13세 이상' }, - { id: 2, title: '어린이', info: '만 2~12세' }, - { id: 3, title: '유아', info: '만 2세 미만' }, + { id: 1, title: '성인', info: '만 13세 이상', desc: 'adult' }, + { id: 2, title: '어린이', info: '만 2~12세', desc: 'child' }, + { id: 3, title: '유아', info: '만 2세 미만', desc: 'toddler' }, ]; - const PersonnelInfo = PERSONNEL_INFO.map((info, index) => { + const PersonnelInfo = PERSONNEL_INFO.map((info: PersonnselInfo, index: number) => { return ( <> - {index !== PERSONNEL_INFO.length - 1 && } + {index !== PERSONNEL_INFO.length - 1 && } ); }); @@ -29,7 +36,6 @@ const PersonnelModalContainer = styled(ModalWrap)` right: 0; padding: 64px; display: flex; - display: none; flex-direction: column; `; diff --git a/fe/src/components/Modals/PersonnelModalWrap.tsx b/fe/src/components/Modals/PersonnelModalWrap.tsx index 581608190..f8d56d13a 100644 --- a/fe/src/components/Modals/PersonnelModalWrap.tsx +++ b/fe/src/components/Modals/PersonnelModalWrap.tsx @@ -1,8 +1,17 @@ +import { usePersonnelState } from 'contexts/PersonnelContext'; + import styled from 'styled-components'; import { ReactComponent as PlusIcon } from 'images/FE_숙소예약서비스/Property 1=plus-circle.svg'; import { ReactComponent as MinusIcon } from 'images/FE_숙소예약서비스/Property 1=minus-circle.svg'; +import { PersonnselInfo } from './PersonnelModal'; + +type InfoProps = { + info: PersonnselInfo; +}; + +function PersonnelModalWrap({ info }: InfoProps) { + const { counter } = usePersonnelState(); -function PersonnelModalWrap({ info }: any) { return ( @@ -11,7 +20,7 @@ function PersonnelModalWrap({ info }: any) { - 0 + {counter[info.desc]} diff --git a/fe/src/components/Modals/PriceModal.tsx b/fe/src/components/Modals/PriceModal.tsx index 20013e253..b26421f39 100644 --- a/fe/src/components/Modals/PriceModal.tsx +++ b/fe/src/components/Modals/PriceModal.tsx @@ -21,7 +21,6 @@ function PriceModal() { const PriceModalWrap = styled(ModalWrap)` width: 493px; height: 364px; - display: none; right: 200px; padding: 52px 64px; justify-content: left; diff --git a/fe/src/contexts/ModalContext.tsx b/fe/src/contexts/ModalContext.tsx new file mode 100644 index 000000000..8669a5756 --- /dev/null +++ b/fe/src/contexts/ModalContext.tsx @@ -0,0 +1,27 @@ +import { createContext, Dispatch, ReactNode, useState } from 'react'; + +type ModalInfo = { + focusModal: string; + setFocusModal?: Dispatch>; + isClicked: boolean; + setClicked?: Dispatch>; +}; + +export const ModalContext = createContext({ + focusModal: '', + isClicked: false, +}); + +export function ModalProvider({ children }: { children: ReactNode }) { + const [focusModal, setFocusModal] = useState(''); + const [isClicked, setClicked] = useState(false); + + return ( + + {children} + + ); +} diff --git a/fe/src/contexts/PersonnelContext.tsx b/fe/src/contexts/PersonnelContext.tsx new file mode 100644 index 000000000..0aba0c118 --- /dev/null +++ b/fe/src/contexts/PersonnelContext.tsx @@ -0,0 +1,62 @@ +import { createContext, Dispatch, ReactNode, useContext, useReducer } from 'react'; + +type Counter = { + [key: string]: number; + adult: number; + child: number; + toddler: number; +}; + +type State = { + counter: Counter; + counterText: string; +}; + +type Action = { type: 'INCREASE_COUNT'; age: string } | { type: 'DECREASE_COUNT'; age: string }; + +type PersonnelDispatch = Dispatch; + +const PersonnelStateContext = createContext(null); +const PersonnelDispatchContext = createContext(null); + +const initState: State = { + counter: { adult: 0, child: 0, toddler: 0 }, + counterText: '인원 선택', +}; + +export function PersonnalProvider({ children }: { children: ReactNode }) { + const [state, dispatch] = useReducer(personnalReducer, initState); + + return ( + + + {children} + + + ); +} + +function personnalReducer(state: State, action: Action) { + switch (action.type) { + case 'INCREASE_COUNT': + return state; + case 'DECREASE_COUNT': + return state; + default: + throw new Error(); + } +} + +export function usePersonnelState() { + const state = useContext(PersonnelStateContext); + if (!state) throw new Error(); + + return { counter: state.counter, counterText: state.counterText }; +} + +export function usePersonnelDispatch() { + const dispatch = useContext(PersonnelDispatchContext); + if (!dispatch) throw new Error(); + + return dispatch; +} diff --git a/fe/src/contexts/PriceContext.tsx b/fe/src/contexts/PriceContext.tsx new file mode 100644 index 000000000..e2512e709 --- /dev/null +++ b/fe/src/contexts/PriceContext.tsx @@ -0,0 +1,54 @@ +import { createContext, Dispatch, ReactNode, useContext, useReducer } from 'react'; + +type range = { + minPrice: number; + maxPrice: number; +}; + +type State = { + range: range; + rangeText: string; +}; + +type Action = { type: 'SET_RANGE'; age: string }; + +type PriceDispatch = Dispatch; + +const PriceStateContext = createContext(null); +const PriceDispatchContext = createContext(null); + +const initState: State = { + range: { minPrice: 0, maxPrice: 0 }, + rangeText: '금액대 설정', +}; + +export function PriceProvider({ children }: { children: ReactNode }) { + const [state, dispatch] = useReducer(priceReducer, initState); + + return ( + + {children} + + ); +} + +function priceReducer(state: State, action: Action): State { + switch (action.type) { + default: + throw new Error(); + } +} + +export function usePriceState() { + const state = useContext(PriceStateContext); + if (!state) throw new Error(); + + return { range: state.range, rangeText: state.rangeText }; +} + +export function usePriceDispatch() { + const dispatch = useContext(PriceDispatchContext); + if (!dispatch) throw new Error(); + + return dispatch; +} diff --git a/fe/src/contexts/periodContext.tsx b/fe/src/contexts/periodContext.tsx new file mode 100644 index 000000000..44529e051 --- /dev/null +++ b/fe/src/contexts/periodContext.tsx @@ -0,0 +1,77 @@ +import { createContext, Dispatch, ReactNode, useContext, useReducer } from 'react'; + +type Period = { + month: number; + checkIn: string; + checkOut: string; +}; + +type Action = + | { type: 'SET_CHECK_IN'; action: string } + | { type: 'SET_CHECK_OUT'; action: string } + | { type: 'SET_MONTH'; month: number }; + +type PeriodDispatch = Dispatch; +const date = new Date(); +const monthData: number = date.getMonth(); +const PeriodStateContext = createContext(null); +const PeriodDispatchContext = createContext(null); +const initialState: Period = { + month: monthData, + checkIn: date.toString(), + checkOut: date.toString(), +}; + +export function PeriodPriovider({ children }: { children: ReactNode }) { + const [state, dispatch] = useReducer(reducer, initialState); + + return ( + + {children} + + ); +} + +function reducer(state: Period, action: Action): Period { + const date: Date = new Date(); + switch (action.type) { + case 'SET_CHECK_IN': + console.log('체크인'); + return { + ...state, + checkIn: '체크인', + checkOut: '', + }; + case 'SET_CHECK_OUT': + console.log(state); + return { + ...state, + checkIn: '', + checkOut: '체크아웃', + }; + case 'SET_MONTH': + return { + ...state, + month: action.month, + }; + default: + throw new Error(); + } +} + +export function usePeriodState() { + const state = useContext(PeriodStateContext); + if (!state) throw new Error(); + + return { checkIn: state.checkIn, checkOut: state.checkOut, month: state.month }; +} + +export function usePeriodDispatch() { + const dispatch = useContext(PeriodDispatchContext); + if (!dispatch) throw new Error(); + + const setCheckIn = (action: string) => dispatch({ type: 'SET_CHECK_IN', action }); + const setCheckOut = (action: string) => dispatch({ type: 'SET_CHECK_OUT', action }); + const setMonth = (month: number) => dispatch({ type: 'SET_MONTH', month }); + return { setCheckIn, setCheckOut, setMonth }; +} diff --git a/fe/src/hooks/useModal.tsx b/fe/src/hooks/useModal.tsx new file mode 100644 index 000000000..3de2810b8 --- /dev/null +++ b/fe/src/hooks/useModal.tsx @@ -0,0 +1,22 @@ +import { useContext } from 'react'; +import { ModalContext } from 'contexts/ModalContext'; + +export function useModal() { + const { focusModal, setFocusModal, isClicked, setClicked } = useContext(ModalContext); + + const clickModalFocus = (id: string) => { + if (focusModal === id) { + setFocusModal?.(''); + setClicked?.(false); + } else { + setFocusModal?.(id); + setClicked?.(true); + } + }; + + const closeModal = () => { + setFocusModal?.(''); + }; + + return { focusModal, setFocusModal, isClicked, setClicked, clickModalFocus, closeModal }; +} diff --git a/fe/src/pages/Layout.jsx b/fe/src/pages/Layout.jsx index 076f8ad7a..64c553235 100644 --- a/fe/src/pages/Layout.jsx +++ b/fe/src/pages/Layout.jsx @@ -1,13 +1,16 @@ import { Outlet } from 'react-router-dom'; import Header from 'components/Header/index'; import styled from 'styled-components'; +import { ModalProvider } from 'contexts/ModalContext'; function Layout() { return ( - -
- - + + +
+ + + ); }