Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue 215 : 필터링 UI를 BottomSheet UI로 수정 & 필터링 옵션 추가 #216

Merged
merged 9 commits into from
Jul 13, 2024
4 changes: 3 additions & 1 deletion src/api/store/fetchStoreList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ const fetchStoreList = async ({
}: FetchParamProps): Promise<FetchStoreListResult> => {
const accessToken = sessionStorage.getItem(ACCESS_TOKEN);
const [, { size, filter, campusId, categoryId, name, type }] = queryKey;

const formattedFilterOption = filter === "basic" ? null : filter;
const params = generateParams({
page: pageParam,
size,
filter,
filter: formattedFilterOption,
campusId,
categoryId,
name,
Expand Down
3 changes: 3 additions & 0 deletions src/asset/check.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/asset/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { ReactComponent as OutwardIcon } from "./outward-icon.svg";
export { ReactComponent as PinIcon } from "./pin-icon.svg";
export { ReactComponent as ClickedPinIcon } from "./clicked-pin-icon.svg";
export { ReactComponent as CampusPinIcon } from "./campus-pin-icon.svg";
export { ReactComponent as Check } from "./check.svg";
16 changes: 14 additions & 2 deletions src/components/common/BottomSheet/BottomSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,25 @@ import React from "react";
import { useEffect, useState } from "react";
import ReactDOM from "react-dom";

import { CSSProp } from "styled-components";

import * as S from "components/common/BottomSheet/BottomSheet.style";

interface BottomSheetProps {
title: string;
children: React.ReactNode;
cssProps?: {
heading: CSSProp;
};
closeSheet: () => void;
}

function BottomSheet({ title, closeSheet, children }: BottomSheetProps) {
function BottomSheet({
title,
closeSheet,
cssProps,
children,
}: BottomSheetProps) {
const [scrollOffset, setScrollOffset] = useState(0);

useEffect(() => {
Expand All @@ -28,7 +38,9 @@ function BottomSheet({ title, closeSheet, children }: BottomSheetProps) {
<S.Container scrollOffset={scrollOffset}>
<S.Backdrop onClick={closeSheet} />
<S.ContentWrapper>
<Heading size="xs">{title}</Heading>
<Heading size="xs" css={cssProps?.heading}>
{title}
</Heading>
<S.Content>{children}</S.Content>
</S.ContentWrapper>
</S.Container>,
Expand Down
9 changes: 4 additions & 5 deletions src/components/common/Chip/Chip.style.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import styled from "styled-components";
import styled, { css } from "styled-components";

import type { ChipProps } from "components/common/Chip/Chip";

export const ChipContainer = styled.button<Partial<ChipProps>>`
padding: 8px 12px;
font-size: 14px;

${({ theme, isSelected }) => `
background-color: ${isSelected ? theme.color.primary : theme.color.white};
border: 1px solid ${isSelected ? theme.color.primary : theme.color.gray200};
color: ${isSelected ? theme.color.white : theme.color.gray800};
${({ theme }) => css`
background-color: ${theme.color.primaryLight1};
border: 1px solid ${theme.color.primary};
`}

border-radius: ${({ theme }) => theme.borderRadius.small};
Expand Down
4 changes: 3 additions & 1 deletion src/components/common/Chip/Chip.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from "react";

import * as S from "components/common/Chip/Chip.style";

export interface ChipProps {
children: string;
children: React.ReactNode;
isSelected?: boolean;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,25 @@ export const ChipContainer = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacer.spacing3};
`;

export const ChipContent = styled.div`
display: flex;
align-items: center;
gap: 8px;
`;

export const FilterOptionContainer = styled.section`
max-height: 600px;
overflow-y: auto;
display: flex;
flex-direction: column;
row-gap: 24px;
`;

export const FilterOption = styled.div`
width: 100%;
display: flex;
justify-content: space-between;

cursor: pointer;
`;
84 changes: 67 additions & 17 deletions src/components/pages/CategoryDetailPage/CategoryDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,57 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from "react";
import { useContext } from "react";
import { MdArrowBackIos } from "react-icons/md";
import { TbArrowsUpDown } from "react-icons/tb";
import { useInfiniteQuery } from "react-query";
import { Navigate, useNavigate, useParams } from "react-router-dom";
import { Campus, CategoryId, StoreItemWithHeart } from "types/common";

import { NETWORK, SIZE, FILTERS } from "constants/api";
import {
NETWORK,
SIZE,
FilterOption,
STORE_FILTER_OPTIONS,
entries,
} from "constants/api";
import { getCampusId } from "constants/campus";
import { categories } from "constants/categories";
import { MESSAGES } from "constants/messages";
import { QUERY_KEY } from "constants/queryKey";
import { PATHNAME } from "constants/routes";

import { Check } from "asset";

import { campusContext } from "context/CampusContextProvider";

import getNextPageParam from "api/getNextPageParam";
import fetchStoreList from "api/store/fetchStoreList";

import BottomSheet from "components/common/BottomSheet/BottomSheet";
import Button from "components/common/Button/Button";
import Chip from "components/common/Chip/Chip";
import ErrorImage from "components/common/ErrorImage/ErrorImage";
import ErrorText from "components/common/ErrorText/ErrorText";
import InfiniteScroll from "components/common/InfiniteScroll/InfiniteScroll";
import SectionHeader from "components/common/SectionHeader/SectionHeader";
import Spinner from "components/common/Spinner/Spinner";
import StoreList from "components/common/StoreList/StoreList";
import StoreListItemWithHeart from "components/common/StoreListItem/StoreListItemWithHeart";
import Text from "components/common/Text/Text";

import * as S from "components/pages/CategoryDetailPage/CategoryDetailPage.style";

function CategoryDetailPage() {
const navigate = useNavigate();

const [isFilteringBottomSheetOpen, setIsFilteringBottomSheetOpen] =
useState(false);
const openSheet = () => setIsFilteringBottomSheetOpen(true);
const closeSheet = () => setIsFilteringBottomSheetOpen(false);

const campusName = useContext(campusContext);
const campusId = getCampusId(campusName as Campus);
const { categoryId } = useParams();

const [filter, setFilter] = useState<string | null>(null);
const [filter, setFilter] = useState<FilterOption>("basic");

const fetchParams = { size: SIZE.LIST_ITEM, filter, campusId, categoryId };

Expand All @@ -48,18 +66,20 @@ function CategoryDetailPage() {
} = useInfiniteQuery(QUERY_KEY.categoryStore(fetchParams), fetchStoreList, {
getNextPageParam,
retry: NETWORK.RETRY_COUNT,
refetchOnWindowFocus: false,
});

const loadMoreStores = () => {
fetchNextPage();
};

const handleClickFilterChip = (index: number) => () => {
setFilter((prev) =>
prev === FILTERS[index].order ? "" : FILTERS[index].order
);
const handleClickFilterOption = (option: FilterOption) => {
if (filter === option) return;
setFilter(option);
};

const currentOption = STORE_FILTER_OPTIONS[filter];

const categoryStores =
data?.pages.reduce<StoreItemWithHeart[]>(
(stores, page) => [...stores, ...page.restaurants],
Expand All @@ -85,15 +105,12 @@ function CategoryDetailPage() {
<S.CategoryDetailPageContainer>
<SectionHeader>{categoryName || "%ERROR%"}</SectionHeader>
<S.ChipContainer>
{FILTERS.map((chip, index) => (
<Chip
key={chip.order}
isSelected={filter === chip.order}
onClick={handleClickFilterChip(index)}
>
{chip.text}
</Chip>
))}
<Chip onClick={openSheet}>
<S.ChipContent>
<TbArrowsUpDown />
{currentOption}
</S.ChipContent>
</Chip>
</S.ChipContainer>
<InfiniteScroll handleContentLoad={loadMoreStores} hasMore={true}>
{(isLoading || isFetching) && <Spinner />}
Expand All @@ -109,6 +126,39 @@ function CategoryDetailPage() {
<ErrorText>가게 정보가 없습니다.</ErrorText>
)}
</InfiniteScroll>
{isFilteringBottomSheetOpen && (
<BottomSheet
title="정렬"
closeSheet={closeSheet}
cssProps={{
heading: {
display: "flex",
justifyContent: "center",
fontSize: "1.8rem",
lineHeight: "2.4rem",
fontWeight: "400",
},
}}
>
<S.FilterOptionContainer>
{entries(STORE_FILTER_OPTIONS).map(([key, value]) => {
return (
<S.FilterOption
onClick={() => {
handleClickFilterOption(key);
closeSheet();
}}
key={key}
>
<Text>{value}</Text>
{filter === key && <Check />}
</S.FilterOption>
);
})}
<Button variant="primary">닫기</Button>
</S.FilterOptionContainer>
</BottomSheet>
)}
</S.CategoryDetailPageContainer>
);
}
Expand Down
19 changes: 19 additions & 0 deletions src/constants/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,31 @@ export const SIZE = {
export const FILTERS = [
{ order: "rating", text: "별점 순" },
{ order: "spell", text: "가나다 순" },
{ order: "distance", text: "거리 순" },
{ order: "bookmark", text: "좋아요 순" },
] as const;

export const STORE_FILTER_OPTIONS = {
basic: "기본 순",
rating: "별점 순",
spell: "가나다 순",
distance: "거리 순",
bookmark: "좋아요 순",
} as const;

export type FilterOption = keyof typeof STORE_FILTER_OPTIONS;

export const ACCESS_TOKEN = "matzipaccessToken";

export const AUTH_LINK = `https://github.com/login/oauth/authorize?client_id=${
process.env.NODE_ENV === "production"
? "a51717e6e0bb9e34da8e"
: "e060e7a6b636763ab22d"
}`;

type Entries<T extends object> = {
[K in keyof T]: [K, T[K]];
}[keyof T][];

export const entries = <T extends object>(obj: T): Entries<T> =>
Object.entries(obj) as any;
2 changes: 1 addition & 1 deletion src/style/Theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const color = {

primaryDark: "#f19203",
primary: "#ffa927",
primaryLight1: "#ffc774",
primaryLight1: "#fff8ed",
primaryLight2: "#ffe5be",
primaryLight3: "#fff4e4",
primaryLight4: "#fff8ed",
Expand Down
Loading