diff --git a/src/components/home/FilterCategory.tsx b/src/components/home/FilterCategory.tsx new file mode 100644 index 00000000..0cd6f844 --- /dev/null +++ b/src/components/home/FilterCategory.tsx @@ -0,0 +1,44 @@ +import React, { Dispatch, SetStateAction } from 'react'; +import { CatogoryColor } from '@/constants/color'; + +interface Props { + option: { category: string[]; recruit: string[]; sort: boolean }; + setOption: Dispatch< + SetStateAction<{ category: string[]; recruit: string[]; sort: boolean }> + >; +} + +function FilterCategory({ option, setOption }: Props) { + function filterCategory(item: string) { + const updatedCategory = option.category.includes(item) + ? option.category.filter((club) => club !== item) + : [...option.category, item]; + setOption((prev) => ({ ...prev, category: updatedCategory })); + } + + return ( +
+ setOption((prev) => ({ ...prev, category: [] }))} + > + 전체 + + {CatogoryColor.map((category, index) => ( +
filterCategory(category.title)} + className={`cursor-pointer before:p-2 before:text-gray-300 before:content-['|'] ${ + option.category.includes(category.title) && 'text-blue-500' + }`} + key={`category${index}`} + > + {category.title} +
+ ))} +
+ ); +} + +export default FilterCategory; diff --git a/src/components/modal/FilterOption.tsx b/src/components/modal/FilterOption.tsx index 7ea865fa..14325b2f 100644 --- a/src/components/modal/FilterOption.tsx +++ b/src/components/modal/FilterOption.tsx @@ -1,9 +1,9 @@ import { Dispatch, SetStateAction, useEffect, useState } from 'react'; -import Image from 'next/image'; -import CheckboxImg from '@/assets/checkbox.svg'; -import { CatogoryColor } from '@/constants/color'; import { Club } from '@/types'; -import CheckBox from '../home/CheckBox'; +import Category from './filter/Category'; +import RecruitStatus from './filter/RecruitStatus'; +import Sort from './filter/Sort'; + type Props = { clubs: Club[]; filteredClubs: Club[]; @@ -21,23 +21,18 @@ export default function FilterOption({ option, setOption, }: Props) { - const [recruitClubList, setRecruitClubList] = useState( - filteredClubs ?? clubs, - ); - const [categoryClubList, setCategoryClubList] = useState( - filteredClubs ?? clubs, - ); + const filteredList = filteredClubs ?? clubs; + const [recruitClubList, setRecruitClubList] = useState(filteredList); + const [categoryClubList, setCategoryClubList] = + useState(filteredList); const { category, recruit, sort } = option; const [isFilter, setIsFilter] = useState(false); + const [active, setActive] = useState(''); useEffect(() => { handleList(); handleFilter(); - }, [category, recruit, sort]); - - useEffect(() => { - handleFilter(); - }, [recruitClubList, categoryClubList]); + }); function handleFilter() { const filtered = recruitClubList.filter((item) => @@ -47,33 +42,31 @@ export default function FilterOption({ setFilteredClubs(sortedClubs); } - function filterRecruitPeriod(item: string) { - const updatedRecruit = recruit.includes(item) - ? recruit.filter((club) => club !== item) - : [...recruit, item]; - setOption((prev) => ({ ...prev, recruit: updatedRecruit })); - } - - function filterCategory(item: string) { - const updatedCategory = category.includes(item) - ? category.filter((club) => club !== item) - : [...category, item]; - setOption((prev) => ({ ...prev, category: updatedCategory })); + function filterOption( + item: string, + option: string[], + setOptionCallback: (updatedOption: string[]) => void, + ) { + const updatedOption = option.includes(item) + ? option.filter((value) => value !== item) + : [...option, item]; + setOptionCallback(updatedOption); } function handleList() { - const filterRecuitList = []; - const filterCategoryList = []; - for (const club of clubs) { - if (recruit.includes(club.recruitStatus)) { - filterRecuitList.push(club); + const filterList = ( + filterValues: string[], + property: 'recruitStatus' | 'category', + ) => { + if (filterValues.length === 0) { + return clubs; } - if (category.includes(club.category)) { - filterCategoryList.push(club); - } - } - setRecruitClubList(recruit.length === 0 ? clubs : filterRecuitList); - setCategoryClubList(category.length === 0 ? clubs : filterCategoryList); + return clubs.filter((club) => filterValues.includes(club[property])); + }; + const filteredRecruitList = filterList(recruit, 'recruitStatus'); + const filteredCategoryList = filterList(category, 'category'); + setRecruitClubList(filteredRecruitList); + setCategoryClubList(filteredCategoryList); } const sortClubsByCategory = (clubs: Club[]) => { @@ -84,27 +77,23 @@ export default function FilterOption({ ); }; - const [active, setActive] = useState(''); function handleOption(item: string) { setActive(item); - item === active && isFilter - ? (setIsFilter(false), setActive('')) - : setIsFilter(true); + item === active && isFilter ? focusoutOption() : setIsFilter(true); + } + + function focusoutOption() { + setIsFilter(false); + setActive(''); } return ( -
{ - setIsFilter(false), setActive(''); - }} - > +
{['카테고리', '정렬', '모집기준'].map((item) => (
handleOption(item)} - onBlur={() => setActive('')} className={`mb-1.5 cursor-pointer font-semibold md:mb-2 ${ item === active ? `border-b text-blue-500` : `text-gray-500` } ${item === '카테고리' && 'md:hidden'}`} @@ -117,94 +106,21 @@ export default function FilterOption({ {isFilter && (
{active === '모집기준' && ( -
-
- setOption((prev) => ({ ...prev, recruit: [] })) - } - key={`recruit-option_all`} - > - -
- {['모집 마감', '모집 중', '모집 예정'].map((recruitType) => ( -
filterRecruitPeriod(recruitType)} - key={`recruit-option_${recruitType}`} - > - -
- ))} -
+ )} {active === '정렬' && ( -
-
- setOption((prev) => ({ ...prev, sort: false })) - } - className={`cursor-pointer rounded-xl p-2 px-5 ${ - option.sort ? `opacity-50` : `bg-gray-100 opacity-100` - }`} - > - 동아리명으로 정렬 -
-
setOption((prev) => ({ ...prev, sort: true }))} - className={`cursor-pointer rounded-xl p-2 px-5 ${ - option.sort ? `bg-gray-100 opacity-100` : `opacity-50` - }`} - > - 카테고리로 정렬 -
-
+ )} - {active === '카테고리' && ( -
-
- setOption((prev) => ({ ...prev, category: [] })) - } - key={`category-option_all`} - > - -
- {CatogoryColor.map((categoryItem) => ( -
filterCategory(categoryItem.title)} - key={`category-option_${categoryItem.title}`} - > - -
- ))} -
+ )}
)} diff --git a/src/components/modal/filter/Category.tsx b/src/components/modal/filter/Category.tsx new file mode 100644 index 00000000..6e05c581 --- /dev/null +++ b/src/components/modal/filter/Category.tsx @@ -0,0 +1,61 @@ +import React, { Dispatch, SetStateAction } from 'react'; +import Image from 'next/image'; +import CheckboxImg from '@/assets/checkbox.svg'; +import { CatogoryColor } from '@/constants/color'; +import CheckBox from '../../home/CheckBox'; + +interface Props { + setOption: Dispatch< + SetStateAction<{ category: string[]; recruit: string[]; sort: boolean }> + >; + option: { category: string[]; recruit: string[]; sort: boolean }; + filterOption: ( + item: string, + option: string[], + setOptionCallback: (updatedOption: string[]) => void, + ) => void; +} + +function Category({ setOption, option, filterOption }: Props) { + const { category } = option; + + function fillCheckBox() { + if (category.length === 0) { + return checkbox; + } + return
; + } + + function handleClickOption(title: string) { + filterOption(title, category, (updatedCategory) => { + setOption((prev) => ({ + ...prev, + category: updatedCategory, + })); + }); + } + + return ( +
+
setOption((prev) => ({ ...prev, category: [] }))} + key={`category-option_all`} + > + +
+ {CatogoryColor.map((categoryItem) => ( +
handleClickOption(categoryItem.title)} + key={`category-option_${categoryItem.title}`} + > + +
+ ))} +
+ ); +} + +export default Category; diff --git a/src/components/modal/filter/RecruitStatus.tsx b/src/components/modal/filter/RecruitStatus.tsx new file mode 100644 index 00000000..2f693f01 --- /dev/null +++ b/src/components/modal/filter/RecruitStatus.tsx @@ -0,0 +1,61 @@ +import React, { Dispatch, SetStateAction } from 'react'; +import Image from 'next/image'; +import CheckboxImg from '@/assets/checkbox.svg'; +import CheckBox from '../../home/CheckBox'; + +interface Props { + setOption: Dispatch< + SetStateAction<{ category: string[]; recruit: string[]; sort: boolean }> + >; + option: { category: string[]; recruit: string[]; sort: boolean }; + filterOption: ( + item: string, + option: string[], + setOptionCallback: (updatedOption: string[]) => void, + ) => void; +} + +function RecruitStatus({ setOption, option, filterOption }: Props) { + const { recruit } = option; + const recruitList = ['모집 마감', '모집 중', '모집 예정']; + + function fillCheckBox() { + if (recruit.length === 0) { + return checkbox; + } + return
; + } + + function handleClickOption(recruitType: string) { + filterOption(recruitType, recruit, (updatedRecruit) => { + setOption((prev) => ({ + ...prev, + recruit: updatedRecruit, + })); + }); + } + + return ( + <> +
setOption((prev) => ({ ...prev, recruit: [] }))} + key={`recruit-option_all`} + > + +
+ {recruitList.map((recruitType) => ( +
handleClickOption(recruitType)} + key={`recruit-option_${recruitType}`} + > + +
+ ))} + + ); +} + +export default RecruitStatus; diff --git a/src/components/modal/filter/Sort.tsx b/src/components/modal/filter/Sort.tsx new file mode 100644 index 00000000..5e2dc6e9 --- /dev/null +++ b/src/components/modal/filter/Sort.tsx @@ -0,0 +1,37 @@ +import React, { Dispatch, SetStateAction } from 'react'; + +interface Props { + setOption: Dispatch< + SetStateAction<{ category: string[]; recruit: string[]; sort: boolean }> + >; + option: { category: string[]; recruit: string[]; sort: boolean }; +} + +function Sort({ setOption, option }: Props) { + function handleClickOption(isCategory: boolean) { + setOption((prev) => ({ ...prev, sort: isCategory })); + } + + return ( + <> +
handleClickOption(false)} + className={`cursor-pointer rounded-xl p-2 px-5 ${ + option.sort ? `opacity-50` : `bg-gray-100 opacity-100` + }`} + > + 동아리명으로 정렬 +
+
handleClickOption(true)} + className={`cursor-pointer rounded-xl p-2 px-5 ${ + option.sort ? `bg-gray-100 opacity-100` : `opacity-50` + }`} + > + 카테고리로 정렬 +
+ + ); +} + +export default Sort; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index b00eb9c1..3fa76ff0 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,10 +1,9 @@ import { useEffect, useState } from 'react'; - import Slider from '@/components/common/Slider'; import ClubCard from '@/components/home/ClubCard'; +import FilterCategory from '@/components/home/FilterCategory'; import SearchBar from '@/components/home/SearchBar'; import FilterOption from '@/components/modal/FilterOption'; -import { CatogoryColor } from '@/constants/color'; import { useAllClubs } from '@/hooks/api/club/useAllClubs'; import type { Club } from '@/types/club'; @@ -29,38 +28,34 @@ export default function Home() { (a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name), ); - const semiClubs = sortedClubs.filter( - (club) => club.category === '준동아리', - ); - sortedClubs = sortedClubs.filter((club) => club.category !== '준동아리'); - sortedClubs = [...sortedClubs, ...semiClubs]; + sortedClubs = resortSemiClub(sortedClubs); setClubs(sortedClubs); setFilteredClubs(sortedClubs); }, [data]); useEffect(() => { - const timeout = setTimeout(() => { - setFilteredClubs( - clubs.filter( - (club) => - club.name.includes(keyword.toUpperCase()) || - club.tag.includes(keyword.toUpperCase()) || - club.category === keyword, + const filterClubs = () => { + return clubs.filter((club) => + [club.name, club.tag, club.category].some((property) => + property.includes(keyword.toUpperCase()), ), ); - }, 300); + }; - return () => clearTimeout(timeout); + setFilteredClubs(filterClubs()); }, [clubs, keyword]); - if (isError) { - return
error
; + + function resortSemiClub(sortedClubs: Club[]) { + const semiClubs = sortedClubs.filter( + (club) => club.category === '준동아리', + ); + sortedClubs = sortedClubs.filter((club) => club.category !== '준동아리'); + sortedClubs = [...sortedClubs, ...semiClubs]; + return sortedClubs; } - function filterCategory(item: string) { - const updatedCategory = filterOption.category.includes(item) - ? filterOption.category.filter((club) => club !== item) - : [...filterOption.category, item]; - setFilterOption((prev) => ({ ...prev, category: updatedCategory })); + if (isError) { + return
error
; } return ( @@ -81,29 +76,7 @@ export default function Home() { setOption={setFilterOption} />
- -
- setFilterOption((prev) => ({ ...prev, category: [] }))} - > - 전체 - - {CatogoryColor.map((category, index) => ( -
filterCategory(category.title)} - className={`cursor-pointer before:p-2 before:text-gray-300 before:content-['|'] ${ - filterOption.category.includes(category.title) && 'text-blue-500' - }`} - key={`category${index}`} - > - {category.title} -
- ))} -
- +
    {filteredClubs.map((club) => (