From 0419250a3716acfcf2d1c84de5b6a87cc7b6f2df Mon Sep 17 00:00:00 2001 From: Cho-heejung <66050038+he2e2@users.noreply.github.com> Date: Fri, 29 Nov 2024 14:46:05 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=EA=B2=80=EC=83=89=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B0=8F=20=EC=A0=84=EC=97=AD=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EC=88=98=EC=A0=95=20(#60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 헤더 검색, 좋아요, 로그인으로 변경 * feat: 검색 바 * feat: tab 레이아웃 * feat: 검색 전체 레이아웃 * feat: gatheringCard CSS * feat: useCustomInfiniteQuery hook * feat: 페이지 이동 및 새로고침 시, 상태 관리 로직 추가 --- eslint.config.mjs | 3 +- src/app/appRouter.tsx | 5 + src/features/archive/archive.api.ts | 11 +++ src/features/archive/archive.hook.ts | 91 ++++-------------- src/features/archive/model/archive.store.ts | 93 +++++++++++-------- src/features/index.ts | 2 +- src/features/search/index.ts | 1 + src/features/search/ui/SearchBar.module.scss | 17 ++++ src/features/search/ui/SearchBar.tsx | 26 ++++++ src/features/search/ui/index.ts | 1 + src/pages/ArchiveListPage/ArchiveListPage.tsx | 4 +- .../DetailArchivePage/DetailArchivePage.tsx | 3 +- src/pages/SearchPage/SearchPage.module.scss | 12 +++ src/pages/SearchPage/SearchPage.tsx | 18 ++++ src/pages/index.ts | 3 +- src/shared/hook/index.ts | 2 + src/shared/hook/useCustomInfiniteQuery.ts | 47 ++++++++++ src/shared/hook/usePageLifecycle.ts | 52 +++++++++++ .../GatheringCard/GatheringCard.module.scss | 16 ++-- src/shared/ui/GatheringCard/GatheringCard.tsx | 10 +- .../ui/SidebarFilter.module.scss | 16 +++- .../GatheringGrid/GatheringGrid.module.scss | 20 +--- src/widgets/Layout/index.tsx | 12 +++ .../Layout/ui/Header/Header.module.scss | 12 +++ src/widgets/Layout/ui/Header/Header.tsx | 17 +++- src/widgets/SearchAll/SearchAll.module.scss | 40 ++++++++ src/widgets/SearchAll/SearchAll.tsx | 70 ++++++++++++++ src/widgets/SearchTap/SearchTap.module.scss | 43 +++++++++ src/widgets/SearchTap/SearchTap.tsx | 72 ++++++++++++++ src/widgets/index.ts | 2 + 30 files changed, 574 insertions(+), 147 deletions(-) create mode 100644 src/features/search/index.ts create mode 100644 src/features/search/ui/SearchBar.module.scss create mode 100644 src/features/search/ui/SearchBar.tsx create mode 100644 src/features/search/ui/index.ts create mode 100644 src/pages/SearchPage/SearchPage.module.scss create mode 100644 src/pages/SearchPage/SearchPage.tsx create mode 100644 src/shared/hook/useCustomInfiniteQuery.ts create mode 100644 src/shared/hook/usePageLifecycle.ts create mode 100644 src/widgets/SearchAll/SearchAll.module.scss create mode 100644 src/widgets/SearchAll/SearchAll.tsx create mode 100644 src/widgets/SearchTap/SearchTap.module.scss create mode 100644 src/widgets/SearchTap/SearchTap.tsx diff --git a/eslint.config.mjs b/eslint.config.mjs index 5c963cf..c11ebfb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -50,8 +50,9 @@ const typescriptConfig = { }, rules: { '@typescript-eslint/no-unsafe-assignment': 'off', - '@typescript-eslint/no-unsafe-call': 'warn', + '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-return': 'warn', '@typescript-eslint/no-floating-promises': 'warn', '@typescript-eslint/no-unused-vars': 'error', diff --git a/src/app/appRouter.tsx b/src/app/appRouter.tsx index bc611b8..86168f5 100644 --- a/src/app/appRouter.tsx +++ b/src/app/appRouter.tsx @@ -7,6 +7,7 @@ import { WriteGatheringPage, WriteArchivePage, RegisterPage, + SearchPage, } from '@/pages'; import { Layout } from '@/widgets'; @@ -43,6 +44,10 @@ const AppRouter = () => { path: '/gathering/write', element: , }, + { + path: '/search', + element: , + }, { path: '/user', element: <>{/** userPage */}, diff --git a/src/features/archive/archive.api.ts b/src/features/archive/archive.api.ts index 861dd8d..de91149 100644 --- a/src/features/archive/archive.api.ts +++ b/src/features/archive/archive.api.ts @@ -58,3 +58,14 @@ export const getArchiveList = (sort: string, page: number, color?: Color | null) }, }) .then(res => res.data); + +export const getSearchArchive = (searchKeyword: string, page: number) => + api + .get('/archive/search', { + params: { + searchKeyword, + page, + size: 9, + }, + }) + .then(res => res.data); diff --git a/src/features/archive/archive.hook.ts b/src/features/archive/archive.hook.ts index 69a61b6..7a8dbfd 100644 --- a/src/features/archive/archive.hook.ts +++ b/src/features/archive/archive.hook.ts @@ -1,5 +1,4 @@ -import { useMutation, useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query'; -import { useMemo } from 'react'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { deleteArchive, @@ -22,7 +21,7 @@ import type { } from './archive.dto'; import type { Color } from './colors.type'; -import { useIntersectionObserver } from '@/shared/hook'; +import { useCustomInfiniteQuery } from '@/shared/hook'; export const useCreateArchive = () => useMutation({ @@ -45,42 +44,12 @@ export const useArchive = (archiveId: number) => queryFn: () => getArchive(archiveId), }); -export const useComments = (archiveId: number, enabled: boolean = false) => { - const { data, fetchNextPage, isLoading, isError, isFetchingNextPage } = useInfiniteQuery< - GetCommentsApiResponse, - Error - >({ - queryKey: ['/archive', archiveId, 'comment'], - queryFn: ({ pageParam = 0 }) => getComments(archiveId, 10, pageParam as number), - enabled, - getNextPageParam: (lastPage, allPages) => { - if (Array.isArray(lastPage.data)) { - const isLastPage = lastPage.data?.length < 10; - return isLastPage ? null : allPages.length; - } - return null; - }, - initialPageParam: 0, - }); - - const items = useMemo(() => { - const temp: Comment[] = []; - data?.pages.forEach(page => { - page.data?.forEach(comment => { - temp.push(comment); - }); - }); - return temp; - }, [data]); - - const ref = useIntersectionObserver( - () => { - void fetchNextPage(); - }, - { threshold: 1.0 }, +export const useComments = (archiveId: number) => { + return useCustomInfiniteQuery( + ['/archive', archiveId, 'comment'], + ({ pageParam }) => getComments(archiveId, 10, pageParam), + 10, ); - - return { items, isFetchingNextPage, isLoading, isError, ref, fetchNextPage }; }; export const useCreateComment = (archiveId: number) => @@ -133,40 +102,18 @@ export const usePopularArchiveList = () => queryFn: () => getPopularlityArchiveList(), }); -export const useArchiveList = (sort: string, color?: Color | 'default') => { - const { data, fetchNextPage, isLoading, isError, isFetchingNextPage } = useInfiniteQuery< - GetArchiveListApiResponse, - Error - >({ - queryKey: ['/archive', sort, color], - queryFn: ({ pageParam = 0 }) => - getArchiveList(sort, pageParam as number, color === 'default' ? null : color), - getNextPageParam: (lastPage, allPages) => { - if (Array.isArray(lastPage.data)) { - const isLastPage = lastPage.data?.length < 9; - return isLastPage ? null : allPages.length; - } - return null; - }, - initialPageParam: 0, - }); - - const items = useMemo(() => { - const temp: ArchiveCardDTO[] = []; - data?.pages.forEach(page => { - page.data?.forEach(archive => { - temp.push(archive); - }); - }); - return temp; - }, [data]); - - const ref = useIntersectionObserver( - () => { - void fetchNextPage(); - }, - { threshold: 1.0 }, +export const useArchiveList = (sort: string, color: Color | 'default') => { + return useCustomInfiniteQuery( + ['/archive', sort, color], + ({ pageParam }) => getArchiveList(sort, pageParam, color === 'default' ? null : color), + 9, ); +}; - return { items, isFetchingNextPage, isLoading, isError, ref, fetchNextPage }; +export const useSearchArchive = (searchKeyword: string) => { + return useCustomInfiniteQuery( + ['/archive/search', searchKeyword], + ({ pageParam }) => getArchiveList(searchKeyword, pageParam), + 9, + ); }; diff --git a/src/features/archive/model/archive.store.ts b/src/features/archive/model/archive.store.ts index 547d6a5..3bafbeb 100644 --- a/src/features/archive/model/archive.store.ts +++ b/src/features/archive/model/archive.store.ts @@ -1,5 +1,6 @@ import { produce } from 'immer'; import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; import type { BaseArchiveDTO } from '../archive.dto'; import type { Color } from '../colors.type'; @@ -13,6 +14,7 @@ interface ArchiveStore { resetArchiveData: () => void; color: Color | null; setColor: (color: Color) => void; + clearStorage: () => void; } export const initialArchiveState: BaseArchiveDTO = { @@ -24,44 +26,61 @@ export const initialArchiveState: BaseArchiveDTO = { imageUrls: [{ url: 'https://source.unsplash.com/random/800x600' }], }; -export const useArchiveStore = create(set => ({ - archiveId: 0, - setArchiveId: id => { - set(() => ({ - archiveId: id, - })); - }, +export const useArchiveStore = create( + persist( + set => ({ + archiveId: 0, + setArchiveId: id => { + set(() => ({ + archiveId: id, + })); + }, - archiveData: initialArchiveState, - setArchiveData: newData => { - set(() => ({ - archiveData: newData, - })); - }, + archiveData: initialArchiveState, + setArchiveData: newData => { + set(() => ({ + archiveData: newData, + })); + }, - updateArchiveData: (key, value) => { - set( - produce((state: ArchiveStore) => { - state.archiveData[key] = value; - }), - ); - }, + updateArchiveData: (key, value) => { + set( + produce((state: ArchiveStore) => { + state.archiveData[key] = value; + }), + ); + }, - resetArchiveData: () => { - set(() => ({ - archiveData: initialArchiveState, - })); - }, + resetArchiveData: () => { + set(() => ({ + archiveData: initialArchiveState, + })); + }, + + color: null, + setColor: color => { + set( + produce((state: ArchiveStore) => { + state.archiveData.type = color; + }), + ); + set(() => ({ + color, + })); + }, - color: null, - setColor: color => { - set( - produce((state: ArchiveStore) => { - state.archiveData.type = color; - }), - ); - set(() => ({ - color, - })); - }, -})); + clearStorage: () => { + set(() => ({ + archiveId: 0, + archiveData: initialArchiveState, + color: null, + })); + useArchiveStore.persist.clearStorage(); + }, + }), + { + name: 'archive-storage', + storage: createJSONStorage(() => sessionStorage), + }, + ), +); diff --git a/src/features/index.ts b/src/features/index.ts index c6c9fbc..c8ba2f4 100644 --- a/src/features/index.ts +++ b/src/features/index.ts @@ -1,3 +1,3 @@ export * from './archive'; export * from './gathering'; - +export * from './search'; diff --git a/src/features/search/index.ts b/src/features/search/index.ts new file mode 100644 index 0000000..5ecdd1f --- /dev/null +++ b/src/features/search/index.ts @@ -0,0 +1 @@ +export * from './ui'; diff --git a/src/features/search/ui/SearchBar.module.scss b/src/features/search/ui/SearchBar.module.scss new file mode 100644 index 0000000..ad3e8c2 --- /dev/null +++ b/src/features/search/ui/SearchBar.module.scss @@ -0,0 +1,17 @@ +.container { + display: flex; + align-items: center; + justify-content: space-between; + width: 40%; + min-width: 300px; + padding: 0.8rem 1.3rem; + color: $secondary-color; + background-color: $primary-color; + border-radius: 20px; + + input { + flex: 1; + padding: 0.5rem; + color: $secondary-color; + } +} diff --git a/src/features/search/ui/SearchBar.tsx b/src/features/search/ui/SearchBar.tsx new file mode 100644 index 0000000..7817c4e --- /dev/null +++ b/src/features/search/ui/SearchBar.tsx @@ -0,0 +1,26 @@ +import { faSearch } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import styles from './SearchBar.module.scss'; + +export const SearchBar = ({ + searchText, + setSearchText, +}: { + searchText: string; + setSearchText: (t: string) => void; +}) => { + return ( +
+ { + setSearchText(e.target.value); + }} + placeholder='검색어를 입력해주세요' + type='text' + value={searchText} + /> + +
+ ); +}; diff --git a/src/features/search/ui/index.ts b/src/features/search/ui/index.ts new file mode 100644 index 0000000..3d7e42e --- /dev/null +++ b/src/features/search/ui/index.ts @@ -0,0 +1 @@ +export * from './SearchBar'; diff --git a/src/pages/ArchiveListPage/ArchiveListPage.tsx b/src/pages/ArchiveListPage/ArchiveListPage.tsx index b9a4c5d..79b0002 100644 --- a/src/pages/ArchiveListPage/ArchiveListPage.tsx +++ b/src/pages/ArchiveListPage/ArchiveListPage.tsx @@ -5,7 +5,7 @@ import styles from './ArchiveListPage.module.scss'; import type { Color } from '@/features'; import { ColorSelect, useArchiveList } from '@/features'; -import { Button, SelectBtn } from '@/shared/ui'; +import { Button, SelectBtn, TripleDot } from '@/shared/ui'; import { ArchiveGrid } from '@/widgets'; export const ArchiveListPage = () => { @@ -45,7 +45,7 @@ export const ArchiveListPage = () => {
- {isFetchingNextPage && '로딩중...'} + {isFetchingNextPage && }
); diff --git a/src/pages/DetailArchivePage/DetailArchivePage.tsx b/src/pages/DetailArchivePage/DetailArchivePage.tsx index 59c7e08..1aca0bd 100644 --- a/src/pages/DetailArchivePage/DetailArchivePage.tsx +++ b/src/pages/DetailArchivePage/DetailArchivePage.tsx @@ -5,6 +5,7 @@ import styles from './DetailArchivePage.module.scss'; import { worker } from '../../mocks/browser'; import { MarkdownPreview, WriteComment, CommentItem, useArchive, useComments } from '@/features'; +import { TripleDot } from '@/shared/ui'; import { DetailHeader } from '@/widgets'; export const DetailArchivePage = () => { @@ -48,7 +49,7 @@ export const DetailArchivePage = () => { items.map(comment => ( ))} - {archive &&
{isFetchingNextPage &&

Loading more comments...

}
} + {archive &&
{isFetchingNextPage && }
} ); diff --git a/src/pages/SearchPage/SearchPage.module.scss b/src/pages/SearchPage/SearchPage.module.scss new file mode 100644 index 0000000..930ca4c --- /dev/null +++ b/src/pages/SearchPage/SearchPage.module.scss @@ -0,0 +1,12 @@ +.wrapper { + display: flex; + flex: 1; + flex-direction: column; + gap: 2rem; + align-items: center; + padding: 4rem 8rem; + + @media (width <= 768px) { + padding: 4rem; + } +} diff --git a/src/pages/SearchPage/SearchPage.tsx b/src/pages/SearchPage/SearchPage.tsx new file mode 100644 index 0000000..e22a15f --- /dev/null +++ b/src/pages/SearchPage/SearchPage.tsx @@ -0,0 +1,18 @@ +import { useState } from 'react'; + +import styles from './SearchPage.module.scss'; + +import { SearchBar } from '@/features'; +import { SearchTap } from '@/widgets'; + +export const SearchPage = () => { + const [searchText, setSearchText] = useState(''); + const [activeTab, setActiveTab] = useState('전체'); + + return ( +
+ + +
+ ); +}; diff --git a/src/pages/index.ts b/src/pages/index.ts index fb37de5..17ed4ff 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -3,4 +3,5 @@ export { WriteArchivePage } from './WriteArchivePage/WriteArchivePage'; export { DetailArchivePage } from './DetailArchivePage/DetailArchivePage'; export { RegisterPage } from './RegisterPage/RegisterPage'; export { WriteGatheringPage } from './WriteGatheringPage/WriteGatheringPage'; -export { ArchiveListPage } from './ArchiveListPage/ArchiveListPage'; \ No newline at end of file +export { SearchPage } from './SearchPage/SearchPage'; +export { ArchiveListPage } from './ArchiveListPage/ArchiveListPage'; diff --git a/src/shared/hook/index.ts b/src/shared/hook/index.ts index 97e1994..77612d6 100644 --- a/src/shared/hook/index.ts +++ b/src/shared/hook/index.ts @@ -1 +1,3 @@ export * from './useIntersectionObserver'; +export * from './useCustomInfiniteQuery'; +export * from './usePageLifecycle'; diff --git a/src/shared/hook/useCustomInfiniteQuery.ts b/src/shared/hook/useCustomInfiniteQuery.ts new file mode 100644 index 0000000..09ff5d3 --- /dev/null +++ b/src/shared/hook/useCustomInfiniteQuery.ts @@ -0,0 +1,47 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +import { useIntersectionObserver } from './useIntersectionObserver'; + +export const useCustomInfiniteQuery = ( + queryKey: (string | number)[], + queryFn: (context: { pageParam: number }) => Promise, + pageSize = 9, + enabled: boolean = false, +) => { + const { data, fetchNextPage, isLoading, isError, isFetchingNextPage } = useInfiniteQuery< + TData, + TError + >({ + queryKey, + queryFn: ({ pageParam = 0 }) => queryFn({ pageParam: pageParam as number }), + getNextPageParam: (lastPage, allPages) => { + if (Array.isArray(lastPage.data)) { + const isLastPage = lastPage.data?.length < pageSize; + return isLastPage ? null : allPages.length; + } + return null; + }, + initialPageParam: 0, + enabled: enabled, + }); + + const items = useMemo(() => { + const temp: TItem[] = []; + data?.pages.forEach(page => { + page.data?.forEach(item => { + temp.push(item); + }); + }); + return temp; + }, [data]); + + const ref = useIntersectionObserver( + () => { + void fetchNextPage(); + }, + { threshold: 1.0 }, + ); + + return { items, isFetchingNextPage, isLoading, isError, ref, fetchNextPage }; +}; diff --git a/src/shared/hook/usePageLifecycle.ts b/src/shared/hook/usePageLifecycle.ts new file mode 100644 index 0000000..ba140e2 --- /dev/null +++ b/src/shared/hook/usePageLifecycle.ts @@ -0,0 +1,52 @@ +import { useEffect, useRef } from 'react'; +import { useLocation } from 'react-router-dom'; + +type UsePageLifecycleOptions = { + onNavigate?: () => void; + onBack?: () => void; + onRefresh?: () => void; +}; + +export const usePageLifecycle = ({ onNavigate, onBack, onRefresh }: UsePageLifecycleOptions) => { + const location = useLocation(); + const previousPath = useRef(location.pathname); + const previousKey = useRef(location.key); + + useEffect(() => { + const handleBeforeUnload = (event: BeforeUnloadEvent) => { + if (onRefresh) { + onRefresh(); + event.preventDefault(); + } + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, [onRefresh]); + + useEffect(() => { + if (location.pathname !== previousPath.current) { + previousPath.current = location.pathname; + previousKey.current = location.key; + + if (previousPath.current.startsWith('/archive/write')) return; + + if (onNavigate) onNavigate(); + } + }, [location.pathname, location.key, onNavigate]); + + useEffect(() => { + const handlePopState = () => { + if (onBack) onBack(); + }; + + window.addEventListener('popstate', handlePopState); + + return () => { + window.removeEventListener('popstate', handlePopState); + }; + }, [onBack]); +}; diff --git a/src/shared/ui/GatheringCard/GatheringCard.module.scss b/src/shared/ui/GatheringCard/GatheringCard.module.scss index cc1ed67..16ce601 100644 --- a/src/shared/ui/GatheringCard/GatheringCard.module.scss +++ b/src/shared/ui/GatheringCard/GatheringCard.module.scss @@ -1,12 +1,10 @@ .card { position: relative; display: flex; - flex-shrink: 0; flex-direction: column; justify-content: center; - width: 21.875rem; - height: 18.75rem; - padding: 20px 25px 0; + aspect-ratio: 1 / 0.8; + padding: 0.5rem; font-family: inherit; color: $secondary-color; cursor: pointer; @@ -25,7 +23,7 @@ font-size: 1.25rem; font-weight: 600; text-overflow: ellipsis; - white-space: nowrap; + white-space: wrap; } &__name { @@ -58,7 +56,13 @@ &__deadlineCon { display: flex; + flex: 1; justify-content: space-between; - width: 100%; + font-size: 0.875rem; } } + +.buttons { + display: flex; + gap: 0.625rem; +} diff --git a/src/shared/ui/GatheringCard/GatheringCard.tsx b/src/shared/ui/GatheringCard/GatheringCard.tsx index 4f8b872..89bd723 100644 --- a/src/shared/ui/GatheringCard/GatheringCard.tsx +++ b/src/shared/ui/GatheringCard/GatheringCard.tsx @@ -1,10 +1,12 @@ +import { faHeart, faPhone } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import cn from 'classnames'; import { Link } from 'react-router-dom'; import styles from './GatheringCard.module.scss'; import { JobTag } from './JobTag'; -interface GatheringCardProps { +export interface GatheringCardProps { title: string; className?: string; // 외부에서 추가 클래스 전달 가능 name?: string; @@ -39,9 +41,9 @@ export const GatheringCard = ({
    {tag?.map((e, i) => )}
마감일 {deadline}
-
- heart icon - contact icon +
+ +
diff --git a/src/shared/ui/SidebarFilter/ui/SidebarFilter.module.scss b/src/shared/ui/SidebarFilter/ui/SidebarFilter.module.scss index 86c6b69..0bea942 100644 --- a/src/shared/ui/SidebarFilter/ui/SidebarFilter.module.scss +++ b/src/shared/ui/SidebarFilter/ui/SidebarFilter.module.scss @@ -18,6 +18,7 @@ .categoryButton { display: flex; + gap: 0.75rem; align-items: center; justify-content: center; width: 100%; @@ -26,7 +27,6 @@ font-weight: 700; color: $primary-color; text-align: center; - gap: 0.75rem; transition: all 0.2s ease; &:hover { @@ -47,9 +47,19 @@ display: flex; flex-direction: column; gap: 1.25rem; - margin-top: 1.25rem; max-height: 20rem; + margin-top: 1.25rem; overflow-y: scroll; + + &::-webkit-scrollbar { + width: 5px; + height: 10px; + } + + &::-webkit-scrollbar-thumb { + background: #ededed; + border-radius: 12px; + } } .subItem { @@ -58,11 +68,11 @@ .subItemButton { width: 100%; - text-align: center; padding: 0.5rem; font-size: 1rem; font-weight: 600; color: $primary-color; + text-align: center; transition: all 0.2s ease; &:hover { diff --git a/src/widgets/GatheringGrid/GatheringGrid.module.scss b/src/widgets/GatheringGrid/GatheringGrid.module.scss index c303ded..9e2000e 100644 --- a/src/widgets/GatheringGrid/GatheringGrid.module.scss +++ b/src/widgets/GatheringGrid/GatheringGrid.module.scss @@ -1,24 +1,14 @@ .container { + display: flex; + justify-content: center; width: 100%; + padding: 0 1.25rem; margin-bottom: 3rem; } .list { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); gap: 1.25rem; - - @media screen and (max-width: 1440px) { - grid-template-columns: repeat(3, 1fr); - } - - @media screen and (max-width: 1200px) { - grid-template-columns: repeat(2, 1fr); - gap: 1rem; - } - - @media screen and (max-width: 830px) { - grid-template-columns: 1fr; - gap: 1.5rem; - } + width: 100%; } diff --git a/src/widgets/Layout/index.tsx b/src/widgets/Layout/index.tsx index 95f1595..983396b 100644 --- a/src/widgets/Layout/index.tsx +++ b/src/widgets/Layout/index.tsx @@ -3,7 +3,19 @@ import { Outlet } from 'react-router-dom'; import { Footer } from './ui/Footer/Footer'; import { Header } from './ui/Header/Header'; +import { useArchiveStore } from '@/features'; +import { usePageLifecycle } from '@/shared/hook'; + export const Layout = () => { + const { resetArchiveData, clearStorage } = useArchiveStore(); + + usePageLifecycle({ + onNavigate: () => { + resetArchiveData(); + clearStorage(); + }, + }); + return ( <>
diff --git a/src/widgets/Layout/ui/Header/Header.module.scss b/src/widgets/Layout/ui/Header/Header.module.scss index 4990811..452438c 100644 --- a/src/widgets/Layout/ui/Header/Header.module.scss +++ b/src/widgets/Layout/ui/Header/Header.module.scss @@ -73,6 +73,7 @@ /* User Menu **/ & .userMenu { + position: relative; display: flex; flex-shrink: 0; flex-basis: 1; @@ -80,3 +81,14 @@ align-items: center; } } + +.button { + cursor: pointer; + transition: 0.3s ease; +} + +.heart { + &:hover { + color: $red; + } +} diff --git a/src/widgets/Layout/ui/Header/Header.tsx b/src/widgets/Layout/ui/Header/Header.tsx index f751440..758161f 100644 --- a/src/widgets/Layout/ui/Header/Header.tsx +++ b/src/widgets/Layout/ui/Header/Header.tsx @@ -1,12 +1,14 @@ +import { faHeart, faSearch } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import cn from 'classnames'; import React from 'react'; -import { Link, useLocation } from 'react-router-dom'; +import { Link, useLocation, useNavigate } from 'react-router-dom'; import styles from './Header.module.scss'; import { NAV_LINKS } from '../../constants'; //assets import Logo from '@/shared/assets/paletteLogo.svg?react'; -import SearchIcon from '@/shared/assets/search.svg?react'; //model import { useModalStore } from '@/shared/model/modalStore'; //component @@ -14,6 +16,7 @@ import { Button } from '@/shared/ui'; export const Header = () => { const { pathname } = useLocation(); + const navigate = useNavigate(); const open = useModalStore(state => state.actions.open); return ( @@ -42,7 +45,14 @@ export const Header = () => { {/** UserMenu */}
- + { + navigate('/search'); + }} + /> + -
); diff --git a/src/widgets/SearchAll/SearchAll.module.scss b/src/widgets/SearchAll/SearchAll.module.scss new file mode 100644 index 0000000..dc25fc6 --- /dev/null +++ b/src/widgets/SearchAll/SearchAll.module.scss @@ -0,0 +1,40 @@ +.wrapper { + display: flex; + flex: 1; + flex-direction: column; + gap: 4rem; +} + +.listWrapper { + display: flex; + flex: 1; + flex-direction: column; + gap: 2rem; + + .tab { + font-size: 1rem; + color: $third-color; + } +} + +.list { + display: grid; + grid-template-rows: auto; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 1rem; + + @media (width <= 1000px) { + grid-template-columns: 1fr 1fr; + } + + @media (width <= 480px) { + grid-template-columns: 1fr; + } +} + +.listHeader { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +} diff --git a/src/widgets/SearchAll/SearchAll.tsx b/src/widgets/SearchAll/SearchAll.tsx new file mode 100644 index 0000000..dd5bc6d --- /dev/null +++ b/src/widgets/SearchAll/SearchAll.tsx @@ -0,0 +1,70 @@ +import { useNavigate } from 'react-router-dom'; + +import styles from './SearchAll.module.scss'; + +import type { ArchiveCardDTO } from '@/features'; +import { ArchiveCard } from '@/features'; +import { Button, GatheringCard } from '@/shared/ui'; +import type { GatheringCardProps } from '@/shared/ui/GatheringCard'; + +export const SearchAll = ({ + archives, + gatherings, + setActiveTab, +}: { + archives: ArchiveCardDTO[]; + gatherings: GatheringCardProps[]; + setActiveTab: (t: string) => void; +}) => { + const navigate = useNavigate(); + return ( +
+
+
+

아카이브

+ +
+
    + {archives.map(archive => ( +
    { + navigate(`/archive/${archive.archiveId}`); + }} + > + +
    + ))} +
+
+
+
+

게더링

+ +
+
    + {gatherings.map(gathering => ( +
    { + navigate(`/gathering/1`); + }} + > + +
    + ))} +
+
+
+ ); +}; diff --git a/src/widgets/SearchTap/SearchTap.module.scss b/src/widgets/SearchTap/SearchTap.module.scss new file mode 100644 index 0000000..4b1e9b4 --- /dev/null +++ b/src/widgets/SearchTap/SearchTap.module.scss @@ -0,0 +1,43 @@ +.wrapper { + display: flex; + flex: 1; + flex-direction: column; + gap: 2rem; + width: 100%; +} + +.tabList { + display: flex; + gap: 1.5rem; + width: 100%; + padding: 1.12rem 1.36rem; + + li { + position: relative; + padding: 0.8rem; + font-size: 1rem; + font-style: normal; + font-weight: 700; + line-height: normal; + color: $primary-color; + text-align: center; + cursor: pointer; + + &::after { + position: absolute; + bottom: -0.25rem; + left: 0; + display: block; + width: 0; + height: 0.125rem; + content: ''; + background-color: $primary-color; + border-radius: 0.5rem; + transition: width 0.3s ease; + } + + &.active::after { + width: 100%; + } + } +} diff --git a/src/widgets/SearchTap/SearchTap.tsx b/src/widgets/SearchTap/SearchTap.tsx new file mode 100644 index 0000000..f4bcbcd --- /dev/null +++ b/src/widgets/SearchTap/SearchTap.tsx @@ -0,0 +1,72 @@ +import { ArchiveGrid } from '../ArchiveGrid'; +import { GatheringGrid } from '../GatheringGrid'; +import styles from './SearchTap.module.scss'; +import { SearchAll } from '../SearchAll/SearchAll'; + +import type { ArchiveCardDTO, Color } from '@/features'; +import type { GatheringCardProps } from '@/shared/ui/GatheringCard'; + +const dummyArchives: ArchiveCardDTO[] = Array.from({ length: 9 }, (_, i) => ({ + archiveId: i, + title: `Sample Archive`, + introduction: `Description for sample archive`, + type: ['red', 'blue', 'green', 'yellow', 'purple'][Math.floor(Math.random() * 4)] as Color, + username: '홍길동', + likeCount: Math.floor(Math.random() * 100), + isLiked: Math.random() > 0.5, + thumbnail: 'https://picsum.photos/300/200', + createDate: new Date(), +})); + +const dummyGatherings: GatheringCardProps[] = Array.from({ length: 9 }, () => ({ + title: `Sample Gathering`, + name: `Sample Name`, + introduction: `Description for sample gathering`, + tag: ['tag1', 'tag2', 'tag3'], + deadline: '2024-11-28', +})); + +const renderingSearchTap = (activeTab: string, setActiveTab: (t: string) => void) => { + if (activeTab === '전체') { + return ( + + ); + } else if (activeTab === '아카이브') { + return ; + } else if (activeTab === '소모임') { + return ; + } +}; + +export const SearchTap = ({ + activeTab, + setActiveTab, +}: { + activeTab: string; + setActiveTab: (t: string) => void; +}) => { + const tabs = ['전체', '아카이브', '소모임']; + + return ( +
+
    + {tabs.map(tab => ( +
  • { + setActiveTab(tab); + }} + > + {tab} +
  • + ))} +
+ {renderingSearchTap(activeTab, setActiveTab)} +
+ ); +}; diff --git a/src/widgets/index.ts b/src/widgets/index.ts index 5a559c8..f301db4 100644 --- a/src/widgets/index.ts +++ b/src/widgets/index.ts @@ -6,3 +6,5 @@ export * from './WriteArchive'; export * from './WriteGathering/WriteGatheringOpts'; export * from './WriteGathering/WriteGatheringDetail'; export * from './ArchiveGrid'; +export * from './SearchTap/SearchTap'; +export * from './SearchAll/SearchAll';