diff --git a/apps/shelter/src/pages/animals/detail/index.tsx b/apps/shelter/src/pages/animals/detail/index.tsx index 49cf55b5..e36382be 100644 --- a/apps/shelter/src/pages/animals/detail/index.tsx +++ b/apps/shelter/src/pages/animals/detail/index.tsx @@ -1,3 +1,21 @@ +import { useEffect } from 'react'; +import useDetailHeaderStore from 'shared/store/detailHeaderStore'; + +const handleDeletePost = (postId: number) => { + // TODO: AnimalPost delete API 호출 + console.log('[Delete Animal] postId:', postId); +}; + export default function AnimalsDetailPage() { + const setOnDelete = useDetailHeaderStore((state) => state.setOnDelete); + + useEffect(() => { + setOnDelete(handleDeletePost); + + return () => { + setOnDelete(() => {}); + }; + }, [setOnDelete]); + return

AnimalsDetailPage

; } diff --git a/apps/shelter/src/pages/animals/search/index.tsx b/apps/shelter/src/pages/animals/search/index.tsx new file mode 100644 index 00000000..5bbff8b6 --- /dev/null +++ b/apps/shelter/src/pages/animals/search/index.tsx @@ -0,0 +1,21 @@ +import { useEffect } from 'react'; +import useSearchHeaderStore from 'shared/store/searchHeaderStore'; + +const handleSearchkeyword = (keyword: string) => { + // TODO: AnimalList 검색 API 호출 + console.log('[Search Animal] - keyword:', keyword); +}; + +export default function AnimalsSearchPage() { + const setOnSearch = useSearchHeaderStore((state) => state.setOnSearch); + + useEffect(() => { + setOnSearch(handleSearchkeyword); + + return () => { + setOnSearch(() => {}); + }; + }, [setOnSearch]); + + return

AnimalsSearchPage

; +} diff --git a/apps/shelter/src/pages/animals/searchPage/index.tsx b/apps/shelter/src/pages/animals/searchPage/index.tsx deleted file mode 100644 index c3481089..00000000 --- a/apps/shelter/src/pages/animals/searchPage/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function AnimalsSearchPage() { - return

AnimalsSearchPage

; -} diff --git a/apps/shelter/src/pages/volunteers/detail/index.tsx b/apps/shelter/src/pages/volunteers/detail/index.tsx index eba1c072..f78cb9fe 100644 --- a/apps/shelter/src/pages/volunteers/detail/index.tsx +++ b/apps/shelter/src/pages/volunteers/detail/index.tsx @@ -1,3 +1,21 @@ +import { useEffect } from 'react'; +import useDetailHeaderStore from 'shared/store/detailHeaderStore'; + +const handleDeletePost = (postId: number) => { + // TODO: VolunteerPost delete API 호출 + console.log('[Delete Volunteer] postId:', postId); +}; + export default function VolunteersDetailPage() { + const setOnDelete = useDetailHeaderStore((state) => state.setOnDelete); + + useEffect(() => { + setOnDelete(handleDeletePost); + + return () => { + setOnDelete(() => {}); + }; + }, [setOnDelete]); + return

VolunteersDetailPage

; } diff --git a/apps/shelter/src/pages/volunteers/search/index.tsx b/apps/shelter/src/pages/volunteers/search/index.tsx index e087f613..b35ca75a 100644 --- a/apps/shelter/src/pages/volunteers/search/index.tsx +++ b/apps/shelter/src/pages/volunteers/search/index.tsx @@ -1,3 +1,21 @@ +import { useEffect } from 'react'; +import useSearchHeaderStore from 'shared/store/searchHeaderStore'; + +const handleSearchkeyword = (keyword: string) => { + // TODO: VolunteerList 검색 API 호출 + console.log('[Search Volunteer] keyword:', keyword); +}; + export default function VolunteersSearchPage() { + const setOnSearch = useSearchHeaderStore((state) => state.setOnSearch); + + useEffect(() => { + setOnSearch(handleSearchkeyword); + + return () => { + setOnSearch(() => {}); + }; + }, [setOnSearch]); + return

VolunteersSearchPage

; } diff --git a/apps/shelter/src/routes/index.tsx b/apps/shelter/src/routes/index.tsx index d008b849..1537e753 100644 --- a/apps/shelter/src/routes/index.tsx +++ b/apps/shelter/src/routes/index.tsx @@ -1,14 +1,17 @@ import { createBrowserRouter, RouterProviderProps } from 'react-router-dom'; +import APP_TYPE from 'shared/constants/appType'; +import PAGE_TYPE from 'shared/constants/pageType'; import Layout from 'shared/layout'; import PATH from '@/constants/path'; import AnimalsPage from '@/pages/animals'; import AnimalsDetailPage from '@/pages/animals/detail'; -import AnimalsSearchPage from '@/pages/animals/searchPage'; +import AnimalsSearchPage from '@/pages/animals/search'; import AnimalsUpdatePage from '@/pages/animals/update'; import AnimalsWritePage from '@/pages/animals/write'; import ChattingsPage from '@/pages/chattings'; import ChattingsRoomPage from '@/pages/chattings/room'; +import ManageApplyPage from '@/pages/manage/apply'; import ManageAttendancePage from '@/pages/manage/attendance'; import MyPage from '@/pages/my'; import MyReviewsPage from '@/pages/my/reviews'; @@ -28,39 +31,39 @@ import VolunteersWritePage from '@/pages/volunteers/write'; export const router: RouterProviderProps['router'] = createBrowserRouter([ { path: '/', - element: , + element: , errorElement: , children: [ { path: PATH.VOLUNTEERS.INDEX, children: [ { - id: `SHELTER_APP:VOLUNTEERS`, + id: PAGE_TYPE.VOLUNTEERS, index: true, element: , }, { - id: 'SHELTER_APP:VOLUNTEERS_DETAIL', + id: PAGE_TYPE.VOLUNTEERS_DETAIL, path: PATH.VOLUNTEERS.DETAIL, element: , }, { - id: 'SHELTER_APP:VOLUNTEERS_PROFILE', + id: PAGE_TYPE.VOLUNTEERS_PROFILE, path: PATH.VOLUNTEERS.PROFILE, element: , }, { - id: 'SHELTER_APP:VOLUNTEERS_SEARCH', + id: PAGE_TYPE.VOLUNTEERS_SEARCH, path: PATH.VOLUNTEERS.SEARCH, element: , }, { - id: 'SHELTER_APP:VOLUNTEERS_WRITE', + id: PAGE_TYPE.VOLUNTEERS_WRITE, path: PATH.VOLUNTEERS.WRITE, element: , }, { - id: 'SHELTER_APP:VOLUNTEERS_UPDATE', + id: PAGE_TYPE.VOLUNTEERS_UPDATE, path: PATH.VOLUNTEERS.UPDATE, element: , }, @@ -70,27 +73,27 @@ export const router: RouterProviderProps['router'] = createBrowserRouter([ path: PATH.ANIMALS.INDEX, children: [ { - id: 'SHELTER_APP:ANIMALS', + id: PAGE_TYPE.ANIMALS, index: true, element: , }, { - id: 'SHELTER_APP:ANIMALS_DETAIL', + id: PAGE_TYPE.ANIMALS_DETAIL, path: PATH.ANIMALS.DETAIL, element: , }, { - id: 'SHELTER_APP:ANIMALS_SEARCH', + id: PAGE_TYPE.ANIMALS_SEARCH, path: PATH.ANIMALS.SEARCH, element: , }, { - id: 'SHELTER_APP:ANIMALS_WRITE', + id: PAGE_TYPE.ANIMALS_WRITE, path: PATH.ANIMALS.WRITE, element: , }, { - id: 'SHELTER_APP:ANIMALS_UPDATE', + id: PAGE_TYPE.ANIMALS_UPDATE, path: PATH.ANIMALS.UPDATE, element: , }, @@ -100,12 +103,12 @@ export const router: RouterProviderProps['router'] = createBrowserRouter([ path: PATH.CHATTINGS.INDEX, children: [ { - id: 'SHELTER_APP:CHATTINGS', + id: PAGE_TYPE.CHATTINGS, index: true, element: , }, { - id: 'SHELTER_APP:CHATTINGS_ROOM', + id: PAGE_TYPE.CHATTINGS_ROOM, path: PATH.CHATTINGS.ROOM, element: , }, @@ -115,12 +118,12 @@ export const router: RouterProviderProps['router'] = createBrowserRouter([ path: PATH.MYPAGE.INDEX, children: [ { - id: 'SHELTER_APP:MYPAGE', + id: PAGE_TYPE.MYPAGE, index: true, element: , }, { - id: 'SHELTER_APP:MYPAGE_REVIEWS', + id: PAGE_TYPE.MYPAGE_REVIEWS, path: PATH.MYPAGE.REVIEWS, element: , }, @@ -130,12 +133,12 @@ export const router: RouterProviderProps['router'] = createBrowserRouter([ path: PATH.SETTINGS.INDEX, children: [ { - id: 'SHELTER_APP:SETTINGS_ACCOUNT', + id: PAGE_TYPE.SETTINGS_ACCOUNT, path: PATH.SETTINGS.ACCOUNT, element: , }, { - id: 'SHELTER_APP:SETTINGS_PASSWORD', + id: PAGE_TYPE.SETTINGS_PASSWORD, path: PATH.SETTINGS.PASSWORD, element: , }, @@ -145,29 +148,29 @@ export const router: RouterProviderProps['router'] = createBrowserRouter([ path: PATH.MANAGE.INDEX, children: [ { - id: 'SHELTER_APP:MANAGE.ATTENDANCE', + id: PAGE_TYPE.MANAGE_ATTENDANCE, path: PATH.MANAGE.ATTENDANCE, element: , }, { - id: 'SHELTER_APP:MANAGE.APPLY', + id: PAGE_TYPE.MANAGE_APPLY, path: PATH.MANAGE.APPLY, - element: , + element: , }, ], }, { - id: 'SHELTER_APP:NOTIFICATIONS', + id: PAGE_TYPE.NOTIFICATIONS, path: PATH.NOTIFICATIONS, element: , }, { - id: 'SHELTER_APP:SIGNUP', + id: PAGE_TYPE.SIGNUP, path: PATH.SIGNUP, element: , }, { - id: 'SHELTER_APP:SIGNIN', + id: PAGE_TYPE.SIGNIN, path: PATH.SIGNIN, element: , }, diff --git a/apps/volunteer/src/pages/volunteers/search/index.tsx b/apps/volunteer/src/pages/volunteers/search/index.tsx index e087f613..b35ca75a 100644 --- a/apps/volunteer/src/pages/volunteers/search/index.tsx +++ b/apps/volunteer/src/pages/volunteers/search/index.tsx @@ -1,3 +1,21 @@ +import { useEffect } from 'react'; +import useSearchHeaderStore from 'shared/store/searchHeaderStore'; + +const handleSearchkeyword = (keyword: string) => { + // TODO: VolunteerList 검색 API 호출 + console.log('[Search Volunteer] keyword:', keyword); +}; + export default function VolunteersSearchPage() { + const setOnSearch = useSearchHeaderStore((state) => state.setOnSearch); + + useEffect(() => { + setOnSearch(handleSearchkeyword); + + return () => { + setOnSearch(() => {}); + }; + }, [setOnSearch]); + return

VolunteersSearchPage

; } diff --git a/apps/volunteer/src/routes/index.tsx b/apps/volunteer/src/routes/index.tsx index 5f07b773..ed39944a 100644 --- a/apps/volunteer/src/routes/index.tsx +++ b/apps/volunteer/src/routes/index.tsx @@ -1,4 +1,6 @@ import { createBrowserRouter, RouterProviderProps } from 'react-router-dom'; +import APP_TYPE from 'shared/constants/appType'; +import PAGE_TYPE from 'shared/constants/pageType'; import Layout from 'shared/layout'; import PATH from '@/constants/path'; @@ -9,6 +11,7 @@ import ChattingsRoomPage from '@/pages/chattings/room'; import MyPage from '@/pages/my'; import NotFoundPage from '@/pages/notfound'; import NotificationsPage from '@/pages/notifications'; +import SettingsPage from '@/pages/settings'; import SettingsAccountPage from '@/pages/settings/account'; import SettingsPasswordPage from '@/pages/settings/password'; import SheltersProfilePage from '@/pages/shelters/profile'; @@ -23,24 +26,24 @@ import VolunteersSearchPage from '@/pages/volunteers/search'; export const router: RouterProviderProps['router'] = createBrowserRouter([ { path: '/', - element: , + element: , errorElement: , children: [ { path: PATH.VOLUNTEERS.INDEX, children: [ { - id: 'VOLUNTEER_APP:VOLUNTEERS', + id: PAGE_TYPE.VOLUNTEERS, index: true, element: , }, { - id: 'VOLUNTEER_APP:VOLUNTEERS_DETAIL', + id: PAGE_TYPE.VOLUNTEERS_DETAIL, path: PATH.VOLUNTEERS.DETAIL, element: , }, { - id: 'VOLUNTEER_APP:VOLUNTEERS_SEARCH', + id: PAGE_TYPE.VOLUNTEERS_SEARCH, path: PATH.VOLUNTEERS.SEARCH, element: , }, @@ -50,12 +53,12 @@ export const router: RouterProviderProps['router'] = createBrowserRouter([ path: PATH.ANIMALS.INDEX, children: [ { - id: 'VOLUNTEER_APP:ANIMALS', + id: PAGE_TYPE.ANIMALS, index: true, element: , }, { - id: 'VOLUNTEER_APP:ANIMALS_DETAIL', + id: PAGE_TYPE.ANIMALS_DETAIL, path: PATH.ANIMALS.DETAIL, element: , }, @@ -65,32 +68,34 @@ export const router: RouterProviderProps['router'] = createBrowserRouter([ path: PATH.CHATTINGS.INDEX, children: [ { - id: 'VOLUNTEER_APP:CHATTINGS', + id: PAGE_TYPE.CHATTINGS, index: true, element: , }, { - id: 'VOLUNTEER_APP:CHATTINGS_ROOM', + id: PAGE_TYPE.CHATTINGS_ROOM, path: PATH.CHATTINGS.ROOM, element: , }, ], }, { - id: 'VOLUNTEER_APP:MYPAGE', + id: PAGE_TYPE.MYPAGE, path: PATH.MYPAGE.INDEX, element: , }, { + id: PAGE_TYPE.SETTINGS, path: PATH.SETTINGS.INDEX, + element: , children: [ { - id: 'VOLUNTEER_APP:SETTINGS_ACCOUNT', + id: PAGE_TYPE.SETTINGS_ACCOUNT, path: PATH.SETTINGS.ACCOUNT, element: , }, { - id: 'VOLUNTEER_APP:SETTINGS_PASSWORD', + id: PAGE_TYPE.SETTINGS_PASSWORD, path: PATH.SETTINGS.PASSWORD, element: , }, @@ -100,34 +105,34 @@ export const router: RouterProviderProps['router'] = createBrowserRouter([ path: PATH.SHELTERS.INDEX, children: [ { - id: 'VOLUNTEER_APP:SHELTERS_PROFILE', + id: PAGE_TYPE.SHELTERS_PROFILE, path: PATH.SHELTERS.PROFILE, element: , }, { - id: 'VOLUNTEER_APP:SHELTERS_REVIEWS_WRITE', + id: PAGE_TYPE.SHELTERS_REVIEWS_WRITE, path: PATH.SHELTERS.REVIEWS_WRITE, element: , }, { - id: 'VOLUNTEER_APP:SHELTERS_REVIEWS_UPDATE', + id: PAGE_TYPE.SHELTERS_REVIEWS_UPDATE, path: PATH.SHELTERS.REVIEWS_UPDATE, element: , }, ], }, { - id: 'VOLUNTEER_APP:NOTIFICATIONS', + id: PAGE_TYPE.NOTIFICATIONS, path: PATH.NOTIFICATIONS, element: , }, { - id: 'VOLUNTEER_APP:SIGNUP', + id: PAGE_TYPE.SIGNUP, path: PATH.SIGNUP, element: , }, { - id: 'VOLUNTEER_APP:SIGNIN', + id: PAGE_TYPE.SIGNIN, path: PATH.SIGNIN, element: , }, diff --git a/packages/shared/components/OptionMenu.tsx b/packages/shared/components/OptionMenu.tsx new file mode 100644 index 00000000..6eb80748 --- /dev/null +++ b/packages/shared/components/OptionMenu.tsx @@ -0,0 +1,26 @@ +import { + Image, + Menu, + MenuButton, + MenuButtonProps, + MenuItem, + MenuList, +} from '@chakra-ui/react'; +import { ReactElement } from 'react'; + +import MenuIcon from '../assets/icon_menu.svg'; + +type OptionMenuProps = { + children: ReactElement | ReactElement[]; +} & Omit; + +export default function OptionMenu({ children, ...props }: OptionMenuProps) { + return ( + + + Menu Icon + + {children} + + ); +} diff --git a/packages/shared/constants/headerTitle.ts b/packages/shared/constants/headerTitle.ts new file mode 100644 index 00000000..da89b7bf --- /dev/null +++ b/packages/shared/constants/headerTitle.ts @@ -0,0 +1,36 @@ +import { PageType } from '../types/page'; + +type HeaderTitle = { + [key in PageType]: string; +}; + +const headerTitle: HeaderTitle = { + VOLUNTEERS: '봉사자 모집', + VOLUNTEERS_DETAIL: '봉사자 모집 상세', + VOLUNTEERS_PROFILE: '봉사자 프로필', + VOLUNTEERS_SEARCH: '봉사자 모집글 검색', + VOLUNTEERS_WRITE: '봉사자 모집글 작성', + VOLUNTEERS_UPDATE: '봉사자 모집글 수정', + ANIMALS: '유기보호 동물', + ANIMALS_DETAIL: '유기보호 동물 상세', + ANIMALS_SEARCH: '유기보호 동물 검색', + ANIMALS_WRITE: '유기보호 동물 작성', + ANIMALS_UPDATE: '유기보호 동물 수정', + CHATTINGS: '채팅', + CHATTINGS_ROOM: '채팅방', + MYPAGE: '마이페이지', + MYPAGE_REVIEWS: '봉사 후기', + SETTINGS: '설정', + SETTINGS_ACCOUNT: '계정 정보 수정', + SETTINGS_PASSWORD: '비밀 번호 수정', + MANAGE_ATTENDANCE: '봉사자 출석 관리', + MANAGE_APPLY: '봉사자 신청 현황', + NOTIFICATIONS: '알림', + SHELTERS_PROFILE: '보호소 프로필', + SHELTERS_REVIEWS_WRITE: '봉사 후기 작성', + SHELTERS_REVIEWS_UPDATE: '봉사 후기 수정', + SIGNUP: '회원가입', + SIGNIN: '로그인', +} as const; + +export default headerTitle; diff --git a/packages/shared/constants/pageType.ts b/packages/shared/constants/pageType.ts index c91c1736..0bd02019 100644 --- a/packages/shared/constants/pageType.ts +++ b/packages/shared/constants/pageType.ts @@ -1,108 +1,30 @@ -import HEADER_TYPE from './headerType'; - -const { DEFAULT, DETAIL, SEARCH } = HEADER_TYPE; - const PAGE_TYPE = { - VOLUNTEERS: { - title: '봉사자 모집', - headerType: DEFAULT, - }, - VOLUNTEERS_DETAIL: { - title: '봉사자 모집 상세', - headerType: DETAIL, - }, - VOLUNTEERS_PROFILE: { - title: '봉사자 프로필', - headerType: DETAIL, - }, - VOLUNTEERS_SEARCH: { - title: '봉사자 모집글 검색', - headerType: SEARCH, - }, - VOLUNTEERS_WRITE: { - title: '봉사자 모집글 작성', - headerType: DETAIL, - }, - VOLUNTEERS_UPDATE: { - title: '봉사자 모집글 수정', - headerType: DETAIL, - }, - ANIMALS: { - title: '유기보호 동물', - headerType: DEFAULT, - }, - ANIMALS_DETAIL: { - title: '유기보호 동물 상세', - headerType: DEFAULT, - }, - ANIMALS_SEARCH: { - title: '유기보호 동물 검색', - headerType: SEARCH, - }, - ANIMALS_WRITE: { - title: '유기보호 동물 작성', - headerType: DETAIL, - }, - ANIMALS_UPDATE: { - title: '유기보호 동물 수정', - headerType: DETAIL, - }, - CHATTINGS: { - title: '채팅', - headerType: DEFAULT, - }, - CHATTINGS_ROOM: { - title: '채팅방', - headerType: DETAIL, - }, - MYPAGE: { - title: '마이페이지', - headerType: DEFAULT, - }, - MYPAGE_REVIEWS: { - title: '봉사 후기', - headerType: DETAIL, - }, - SETTINGS_ACCOUNT: { - title: '계정 정보 수정', - headerType: DETAIL, - }, - SETTINGS_PASSWORD: { - title: '비밀 번호 수정', - headerType: DETAIL, - }, - MANAGE_ATTENDANCE: { - title: '봉사자 출석 관리', - headerType: DETAIL, - }, - MANAGE_APPLY: { - title: '봉사자 신청 현황', - headerType: DETAIL, - }, - NOTIFICATIONS: { - title: '알림', - headerType: DETAIL, - }, - SHELTERS_PROFILE: { - title: '보호소 프로필', - headerType: DETAIL, - }, - SHELTERS_REVIEWS_WRITE: { - title: '봉사 후기 작성', - headerType: DETAIL, - }, - SHELTERS_REVIEWS_UPDATE: { - title: '봉사 후기 수정', - headerType: DETAIL, - }, - SIGNUP: { - title: '회원가입', - headerType: DEFAULT, - }, - SIGNIN: { - title: '로그인', - headerType: DEFAULT, - }, + VOLUNTEERS: 'VOLUNTEERS', + VOLUNTEERS_DETAIL: 'VOLUNTEERS_DETAIL', + VOLUNTEERS_PROFILE: 'VOLUNTEERS_PROFILE', + VOLUNTEERS_SEARCH: 'VOLUNTEERS_SEARCH', + VOLUNTEERS_WRITE: 'VOLUNTEERS_WRITE', + VOLUNTEERS_UPDATE: 'VOLUNTEERS_UPDATE', + ANIMALS: 'ANIMALS', + ANIMALS_DETAIL: 'ANIMALS_DETAIL', + ANIMALS_SEARCH: 'ANIMALS_SEARCH', + ANIMALS_WRITE: 'ANIMALS_WRITE', + ANIMALS_UPDATE: 'ANIMALS_UPDATE', + CHATTINGS: 'CHATTINGS', + CHATTINGS_ROOM: 'CHATTINGS_ROOM', + MYPAGE: 'MYPAGE', + MYPAGE_REVIEWS: 'MYPAGE_REVIEWS', + SETTINGS: 'SETTINGS', + SETTINGS_ACCOUNT: 'SETTINGS_ACCOUNT', + SETTINGS_PASSWORD: 'SETTINGS_PASSWORD', + MANAGE_ATTENDANCE: 'MANAGE_ATTENDANCE', + MANAGE_APPLY: 'MANAGE_APPLY', + NOTIFICATIONS: 'NOTIFICATIONS', + SHELTERS_PROFILE: 'SHELTERS_PROFILE', + SHELTERS_REVIEWS_WRITE: 'SHELTERS_REVIEWS_WRITE', + SHELTERS_REVIEWS_UPDATE: 'SHELTERS_REVIEWS_UPDATE', + SIGNUP: 'SIGNUP', + SIGNIN: 'SIGNIN', } as const; export default PAGE_TYPE; diff --git a/packages/shared/hooks/usePageType.ts b/packages/shared/hooks/usePageType.ts new file mode 100644 index 00000000..065b9ea3 --- /dev/null +++ b/packages/shared/hooks/usePageType.ts @@ -0,0 +1,18 @@ +import { useEffect, useState } from 'react'; +import { useMatches } from 'react-router-dom'; + +import { PageType } from '../types/page'; + +export const usePageType = () => { + const [pageType, setPageType] = useState(); + + const match = useMatches().at(-1); + + useEffect(() => { + const page = match?.id; + + setPageType(page as PageType); + }, [match]); + + return { pageType }; +}; diff --git a/packages/shared/layout/BottomNavBar/index.tsx b/packages/shared/layout/BottomNavBar/index.tsx index 2e5c2921..2e9b0f28 100644 --- a/packages/shared/layout/BottomNavBar/index.tsx +++ b/packages/shared/layout/BottomNavBar/index.tsx @@ -1,4 +1,5 @@ import { Flex } from '@chakra-ui/react'; +import { useNavigate } from 'react-router-dom'; import AnimalsSelectedIcon from '../../assets/bottomNavBar/icon_animals_selected.svg'; import AnimalsUnselectedIcon from '../../assets/bottomNavBar/icon_animals_unselected.svg'; @@ -8,15 +9,24 @@ import MyPageSeletedIcon from '../../assets/bottomNavBar/icon_mypage_selected.sv import MyPageUnselectedIcon from '../../assets/bottomNavBar/icon_mypage_unselected.svg'; import VolunteersSelectedIcon from '../../assets/bottomNavBar/icon_volunteers_selected.svg'; import VolunteersUnselectedIcon from '../../assets/bottomNavBar/icon_volunteers_unselected.svg'; +import PAGE_TYPE from '../../constants/pageType'; +import { usePageType } from '../../hooks/usePageType'; import NavBarButton from './NavBarButton'; import { useBottomNavBar } from './useBottomNavBar'; export default function BottomNavBar() { - const { selected, goVounteers, goAnimals, goChattings, goMyPage } = - useBottomNavBar(); + const { pageType } = usePageType(); + const { isBottomNavBarVisible } = useBottomNavBar(pageType); + + const navigate = useNavigate(); + + const goVolunteers = () => navigate('/volunteers'); + const goAnimals = () => navigate('/animals'); + const goChattings = () => navigate('/chattings'); + const goMyPage = () => navigate('/mypage'); return ( - selected && ( + isBottomNavBarVisible && ( { - const [selected, setSelected] = useState< - 'volunteers' | 'animals' | 'chattings' | 'mypage' - >(); - const [pageType, setPageType] = useState(); - - const match = useMatches().at(-1); - const navigate = useNavigate(); +export const useBottomNavBar = (pageType?: PageType) => { + const [isBottomNavBarVisible, setIsBottomNavBarVisible] = useState(false); useEffect(() => { - const [, page] = match?.id?.split(':') ?? [undefined, undefined]; - - setPageType(page as PageType); - }, [match]); - - useEffect(() => { - if (pageType === 'VOLUNTEERS') { - return setSelected('volunteers'); - } - if (pageType === 'ANIMALS') { - return setSelected('animals'); + if ( + pageType === PAGE_TYPE.VOLUNTEERS || + pageType === PAGE_TYPE.ANIMALS || + pageType === PAGE_TYPE.CHATTINGS || + pageType === PAGE_TYPE.MYPAGE + ) { + return setIsBottomNavBarVisible(true); } - if (pageType === 'CHATTINGS') { - return setSelected('chattings'); - } - if (pageType === 'MYPAGE') { - return setSelected('mypage'); - } - return setSelected(undefined); - }, [pageType]); - const goVounteers = () => navigate('/volunteers'); - const goAnimals = () => navigate('/animals'); - const goChattings = () => navigate('/chattings'); - const goMyPage = () => navigate('/mypage'); + return setIsBottomNavBarVisible(false); + }, [pageType]); - return { - selected, - goVounteers, - goAnimals, - goChattings, - goMyPage, - }; + return { isBottomNavBarVisible }; }; diff --git a/packages/shared/layout/Header/DefaultHeader.tsx b/packages/shared/layout/Header/DefaultHeader.tsx deleted file mode 100644 index 97d7765b..00000000 --- a/packages/shared/layout/Header/DefaultHeader.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { ButtonGroup, Flex, Image } from '@chakra-ui/react'; -import { useEffect, useState } from 'react'; - -import NotificationsIcon from '../../assets/icon_notifications.svg'; -import SearchIcon from '../../assets/icon_search.svg'; -import SettingsIcon from '../../assets/icon_settings.svg'; -import { HeaderProps } from '../../types/header'; - -export default function DefaultHeader({ title }: HeaderProps) { - const [iconVisibility, setIconVisibility] = useState({ - searchIcon: false, - settingsIcon: false, - notificationsIcon: true, - }); - - const { searchIcon, settingsIcon, notificationsIcon } = iconVisibility; - - useEffect(() => { - setIconVisibility({ - searchIcon: false, - settingsIcon: false, - notificationsIcon: true, - }); - }, []); - - return ( - - {title} - - {searchIcon && ( - Search Icon - )} - {settingsIcon && ( - Settings Icon - )} - {notificationsIcon && ( - Notifications Icon - )} - - - ); -} diff --git a/packages/shared/layout/Header/DefaultHeader/headerIconState.ts b/packages/shared/layout/Header/DefaultHeader/headerIconState.ts new file mode 100644 index 00000000..7a296f32 --- /dev/null +++ b/packages/shared/layout/Header/DefaultHeader/headerIconState.ts @@ -0,0 +1,85 @@ +import { AppType } from '../../../types/app'; +import { DefaultHeaderIconVisibility } from './useDefaultHeader'; + +type DefaultHeaderIconState = { + [key: string]: { + [key in AppType]: DefaultHeaderIconVisibility; + }; +}; + +const defaultHeaderIconState: DefaultHeaderIconState = { + VOLUNTEERS: { + SHELTER_APP: { + searchIcon: true, + settingsIcon: false, + notificationsIcon: true, + }, + VOLUNTEER_APP: { + searchIcon: true, + settingsIcon: false, + notificationsIcon: true, + }, + }, + ANIMALS: { + SHELTER_APP: { + searchIcon: true, + settingsIcon: false, + notificationsIcon: true, + }, + VOLUNTEER_APP: { + searchIcon: false, + settingsIcon: false, + notificationsIcon: true, + }, + }, + CHATTINGS: { + SHELTER_APP: { + searchIcon: false, + settingsIcon: false, + notificationsIcon: true, + }, + VOLUNTEER_APP: { + searchIcon: false, + settingsIcon: false, + notificationsIcon: true, + }, + }, + MYPAGE: { + SHELTER_APP: { + searchIcon: false, + settingsIcon: false, + notificationsIcon: true, + }, + VOLUNTEER_APP: { + searchIcon: false, + settingsIcon: true, + notificationsIcon: true, + }, + }, + SIGNUP: { + SHELTER_APP: { + searchIcon: false, + settingsIcon: false, + notificationsIcon: false, + }, + VOLUNTEER_APP: { + searchIcon: false, + settingsIcon: false, + notificationsIcon: false, + }, + }, + SIGNIN: { + SHELTER_APP: { + searchIcon: false, + settingsIcon: false, + notificationsIcon: false, + }, + VOLUNTEER_APP: { + searchIcon: false, + settingsIcon: false, + notificationsIcon: false, + }, + }, +} as const; + +export default defaultHeaderIconState; diff --git a/packages/shared/layout/Header/DefaultHeader/index.tsx b/packages/shared/layout/Header/DefaultHeader/index.tsx new file mode 100644 index 00000000..7503836f --- /dev/null +++ b/packages/shared/layout/Header/DefaultHeader/index.tsx @@ -0,0 +1,56 @@ +import { Box, ButtonGroup, Flex, Image, Text } from '@chakra-ui/react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import NotificationsIcon from '../../../assets/icon_notifications.svg'; +import SearchIcon from '../../../assets/icon_search.svg'; +import SettingsIcon from '../../../assets/icon_settings.svg'; +import { HeaderProps } from '../index'; +import { useDefaultHeader } from './useDefaultHeader'; + +export default function DefaultHeader({ appType }: HeaderProps) { + const navigate = useNavigate(); + const { pathname } = useLocation(); + const { title, iconVisibility } = useDefaultHeader(appType); + + const { searchIcon, settingsIcon, notificationsIcon } = iconVisibility; + + const goSearch = () => navigate(`${pathname}/search`); + const goSettings = () => navigate('/settings'); + const goNotifications = () => navigate('/notifications'); + + return ( + + + {title} + + + {searchIcon && ( + + Search Icon + + )} + {settingsIcon && ( + + Settings Icon + + )} + {notificationsIcon && ( + + Notifications Icon + + )} + + + ); +} diff --git a/packages/shared/layout/Header/DefaultHeader/useDefaultHeader.ts b/packages/shared/layout/Header/DefaultHeader/useDefaultHeader.ts new file mode 100644 index 00000000..de7a25d7 --- /dev/null +++ b/packages/shared/layout/Header/DefaultHeader/useDefaultHeader.ts @@ -0,0 +1,37 @@ +import { useEffect, useState } from 'react'; + +import { usePageType } from '../../../hooks/usePageType'; +import { AppType } from '../../../types/app'; +import { getHeaderTitle } from '../utils'; +import defaultHeaderState from './headerIconState'; + +export type DefaultHeaderIconVisibility = { + searchIcon: boolean; + settingsIcon: boolean; + notificationsIcon: boolean; +}; + +export const useDefaultHeader = (appType: AppType) => { + const { pageType } = usePageType(); + const [title, setTitle] = useState(''); + const [iconVisibility, setIconVisibility] = + useState({ + searchIcon: false, + settingsIcon: false, + notificationsIcon: false, + }); + + useEffect(() => { + if (pageType) { + setTitle(getHeaderTitle(pageType)); + + const iconState = defaultHeaderState[pageType]; + + if (iconState) { + setIconVisibility(iconState[appType]); + } + } + }, [appType, pageType]); + + return { title, iconVisibility }; +}; diff --git a/packages/shared/layout/Header/DetailHeader.tsx b/packages/shared/layout/Header/DetailHeader.tsx deleted file mode 100644 index 5e85c7e0..00000000 --- a/packages/shared/layout/Header/DetailHeader.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Flex, Image } from '@chakra-ui/react'; -import { useEffect, useState } from 'react'; - -import BackIcon from '../../assets/icon_back.svg'; -import MenuIcon from '../../assets/icon_menu.svg'; -import { HeaderProps } from '../../types/header'; - -export default function DetailHeader({ title }: HeaderProps) { - const [iconVisibility, setIconVisibility] = useState({ menuIcon: false }); - const { menuIcon } = iconVisibility; - - useEffect(() => { - setIconVisibility({ menuIcon: false }); - }, []); - - return ( - - Back Icon - {title} - {menuIcon && ( - Menu Icon - )} - - ); -} diff --git a/packages/shared/layout/Header/DetailHeader/index.tsx b/packages/shared/layout/Header/DetailHeader/index.tsx new file mode 100644 index 00000000..e9048d47 --- /dev/null +++ b/packages/shared/layout/Header/DetailHeader/index.tsx @@ -0,0 +1,60 @@ +import { Box, Flex, Image, MenuItem, Text } from '@chakra-ui/react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import BackIcon from '../../../assets/icon_back.svg'; +import OptionMenu from '../../../components/OptionMenu'; +import useDetailHeaderStore from '../../../store/detailHeaderStore'; +import { HeaderProps } from '../index'; +import { useDetailHeader } from './useDetailHeader'; + +export default function DetailHeader({ appType }: HeaderProps) { + const navigate = useNavigate(); + const { pathname } = useLocation(); + const { title, iconVisibility } = useDetailHeader(appType); + const onDelete = useDetailHeaderStore((state) => state.onDelete); + + const { menuIcon } = iconVisibility; + + const goBack = () => navigate(-1); + + const handleUpdate = () => { + const [, path, id] = pathname.split('/'); + + navigate(`${path}/write/${id}`); + }; + + const handleDelete = () => { + const [, , id] = pathname.split('/'); + + onDelete(Number(id)); + }; + + return ( + + + Back Icon + + + {title} + + {menuIcon && ( + + 수정하기 + 삭제하기 + + )} + + ); +} diff --git a/packages/shared/layout/Header/DetailHeader/useDetailHeader.ts b/packages/shared/layout/Header/DetailHeader/useDetailHeader.ts new file mode 100644 index 00000000..64eff87b --- /dev/null +++ b/packages/shared/layout/Header/DetailHeader/useDetailHeader.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react'; + +import APP_TYPE from '../../../constants/appType'; +import PAGE_TYPE from '../../../constants/pageType'; +import { usePageType } from '../../../hooks/usePageType'; +import { AppType } from '../../../types/app'; +import { getHeaderTitle } from '../utils'; + +export type DetailHeaderIconVisibility = { + menuIcon: boolean; +}; + +export const useDetailHeader = (appType: AppType) => { + const { pageType } = usePageType(); + const [title, setTitle] = useState(''); + const [iconVisibility, setIconVisibility] = + useState({ menuIcon: false }); + + useEffect(() => { + if (pageType) { + setTitle(getHeaderTitle(pageType)); + + if ( + appType === APP_TYPE.SHELTER_APP && + (pageType === PAGE_TYPE.VOLUNTEERS_DETAIL || + pageType === PAGE_TYPE.ANIMALS_DETAIL) + ) { + setIconVisibility({ menuIcon: true }); + } else { + setIconVisibility({ menuIcon: false }); + } + } + }, [appType, pageType]); + + return { title, iconVisibility }; +}; diff --git a/packages/shared/layout/Header/SearchHeader.tsx b/packages/shared/layout/Header/SearchHeader.tsx deleted file mode 100644 index 2eddc857..00000000 --- a/packages/shared/layout/Header/SearchHeader.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Flex } from '@chakra-ui/react'; - -import { HeaderProps } from '../../types/header'; - -export default function SearchHeader({ title }: HeaderProps) { - return ( - - {title} - - ); -} diff --git a/packages/shared/layout/Header/SearchHeader/index.tsx b/packages/shared/layout/Header/SearchHeader/index.tsx new file mode 100644 index 00000000..f3a24008 --- /dev/null +++ b/packages/shared/layout/Header/SearchHeader/index.tsx @@ -0,0 +1,67 @@ +import { Box, Flex, FormControl, Image, Input } from '@chakra-ui/react'; +import { ChangeEvent, FormEvent } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import BackIcon from '../../../assets/icon_back.svg'; +import useSearchHeaderStore from '../../../store/searchHeaderStore'; + +export default function SearchHeader() { + const navigate = useNavigate(); + const [keyword, setKeyword, onSearch] = useSearchHeaderStore((state) => [ + state.keyword, + state.setKeyword, + state.onSearch, + ]); + + const goBack = () => navigate(-1); + + const handleChangeKeyword = (event: ChangeEvent) => { + const { value } = event.target; + setKeyword(value); + }; + + const handleSubmit = (event: FormEvent) => { + event.preventDefault(); + onSearch(keyword); + }; + + return ( + + + Back Icon + + + + + + ); +} diff --git a/packages/shared/layout/Header/index.tsx b/packages/shared/layout/Header/index.tsx index 4fdf9984..5f4c9f5d 100644 --- a/packages/shared/layout/Header/index.tsx +++ b/packages/shared/layout/Header/index.tsx @@ -1,4 +1,5 @@ -import { CommonHeaderProps, HeaderProps } from '../../types/header'; +import { usePageType } from '../../hooks/usePageType'; +import { AppType } from '../../types/app'; import DefaultHeader from './DefaultHeader'; import DetailHeader from './DetailHeader'; import SearchHeader from './SearchHeader'; @@ -7,11 +8,20 @@ import { useHeader } from './useHeader'; const Headers = { DEFAULT: (props: HeaderProps) => , DETAIL: (props: HeaderProps) => , - SEARCH: (props: HeaderProps) => , + SEARCH: () => , }; -export default function Header({ headerOption }: CommonHeaderProps) { - const { headerType, title } = useHeader(); +export type HeaderProps = { + appType: AppType; +}; + +export default function Header({ appType }: HeaderProps) { + const { pageType } = usePageType(); + const { headerType } = useHeader(); + + if (!pageType) { + return null; + } - return Headers[headerType]({ title, headerOption }); + return Headers[headerType]({ appType }); } diff --git a/packages/shared/layout/Header/useHeader.ts b/packages/shared/layout/Header/useHeader.ts index abd6b2ba..6a283b0d 100644 --- a/packages/shared/layout/Header/useHeader.ts +++ b/packages/shared/layout/Header/useHeader.ts @@ -1,35 +1,19 @@ import { useEffect, useState } from 'react'; -import { useMatches } from 'react-router-dom'; -import { AppType } from '../../types/app'; +import HEADER_TYPE from '../../constants/headerType'; +import { usePageType } from '../../hooks/usePageType'; import { HeaderType } from '../../types/header'; -import { PageType } from '../../types/page'; -import { getHeaderTitle, getHeaderType } from './utils'; +import { getHeaderType } from './utils'; export const useHeader = () => { - const [appType, setAppType] = useState(); - const [pageType, setPageType] = useState(); - const [headerType, setHeaderType] = useState('DEFAULT'); - const [title, setTitle] = useState(''); - - const match = useMatches().at(-1); - - useEffect(() => { - const [app, page] = match?.id?.split(':') ?? [undefined, undefined]; - - setAppType(app as AppType); - setPageType(page as PageType); - }, [match]); + const { pageType } = usePageType(); + const [headerType, setHeaderType] = useState(HEADER_TYPE.DEFAULT); useEffect(() => { if (pageType) { - setTitle(getHeaderTitle(pageType)); setHeaderType(getHeaderType(pageType)); } - }, [appType, pageType]); + }, [pageType]); - return { - headerType, - title, - }; + return { headerType }; }; diff --git a/packages/shared/layout/Header/utils.ts b/packages/shared/layout/Header/utils.ts index 6a93e37d..f599537e 100644 --- a/packages/shared/layout/Header/utils.ts +++ b/packages/shared/layout/Header/utils.ts @@ -1,8 +1,28 @@ +import headerTitle from '../../constants/headerTitle'; +import HEADER_TYPE from '../../constants/headerType'; import PAGE_TYPE from '../../constants/pageType'; import { HeaderType } from '../../types/header'; import { PageType } from '../../types/page'; -export const getHeaderType = (pageType: PageType): HeaderType => - PAGE_TYPE[pageType].headerType; +export const getHeaderType = (pageType: PageType): HeaderType => { + if ( + pageType === PAGE_TYPE.VOLUNTEERS || + pageType === PAGE_TYPE.ANIMALS || + pageType === PAGE_TYPE.CHATTINGS || + pageType === PAGE_TYPE.MYPAGE + ) { + return HEADER_TYPE.DEFAULT; + } -export const getHeaderTitle = (pageType: PageType) => PAGE_TYPE[pageType].title; + if ( + pageType === PAGE_TYPE.VOLUNTEERS_SEARCH || + pageType === PAGE_TYPE.ANIMALS_SEARCH + ) { + return HEADER_TYPE.SEARCH; + } + + return HEADER_TYPE.DETAIL; +}; + +export const getHeaderTitle = (pageType: PageType): string => + headerTitle[pageType]; diff --git a/packages/shared/layout/index.tsx b/packages/shared/layout/index.tsx index a76fdd4a..7a17040e 100644 --- a/packages/shared/layout/index.tsx +++ b/packages/shared/layout/index.tsx @@ -1,13 +1,18 @@ import { Box, Container } from '@chakra-ui/react'; import { Outlet } from 'react-router-dom'; +import { AppType } from '../types/app'; import BottomNavBar from './BottomNavBar'; import Header from './Header'; -export default function Layout() { +type LayoutProps = { + appType: AppType; +}; + +export default function Layout({ appType }: LayoutProps) { return ( -
+
diff --git a/packages/shared/package.json b/packages/shared/package.json index 669c3cbd..488ccc79 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -15,7 +15,8 @@ "framer-motion": "^10.16.4", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-router-dom": "^6.17.0" + "react-router-dom": "^6.17.0", + "zustand": "^4.4.4" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/packages/shared/store/detailHeaderStore.ts b/packages/shared/store/detailHeaderStore.ts new file mode 100644 index 00000000..6caad5c9 --- /dev/null +++ b/packages/shared/store/detailHeaderStore.ts @@ -0,0 +1,19 @@ +import { create } from 'zustand'; + +type DeleteFunction = (id: number) => void; +interface DetailHeaderState { + onDelete: DeleteFunction; +} + +interface DetailHeaderActions { + setOnDelete: (onDelete: DeleteFunction) => void; +} + +const useDetailHeaderStore = create( + (set) => ({ + onDelete: () => {}, + setOnDelete: (onDelete: DeleteFunction) => set(() => ({ onDelete })), + }), +); + +export default useDetailHeaderStore; diff --git a/packages/shared/store/searchHeaderStore.ts b/packages/shared/store/searchHeaderStore.ts new file mode 100644 index 00000000..38e94ec4 --- /dev/null +++ b/packages/shared/store/searchHeaderStore.ts @@ -0,0 +1,24 @@ +import { create } from 'zustand'; + +type SearchFunction = (keyword: string) => void; + +interface SearchHeaderState { + keyword: string; + onSearch: SearchFunction; +} + +interface SearchHeaderActions { + setKeyword: (keyword: string) => void; + setOnSearch: (onSearch: SearchFunction) => void; +} + +const useSearchHeaderStore = create( + (set) => ({ + keyword: '', + onSearch: () => {}, + setKeyword: (keyword: string) => set(() => ({ keyword })), + setOnSearch: (onSearch: SearchFunction) => set(() => ({ onSearch })), + }), +); + +export default useSearchHeaderStore; diff --git a/packages/shared/types/header.ts b/packages/shared/types/header.ts index d642388f..3ded0871 100644 --- a/packages/shared/types/header.ts +++ b/packages/shared/types/header.ts @@ -1,17 +1,3 @@ import HEADER_TYPE from '../constants/headerType'; export type HeaderType = keyof typeof HEADER_TYPE; - -export type HeaderOption = { - onMenuClick?: VoidFunction; - onSubmit?: (keyword: string) => void; -}; - -export type CommonHeaderProps = { - headerOption?: HeaderOption; -}; - -export type HeaderProps = { - title: string; - headerOption?: HeaderOption; -}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 414b88fc..10d771a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -272,6 +272,9 @@ importers: react-router-dom: specifier: ^6.17.0 version: 6.18.0(react-dom@18.2.0)(react@18.2.0) + zustand: + specifier: ^4.4.4 + version: 4.4.5(@types/react@18.2.33)(react@18.2.0) devDependencies: '@types/react': specifier: ^18.2.15