diff --git a/services/one-app/package.json b/services/one-app/package.json index a987c945..84824a68 100644 --- a/services/one-app/package.json +++ b/services/one-app/package.json @@ -15,12 +15,14 @@ }, "dependencies": { "@faker-js/faker": "^9.0.3", + "@radix-ui/react-dropdown-menu": "^2.1.2", "@svgr/webpack": "^8.1.0", "@tanstack/react-query": "^5.59.16", "axios": "^1.7.7", "clsx": "^2.1.1", "js-cookie": "^3.0.5", "next": "14.2.16", + "nuqs": "^2.2.2", "query-string": "^9.1.1", "react": "^18", "react-dom": "^18", diff --git a/services/one-app/src/app/(site)/layout.tsx b/services/one-app/src/app/(site)/layout.tsx index 0741d4ee..f55263a6 100644 --- a/services/one-app/src/app/(site)/layout.tsx +++ b/services/one-app/src/app/(site)/layout.tsx @@ -1,4 +1,6 @@ import type { Metadata } from 'next'; +import { NuqsAdapter } from 'nuqs/adapters/next/app'; + import '../globals.css'; import { RQProvider } from '../_components/RQProvider'; import { MSWComponent } from '../_components/MSWComponent'; @@ -19,7 +21,9 @@ export default function RootLayout({ - {children} + + {children} + ); diff --git a/services/one-app/src/app/(site)/lost-found/[lostId]/page.tsx b/services/one-app/src/app/(site)/lost-found/[lostId]/page.tsx index 2d57ae3e..500c67b5 100644 --- a/services/one-app/src/app/(site)/lost-found/[lostId]/page.tsx +++ b/services/one-app/src/app/(site)/lost-found/[lostId]/page.tsx @@ -1,7 +1,8 @@ import Link from 'next/link'; + +import LostFoundPostDetail from '../_components/postDetail/PostDetail'; import PlusIcon from '@/common/assets/icons/plus'; -import { SuspenseQueryBoundary } from '@/common/components'; -import LostFoundDetail from '../_components/LostFoundDetail'; +import { SuspenseQueryBoundary } from '@/common/components/SuspenseQueryBoundary/SuspenseQueryBoundary'; type Props = { params: { @@ -18,8 +19,11 @@ export default function LostFoundDetailPage({ params }: Props) { - loading}> - + error} + suspenseFallback={
loading
} + > +
diff --git a/services/one-app/src/app/(site)/lost-found/_components/LostFoundDetail.tsx b/services/one-app/src/app/(site)/lost-found/_components/LostFoundDetail.tsx deleted file mode 100644 index 114f2298..00000000 --- a/services/one-app/src/app/(site)/lost-found/_components/LostFoundDetail.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client'; - -import React from 'react'; -import { useGetLostFoundDetail } from '../_lib/get'; - -type Params = { - lostId: string; -}; - -const LostFoundDetail = ({ lostId }: Params) => { - const { data } = useGetLostFoundDetail(lostId); - - return
{data.title}
; -}; - -export default LostFoundDetail; diff --git a/services/one-app/src/app/(site)/lost-found/_components/LostFoundList.tsx b/services/one-app/src/app/(site)/lost-found/_components/LostFoundList.tsx deleted file mode 100644 index e32e0bf7..00000000 --- a/services/one-app/src/app/(site)/lost-found/_components/LostFoundList.tsx +++ /dev/null @@ -1,37 +0,0 @@ -'use client'; - -import React from 'react'; -import Link from 'next/link'; - -import { useGetLostFoundList } from '../_lib/get'; -import { flattenInfinityList } from '@/common/utils/react-query'; -import { useIntersectionObserver } from '@/common/hooks/useIntersectionObserver'; - -const LostFoundList = () => { - const { data, hasNextPage, isFetchingNextPage, fetchNextPage } = - useGetLostFoundList({ - pageSize: 40, - lostType: 'ACQUIRE', - }); - - const lostArticles = flattenInfinityList(data); - const intersectCallback = () => !isFetchingNextPage && fetchNextPage(); - - const { ref } = useIntersectionObserver({ - callback: intersectCallback, - }); - - return ( - <> - {lostArticles.map((item, idx) => ( - - {/* item.id로만 하면 key 중복이 발생하고 있음. 확인 필요 */} - {item.title} - - ))} - {hasNextPage && 더 보기} - - ); -}; - -export default LostFoundList; diff --git a/services/one-app/src/app/(site)/lost-found/_components/postDetail/PostDetail.tsx b/services/one-app/src/app/(site)/lost-found/_components/postDetail/PostDetail.tsx new file mode 100644 index 00000000..04889aab --- /dev/null +++ b/services/one-app/src/app/(site)/lost-found/_components/postDetail/PostDetail.tsx @@ -0,0 +1,16 @@ +'use client'; + +import React from 'react'; +import { useGetLostFoundDetail } from '../../_lib/get'; + +type Props = { + lostId: string; +}; + +const LostFoundPostDetail = ({ lostId }: Props) => { + const { data } = useGetLostFoundDetail(lostId); + + return
{data.title}
; +}; + +export default LostFoundPostDetail; diff --git a/services/one-app/src/app/(site)/lost-found/_components/searchResults/filterList/FilterList.tsx b/services/one-app/src/app/(site)/lost-found/_components/searchResults/filterList/FilterList.tsx new file mode 100644 index 00000000..5feeda16 --- /dev/null +++ b/services/one-app/src/app/(site)/lost-found/_components/searchResults/filterList/FilterList.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { FilterState } from '@/store/filter'; +import { LostFoundFilters, LostFoundType } from '@/model/LostFound'; +import { DropdownFilter } from '@/common/components/Filters/DropdownFilter'; +import { subwayLineIdOptions } from '@/common/constants/subway'; +import ResetFilter from '@/common/components/Filters/ResetFilter'; +import SearchFilter from '@/common/components/Filters/SearchFilter'; + +const lostTypeOptions = { + [LostFoundType.LOST]: '분실물', + [LostFoundType.ACQUIRE]: '습득물', +}; + +interface LostFoundFilterListProps + extends Omit, 'loaded'> {} + +const LostFoundFilterList = ({ + filters, + activedCount, + handleSelect, + handleReset, +}: LostFoundFilterListProps) => { + return ( +
+ +
+ + + +
+
+ ); +}; + +export default LostFoundFilterList; diff --git a/services/one-app/src/app/(site)/lost-found/_components/searchResults/searchedList/SearchedList.tsx b/services/one-app/src/app/(site)/lost-found/_components/searchResults/searchedList/SearchedList.tsx new file mode 100644 index 00000000..d7207be8 --- /dev/null +++ b/services/one-app/src/app/(site)/lost-found/_components/searchResults/searchedList/SearchedList.tsx @@ -0,0 +1,60 @@ +'use client'; + +import React, { useMemo } from 'react'; +import Link from 'next/link'; + +import type { LostFoundFilters } from '@/model/LostFound'; +import { flattenInfinityList } from '@/common/utils/react-query'; +import { gneratetAccurateSubwayLineId } from '@/common/utils/subway'; +import { useIntersectionObserver } from '@/common/hooks/useIntersectionObserver'; +import { useGetLostFoundList } from '@/app/(site)/lost-found/_lib/get'; + +interface Props { + keyword: string | null; + filters: LostFoundFilters; +} + +const LostFoundSearchedList = ({ keyword, filters }: Props) => { + const temporaryUserFavoriteLineId = 1; // TODO: 추후 유저가 즐겨찾는 역 설장하는 피쳐 개발 후 수정하기 + const subwayLineId = useMemo( + () => + gneratetAccurateSubwayLineId( + filters.subwayLineId, + temporaryUserFavoriteLineId, + ), + [filters.subwayLineId], + ); + + const { data, hasNextPage, isFetchingNextPage, fetchNextPage } = + useGetLostFoundList({ + pageSize: 40, + keyword, + subwayLineId, + lostType: filters.lostType, + }); + + const lostArticles = flattenInfinityList(data); + const intersectCallback = () => !isFetchingNextPage && fetchNextPage(); + const { ref: loadMoreRef } = useIntersectionObserver({ + callback: intersectCallback, + }); + + return ( + <> + {lostArticles.map((item, idx) => ( + // item.id로만 하면 key 중복이 발생하고 있음. + // 해결 필요 + + {item.title} + + ))} + {hasNextPage && ( + + 더 보기 + + )} + + ); +}; + +export default LostFoundSearchedList; diff --git a/services/one-app/src/app/(site)/lost-found/_lib/get.ts b/services/one-app/src/app/(site)/lost-found/_lib/get.ts index 104e1f42..d0e14cc6 100644 --- a/services/one-app/src/app/(site)/lost-found/_lib/get.ts +++ b/services/one-app/src/app/(site)/lost-found/_lib/get.ts @@ -10,13 +10,13 @@ import type { LostFoundPostDetail, LostFoundListParams, } from '@/model/LostFound'; -import { TIMESTAMP } from '@/constants/time'; +import { TIMESTAMP } from '@/common/constants/time'; import { generateQueryKey } from '@/common/utils/react-query'; import { objectToQueryString } from '@/common/utils/object'; const getLostFoundList = (params: LostFoundListParams) => apiClient.get>>( - `/lost-posts?${objectToQueryString(params)}`, + `/lost-posts?${objectToQueryString(params, { removeZero: true })}`, ); export const useGetLostFoundList = (params: LostFoundListParams) => diff --git a/services/one-app/src/app/(site)/lost-found/_lib/useLostFoundFilterStore.ts b/services/one-app/src/app/(site)/lost-found/_lib/useLostFoundFilterStore.ts new file mode 100644 index 00000000..3980188f --- /dev/null +++ b/services/one-app/src/app/(site)/lost-found/_lib/useLostFoundFilterStore.ts @@ -0,0 +1,42 @@ +'use client'; + +import { useQueryState } from 'nuqs'; + +import { + APP_UNIQUE_FILTER_ID_LIST, + FilterState, + createFilterStoreWithPersist, +} from '@/store/filter'; +import { SubwayLineFilterOptions } from '@/model/Subway'; +import { LostFoundType, type LostFoundFilters } from '@/model/LostFound'; + +const LOST_FOUND_FILTER_DEFAULT_VALUES: LostFoundFilters = { + lostType: LostFoundType.LOST, + subwayLineId: SubwayLineFilterOptions.ALL_LINES, +} as const; + +export const useLostFoundFilters = () => { + const [keyword] = useQueryState('keyword'); + const { filters, loaded, activedCount, handleSelect, handleReset } = + createFilterStoreWithPersist( + LOST_FOUND_FILTER_DEFAULT_VALUES, + APP_UNIQUE_FILTER_ID_LIST.LOST_FOUND, + )(); + + const boundaryKeys = [...Object.values(filters), keyword]; + + const getFilterProps = (): Omit, 'loaded'> => ({ + filters, + activedCount, + handleSelect, + handleReset, + }); + + return { + loaded, + filters, + keyword, + boundaryKeys, + getFilterProps, + }; +}; diff --git a/services/one-app/src/app/(site)/lost-found/page.tsx b/services/one-app/src/app/(site)/lost-found/page.tsx index b1c6ea24..27f622d2 100644 --- a/services/one-app/src/app/(site)/lost-found/page.tsx +++ b/services/one-app/src/app/(site)/lost-found/page.tsx @@ -1,19 +1,40 @@ -import Link from 'next/link'; -import PlusIcon from '@/common/assets/icons/plus'; -import LostFoundList from './_components/LostFoundList'; -import { SuspenseQueryBoundary } from '@/common/components'; +'use client'; +import { Suspense } from 'react'; -export default function LostFoundPage() { - return ( +import LostFoundFilterList from './_components/searchResults/filterList/FilterList'; +import LostFoundSearchedList from './_components/searchResults/searchedList/SearchedList'; +import { useLostFoundFilters } from './_lib/useLostFoundFilterStore'; +import { SuspenseQueryBoundary } from '@/common/components/SuspenseQueryBoundary/SuspenseQueryBoundary'; + +const LoadingPage = () =>
Loading...
; + +function LostFound() { + const { loaded, keyword, filters, boundaryKeys, getFilterProps } = + useLostFoundFilters(); + + return loaded ? (
- - - - loading
}> - + + {}} + errorFallback={
error
} + suspenseFallback={
loading
} + > +
+ ) : ( + + ); +} + +export default function LostFoundPage() { + return ( + }> + + ); } diff --git a/services/one-app/src/common/assets/icons/chevron-down.tsx b/services/one-app/src/common/assets/icons/chevron-down.tsx new file mode 100644 index 00000000..5ea5977c --- /dev/null +++ b/services/one-app/src/common/assets/icons/chevron-down.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; + +function ChevronDownIcon() { + return ( + + + + + + + + + + + ); +} + +export default ChevronDownIcon; diff --git a/services/one-app/src/common/components/ConditionalRender.tsx b/services/one-app/src/common/components/ConditionalRender.tsx index 8eda01f7..e8bf0b0e 100644 --- a/services/one-app/src/common/components/ConditionalRender.tsx +++ b/services/one-app/src/common/components/ConditionalRender.tsx @@ -11,7 +11,6 @@ interface Props { * 선언적으로 간편한 조건부 렌더링을 제공합니다. * @param props */ -export const ConditionalRender = (props: Props) => { - const { children, isRender } = props; +export const ConditionalRender = ({ isRender, children }: Props) => { return <>{isRender ? children : null}; }; diff --git a/services/one-app/src/common/components/Filters/DropdownFilter.tsx b/services/one-app/src/common/components/Filters/DropdownFilter.tsx new file mode 100644 index 00000000..9cc2d46f --- /dev/null +++ b/services/one-app/src/common/components/Filters/DropdownFilter.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; + +import type { KeyOf } from '@/model/Utils'; +import { objectEntries } from '@/common/utils/object'; +import CheckIcon from '@/common/assets/icons/check'; +import ChevronDownIcon from '@/common/assets/icons/chevron-down'; + +export interface DropdownFilterProps< + T extends Record, + K extends KeyOf, +> { + name: K; + filters: T; + options: Record; + onSelect: (key: K, value: T[K]) => void; +} + +export const DropdownFilter = < + T extends Record, + K extends KeyOf, +>({ + filters, + options, + onSelect, + name, +}: DropdownFilterProps): React.ReactElement => { + const activeValue = filters[name]; + const defaultValue = Object.keys(options)[0] as T[K]; + const isActive = activeValue !== defaultValue; + + return ( + + + + + + onSelect(name, newValue as T[K])} + > + {objectEntries(options).map(([val, label]) => ( + + + + + {label} + + ))} + + + + ); +}; diff --git a/services/one-app/src/common/components/Filters/ResetFilter.tsx b/services/one-app/src/common/components/Filters/ResetFilter.tsx new file mode 100644 index 00000000..d17bdeed --- /dev/null +++ b/services/one-app/src/common/components/Filters/ResetFilter.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import ChevronDownIcon from '@/common/assets/icons/chevron-down'; +import { ConditionalRender } from '../ConditionalRender'; + +interface Props { + activedCount: number; + handleReset: () => void; +} +const ResetFilter = ({ activedCount, handleReset }: Props) => { + return ( + 0}> + + + + + + + + {activedCount}개 필터가 적용됨. + + + 모든 필터 지우기 + + + + + ); +}; + +export default ResetFilter; diff --git a/services/one-app/src/common/components/Filters/SearchFilter.tsx b/services/one-app/src/common/components/Filters/SearchFilter.tsx new file mode 100644 index 00000000..bebf78c3 --- /dev/null +++ b/services/one-app/src/common/components/Filters/SearchFilter.tsx @@ -0,0 +1,37 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { useQueryState } from 'nuqs'; +import { useDebounce } from '@/common/hooks/useDebounce'; + +const SearchFilter = () => { + const [keyword, setKeyword] = useQueryState('keyword', { + scroll: true, + }); + const [inputValue, setInputValue] = useState(keyword); + + const debouncedSetKeyword = useDebounce((value: string) => { + setKeyword(value || null); + }, 300); + + useEffect(() => { + setInputValue(keyword); + }, [keyword]); + + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setInputValue(value); + debouncedSetKeyword(value); + }; + + return ( + + ); +}; + +export default SearchFilter; diff --git a/services/one-app/src/common/components/SuspenseQueryBoundary/ErrorBoundary.tsx b/services/one-app/src/common/components/SuspenseQueryBoundary/ErrorBoundary.tsx index 0c5c8983..4b55914f 100644 --- a/services/one-app/src/common/components/SuspenseQueryBoundary/ErrorBoundary.tsx +++ b/services/one-app/src/common/components/SuspenseQueryBoundary/ErrorBoundary.tsx @@ -3,20 +3,20 @@ import React from 'react'; import { isChangedArray } from '@/common/utils/array'; -type ErrorFallbackType = ( - props: ErrorFallbackProps, -) => React.ReactNode; +type ErrorFallbackProps = { + error: Error; + reset: () => void; +}; -interface ErrorFallbackProps { - error: ErrorType; - reset: VoidFunction; -} +type ErrorFallbackType = + | React.ReactNode + | ((props: ErrorFallbackProps) => React.ReactNode); interface ErrorBoundaryProps { + keys?: unknown[]; children: React.ReactNode; - errorFallback?: ErrorFallbackType; + errorFallback: ErrorFallbackType; resetError?: VoidFunction; - keys?: unknown[]; } interface ErrorBoundaryState { @@ -66,11 +66,11 @@ export class BaseErrorBoundary extends React.Component< const { hasError, error } = this.state; const { children, errorFallback } = this.props; - const isErrExist = hasError && error !== null; - const fallbackUI = (err: ErrorFallbackProps['error']) => - errorFallback?.({ error: err, reset: this.resetBoundary }); - - if (isErrExist) return fallbackUI(error); + if (hasError && error !== null) { + return typeof errorFallback === 'function' + ? errorFallback({ error, reset: this.resetBoundary }) + : errorFallback; + } return children; } } diff --git a/services/one-app/src/constants/colors.ts b/services/one-app/src/common/constants/colors.ts similarity index 100% rename from services/one-app/src/constants/colors.ts rename to services/one-app/src/common/constants/colors.ts diff --git a/services/one-app/src/common/constants/subway.ts b/services/one-app/src/common/constants/subway.ts new file mode 100644 index 00000000..cf8c00e7 --- /dev/null +++ b/services/one-app/src/common/constants/subway.ts @@ -0,0 +1,6 @@ +import { SubwayLineFilterOptions } from '@/model/Subway'; + +export const subwayLineIdOptions = { + [SubwayLineFilterOptions.ALL_LINES]: '전체 호선 보기', + [SubwayLineFilterOptions.ONLY_MY_LINE]: '내 호선만 보기', +} as const; diff --git a/services/one-app/src/constants/time.ts b/services/one-app/src/common/constants/time.ts similarity index 100% rename from services/one-app/src/constants/time.ts rename to services/one-app/src/common/constants/time.ts diff --git a/services/one-app/src/common/utils/object.ts b/services/one-app/src/common/utils/object.ts index b56398a6..b5466464 100644 --- a/services/one-app/src/common/utils/object.ts +++ b/services/one-app/src/common/utils/object.ts @@ -1,4 +1,5 @@ import queryString from 'query-string'; +import type { ObjectKeys } from '@/model/Utils'; export const isValidObject = (obj: unknown): obj is Record => { return typeof obj === 'object' && obj !== null && !Array.isArray(obj); @@ -35,3 +36,11 @@ export const objectToQueryString = >( return queryString.stringify(removeFalsyValues(params, options)); }; + +export function objectEntries>( + obj: Type, +): Array<[ObjectKeys, Type[ObjectKeys]]> { + return Object.entries(obj) as Array< + [ObjectKeys, Type[ObjectKeys]] + >; +} diff --git a/services/one-app/src/common/utils/subway.ts b/services/one-app/src/common/utils/subway.ts new file mode 100644 index 00000000..b3ea4a1d --- /dev/null +++ b/services/one-app/src/common/utils/subway.ts @@ -0,0 +1,12 @@ +export const gneratetAccurateSubwayLineId = ( + filtersSubwayLineId: string, + userFavoriteLineId: number, +) => { + switch (filtersSubwayLineId) { + case 'ALL_LINES': + return 0; + case 'ONLY_MY_LINE': + default: + return userFavoriteLineId; + } +}; diff --git a/services/one-app/src/model/LostFound.ts b/services/one-app/src/model/LostFound.ts index 002bc84a..a6ab2fb7 100644 --- a/services/one-app/src/model/LostFound.ts +++ b/services/one-app/src/model/LostFound.ts @@ -1,9 +1,12 @@ import type { PostImage } from './PostImage'; -import type { WithSubwayLineId } from './Subway'; +import type { SubwayLineFilterOptions, WithSubwayLineId } from './Subway'; import type { Post, RecommendPost } from './Post'; import type { CursorBasedPaginationParams } from './Utils'; -export type LostType = 'LOST' | 'ACQUIRE'; +export enum LostFoundType { + LOST = 'LOST', + ACQUIRE = 'ACQUIRE', +} export type LostStatus = 'PROGRESS' | 'COMPLETE'; export interface LostFoundPost extends Post { @@ -25,6 +28,17 @@ export interface LostFoundPostDetail extends LostFoundPost { export interface LostFoundListParams extends CursorBasedPaginationParams, Partial { - lostType: LostType; - keyword?: string; + keyword: string | null; + lostType: LostFoundType; } + +export type LostFoundFilterKeys = 'lostType' | 'subwayLineId'; + +export type LostFoundFilterValues = { + lostType: LostFoundType; + subwayLineId: SubwayLineFilterOptions; +}; + +export type LostFoundFilters = { + [K in LostFoundFilterKeys]: LostFoundFilterValues[K]; +}; diff --git a/services/one-app/src/model/Subway.ts b/services/one-app/src/model/Subway.ts index 0c901032..12e036bb 100644 --- a/services/one-app/src/model/Subway.ts +++ b/services/one-app/src/model/Subway.ts @@ -1,3 +1,8 @@ export interface WithSubwayLineId { subwayLineId: number; } + +export enum SubwayLineFilterOptions { + ALL_LINES = 'ALL_LINES', + ONLY_MY_LINE = 'ONLY_MY_LINE', +} diff --git a/services/one-app/src/model/Utils.ts b/services/one-app/src/model/Utils.ts index dbb9385c..d68d7e3e 100644 --- a/services/one-app/src/model/Utils.ts +++ b/services/one-app/src/model/Utils.ts @@ -1,3 +1,9 @@ +export type KeyOf = keyof T; +export type ValueOf = T[keyof T]; +export type StringRecord = Record; +export type ObjectKeys> = + `${Exclude}`; + export interface IResponse { code: string; message: string; diff --git a/services/one-app/src/store/filter.ts b/services/one-app/src/store/filter.ts new file mode 100644 index 00000000..4522cff3 --- /dev/null +++ b/services/one-app/src/store/filter.ts @@ -0,0 +1,63 @@ +import { create, StateCreator } from 'zustand'; +import { persist } from 'zustand/middleware'; +import type { KeyOf, ValueOf, StringRecord } from '@/model/Utils'; + +export const filterKey = 'AHAHCHUL_FILTER_STORAGE'; + +export type AppUniqueFilterId = ValueOf; +export enum APP_UNIQUE_FILTER_ID_LIST { + COMMUNITY = 'COMMUNITY', + LOST_FOUND = 'LOST_FOUND', +} + +export type FilterState = { + filters: T; + loaded: boolean; + activedCount: number; + handleSelect: >(key: K, value: T[K]) => void; + handleReset: () => void; +}; + +type FilterStoreCreator = StateCreator>; + +const createFilterStoreWithPersist = ( + defaultValues: T, + uniqueId: AppUniqueFilterId, +) => { + const createStore: FilterStoreCreator = (set) => ({ + filters: defaultValues, + activedCount: 0, + loaded: false, + handleSelect: (key, value) => { + set((state) => { + const isDefaultValue = value === defaultValues[key]; + const wasDefaultValue = state.filters[key] === defaultValues[key]; + + const newFilters = { ...state.filters, [key]: value }; + const newActiveFilterCount = + state.activedCount + + (isDefaultValue && !wasDefaultValue ? -1 : 0) + + (!isDefaultValue && wasDefaultValue ? 1 : 0); + + return { + filters: newFilters, + activedCount: newActiveFilterCount, + }; + }); + }, + handleReset: () => set({ filters: defaultValues, activedCount: 0 }), + }); + + return create>()( + persist(createStore, { + name: `${filterKey}-${uniqueId}`, + onRehydrateStorage: () => (state) => { + if (state) { + state.loaded = true; + } + }, + }), + ); +}; + +export { createFilterStoreWithPersist }; diff --git a/services/one-app/tailwind.config.ts b/services/one-app/tailwind.config.ts index c832c023..042c5835 100644 --- a/services/one-app/tailwind.config.ts +++ b/services/one-app/tailwind.config.ts @@ -1,6 +1,6 @@ import type { Config } from 'tailwindcss'; -const colors = require('./src/constants/colors'); +const colors = require('./src/common/constants/colors'); const config: Config = { content: [ diff --git a/yarn.lock b/yarn.lock index e7b8bb61..96eeef47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -56,6 +56,7 @@ __metadata: dependencies: "@faker-js/faker": "npm:^9.0.3" "@mswjs/http-middleware": "npm:^0.10.2" + "@radix-ui/react-dropdown-menu": "npm:^2.1.2" "@svgr/webpack": "npm:^8.1.0" "@tanstack/react-query": "npm:^5.59.16" "@tanstack/react-query-devtools": "npm:^5.59.16" @@ -82,6 +83,7 @@ __metadata: js-cookie: "npm:^3.0.5" msw: "npm:^2.5.2" next: "npm:14.2.16" + nuqs: "npm:^2.2.2" postcss: "npm:^8" query-string: "npm:^9.1.1" react: "npm:^18" @@ -6548,6 +6550,19 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-context@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-context@npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/fc4ace9d79d7954c715ade765e06c95d7e1b12a63a536bcbe842fb904f03f88fc5bd6e38d44bd23243d37a270b4c44380fedddaeeae2d274f0b898a20665aba2 + languageName: node + linkType: hard + "@radix-ui/react-dialog@npm:^1.1.1": version: 1.1.1 resolution: "@radix-ui/react-dialog@npm:1.1.1" @@ -6655,6 +6670,29 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-dismissable-layer@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-dismissable-layer@npm:1.1.1" + dependencies: + "@radix-ui/primitive": "npm:1.1.0" + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-use-callback-ref": "npm:1.1.0" + "@radix-ui/react-use-escape-keydown": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/637f8d55437bd2269d5aa9fa48e869eade31082cd950b5efcc5f0d9ed016b46feb7fcfcc115ba9972dba68c4686b57873d84aca67ece76ab77463e7de995f6da + languageName: node + linkType: hard + "@radix-ui/react-dropdown-menu@npm:^2.1.1": version: 2.1.1 resolution: "@radix-ui/react-dropdown-menu@npm:2.1.1" @@ -6680,6 +6718,31 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-dropdown-menu@npm:^2.1.2": + version: 2.1.2 + resolution: "@radix-ui/react-dropdown-menu@npm:2.1.2" + dependencies: + "@radix-ui/primitive": "npm:1.1.0" + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-context": "npm:1.1.1" + "@radix-ui/react-id": "npm:1.1.0" + "@radix-ui/react-menu": "npm:2.1.2" + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-use-controllable-state": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/28e84cb116a34c3a73cd9be774170fc920fad6254c1ce285e8e3d86e33c02011229adc5590e385a42106b41bced23e0a482e884e6894e37f68d7e87c76171279 + languageName: node + linkType: hard + "@radix-ui/react-focus-guards@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-focus-guards@npm:1.0.1" @@ -6708,6 +6771,19 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-focus-guards@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-focus-guards@npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/2e99750ca593083a530542a185d656b45b100752353a7a193a67566e3c256414a76fa9171d152f8c0167b8d6c1fdf62b2e07750d7af2974bf8ef39eb204aa537 + languageName: node + linkType: hard + "@radix-ui/react-focus-scope@npm:1.0.3": version: 1.0.3 resolution: "@radix-ui/react-focus-scope@npm:1.0.3" @@ -6827,6 +6903,42 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-menu@npm:2.1.2": + version: 2.1.2 + resolution: "@radix-ui/react-menu@npm:2.1.2" + dependencies: + "@radix-ui/primitive": "npm:1.1.0" + "@radix-ui/react-collection": "npm:1.1.0" + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-context": "npm:1.1.1" + "@radix-ui/react-direction": "npm:1.1.0" + "@radix-ui/react-dismissable-layer": "npm:1.1.1" + "@radix-ui/react-focus-guards": "npm:1.1.1" + "@radix-ui/react-focus-scope": "npm:1.1.0" + "@radix-ui/react-id": "npm:1.1.0" + "@radix-ui/react-popper": "npm:1.2.0" + "@radix-ui/react-portal": "npm:1.1.2" + "@radix-ui/react-presence": "npm:1.1.1" + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-roving-focus": "npm:1.1.0" + "@radix-ui/react-slot": "npm:1.1.0" + "@radix-ui/react-use-callback-ref": "npm:1.1.0" + aria-hidden: "npm:^1.1.1" + react-remove-scroll: "npm:2.6.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/4259f6fbc63048d58bffab443abda9b56ea6b0a28f1e4ae91787a360b9a31e7604de06c8fc70be861c1aaa7abff2858c9314aa3fffbc375c27b0c9aa219a51af + languageName: node + linkType: hard + "@radix-ui/react-popper@npm:1.1.2": version: 1.1.2 resolution: "@radix-ui/react-popper@npm:1.1.2" @@ -6924,6 +7036,26 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-portal@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-portal@npm:1.1.2" + dependencies: + "@radix-ui/react-primitive": "npm:2.0.0" + "@radix-ui/react-use-layout-effect": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/836967330893b16b85371775ed1a59e038ce99189f4851cfa976bde2710d704c2a9e49e0a5206e7ac3fcf8a67ddd2d126b8352a88f295d6ef49d04e269736ed1 + languageName: node + linkType: hard + "@radix-ui/react-presence@npm:1.0.1": version: 1.0.1 resolution: "@radix-ui/react-presence@npm:1.0.1" @@ -6965,6 +7097,26 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-presence@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-presence@npm:1.1.1" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.0" + "@radix-ui/react-use-layout-effect": "npm:1.1.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/777cda0406450ff5ca0e49235e486237723323d046a3382e35a0e78eededccfc95a76a9b5fecd7404dac793264762f4bc10111af1e08f8cc2d4d571d7971220e + languageName: node + linkType: hard + "@radix-ui/react-primitive@npm:1.0.3": version: 1.0.3 resolution: "@radix-ui/react-primitive@npm:1.0.3" @@ -21944,6 +22096,13 @@ __metadata: languageName: node linkType: hard +"mitt@npm:^3.0.1": + version: 3.0.1 + resolution: "mitt@npm:3.0.1" + checksum: 10c0/3ab4fdecf3be8c5255536faa07064d05caa3dd332bd318ff02e04621f7b3069ca1de9106cfe8e7ced675abfc2bec2ce4c4ef321c4a1bb1fb29df8ae090741913 + languageName: node + linkType: hard + "mkdirp-classic@npm:^0.5.2": version: 0.5.3 resolution: "mkdirp-classic@npm:0.5.3" @@ -22705,6 +22864,27 @@ __metadata: languageName: node linkType: hard +"nuqs@npm:^2.2.2": + version: 2.2.2 + resolution: "nuqs@npm:2.2.2" + dependencies: + mitt: "npm:^3.0.1" + peerDependencies: + "@remix-run/react": ">=2" + next: ">=14.2.0" + react: ">=18.2.0 || ^19.0.0-0" + react-router-dom: ">=6" + peerDependenciesMeta: + "@remix-run/react": + optional: true + next: + optional: true + react-router-dom: + optional: true + checksum: 10c0/d7c68eb22cf12b99b7e23204d9d979434f93e684ed47e293bf0851ca4f741d23fe8c5f338cf16a12b0c6712997433c9ab3f64b2055c21b9a6a4e84d30dac4bf8 + languageName: node + linkType: hard + "nwsapi@npm:^2.2.0": version: 2.2.7 resolution: "nwsapi@npm:2.2.7" @@ -25201,7 +25381,7 @@ __metadata: languageName: node linkType: hard -"react-remove-scroll-bar@npm:^2.3.4": +"react-remove-scroll-bar@npm:^2.3.4, react-remove-scroll-bar@npm:^2.3.6": version: 2.3.6 resolution: "react-remove-scroll-bar@npm:2.3.6" dependencies: @@ -25255,6 +25435,25 @@ __metadata: languageName: node linkType: hard +"react-remove-scroll@npm:2.6.0": + version: 2.6.0 + resolution: "react-remove-scroll@npm:2.6.0" + dependencies: + react-remove-scroll-bar: "npm:^2.3.6" + react-style-singleton: "npm:^2.2.1" + tslib: "npm:^2.1.0" + use-callback-ref: "npm:^1.3.0" + use-sidecar: "npm:^1.1.2" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/c5881c537477d986e8d25d2588a9b6f7fe1254e05946fb4f4b55baeead502b0e1875fc3c42bb6f82736772cd96a50266e41d84e3c4cd25e9525bdfe2d838e96d + languageName: node + linkType: hard + "react-router-dom@npm:^6.21.3": version: 6.22.3 resolution: "react-router-dom@npm:6.22.3"