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

[SP2] 드롭다운 컴포넌트 추가, 프로젝트 페이지 필터에 적용 #223

Merged
merged 8 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/components/common/Select/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import useOutsideClickListener from '@src/hooks/useOutsideClickListener';
import { LabelKeyType } from '@src/lib/types/universal';
import { useCallback, useRef, useState } from 'react';
import { S } from './style';

interface SelectProps<T extends LabelKeyType> {
options: T[];
baseValue: T;
baseLabel: string;
selectedValue: T;
setSelectedValue: React.Dispatch<React.SetStateAction<T>>;
labels: Record<T, string>;
}

export default function Select<T extends LabelKeyType>({
options,
selectedValue,
setSelectedValue,
baseValue,
baseLabel,
labels,
}: SelectProps<T>) {
const [isOpen, setIsOpen] = useState(false);

const toggleSelect = () => setIsOpen(!isOpen);

const handleSelect = (value: T) => {
setSelectedValue(value);
};

const closeSelectItem = useCallback(() => setIsOpen(false), []);

const selectItemWrapperRef = useRef<HTMLDivElement>(null);
const selectTriggerRef = useRef<HTMLButtonElement>(null);
Comment on lines +33 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question;

Item과 Trigger 을 각각 div, button 분리해 다른 ref를 사용한 이유가 있나요?
둘 다 같은 ref를 참조해도 되는거 아닌가 싶어서요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

같은 ref를 참조하면 더 밑에 있는 ref가 위에 있는 ref를 덮어쓰게 되지 않나요 ..??!
같은 ref를 참조하는 상황에 대하여 더 자세히 설명 주시면 감사하겠습니닷..!

useOutsideClickListener([selectItemWrapperRef, selectTriggerRef], closeSelectItem);

return (
<div>
<S.SelectTrigger
ref={selectTriggerRef}
onClick={toggleSelect}
isSelectionExist={selectedValue !== baseValue}
isOpened={isOpen}
>
{selectedValue === baseValue ? baseLabel : labels[selectedValue]}
</S.SelectTrigger>
{isOpen && (
<S.SelectItemWrapper ref={selectItemWrapperRef}>
{options.map((option, index) => (
<S.SelectItem
key={index}
isSelected={selectedValue === option}
onClick={() => handleSelect(option)}
>
{labels[option]}
</S.SelectItem>
))}
</S.SelectItemWrapper>
)}
</div>
);
}
70 changes: 70 additions & 0 deletions src/components/common/Select/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import styled from '@emotion/styled';
import arrowDown from '@src/assets/icons/arrow_down.svg';
import { colors } from '@src/lib/styles/colors';

const SelectWrapper = styled.div`
position: relative;
`;

const SelectTrigger = styled.button<{ isSelectionExist: boolean; isOpened: boolean }>`
cursor: pointer;
position: relative;
width: 110px;
font-size: 16px;
font-weight: 500;
padding: 9px 22px;
text-align: left;
color: white;
border-radius: 20px;
background-color: ${({ isSelectionExist }) =>
isSelectionExist ? colors.gray700 : colors.gray600};
border: 1px solid;
border-color: ${({ isSelectionExist }) => (isSelectionExist ? colors.gray200 : colors.gray700)};
&::after {
content: '';
background-repeat: no-repeat;
background-position: center;
position: absolute;
right: 22px;
top: 9px;
transition: 0.2s;
width: 10px;
height: 18px;
background-image: url(${arrowDown});
transform: ${({ isOpened }) => (isOpened ? 'rotate(180deg)' : 'none')};
}
`;

const SelectItem = styled.div<{ isSelected: boolean }>`
background-color: ${({ isSelected }) => (isSelected ? colors.gray400 : 'transparent')};
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
transition: 0.1s;
color: ${colors.gray30};
`;

const SelectItemWrapper = styled.div`
display: flex;
flex-direction: column;
position: absolute;
background-color: ${colors.gray600};
z-index: 200;
width: 110px;
border-radius: 20px;
padding: 9px 12px;
margin-top: 8px;
gap: 8px;
&:hover {
${SelectItem} {
background-color: transparent;
&:hover {
background-color: ${colors.gray400};
}
}
}
`;

export const S = { SelectWrapper, SelectTrigger, SelectItem, SelectItemWrapper };
26 changes: 26 additions & 0 deletions src/hooks/useOutsideClickListener.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useEffect } from 'react';

const useOutsideClickListener = (targetRefs: React.RefObject<Node>[], callback: () => void) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target;
if (target instanceof Node) {
const isOutsideClick = targetRefs.every(
(targetRef: React.RefObject<Node>) =>
targetRef.current === null || !targetRef.current.contains(target),
);
if (isOutsideClick) {
callback();
}
}
};

document.addEventListener('mousedown', handleClickOutside);

return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [targetRefs, callback]);
};

export default useOutsideClickListener;
2 changes: 0 additions & 2 deletions src/lib/constants/gtmClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@ type GtmClass = {
informationCard프로젝트: string;
informationCardFAQ: string;
informationCardYoutube: string;
projectFilter: string;
};

export const GTM_CLASS: GtmClass = {
projectCard: 'project-card',
informationCard프로젝트: 'information_card_project',
informationCardFAQ: 'information_card_faq',
informationCardYoutube: 'information_card_youtube',
projectFilter: 'project_filter',
};
26 changes: 15 additions & 11 deletions src/lib/constants/project.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { ProjectCategoryType } from '@src/lib/types/project';

export const projectCategoryList: {
type: ProjectCategoryType;
name: string;
}[] = [
{ type: ProjectCategoryType.ALL, name: '전체' },
{ type: ProjectCategoryType.APPJAM, name: '🎊 앱잼' },
{ type: ProjectCategoryType.SOPKATHON, name: '💡 솝커톤' },
{ type: ProjectCategoryType.SOPTERM, name: '🛎 솝텀 프로젝트' },
{ type: ProjectCategoryType.STUDY, name: '📖 스터디' },
{ type: ProjectCategoryType.JOINTSEMINAR, name: '👥 합동 세미나' },
{ type: ProjectCategoryType.ETC, name: '💬 기타' },
export const activeProjectCategoryList: ProjectCategoryType[] = [
ProjectCategoryType.ALL,
ProjectCategoryType.APPJAM,
ProjectCategoryType.SOPKATHON,
ProjectCategoryType.SOPTERM,
];

export const projectCategoryLabel: Record<ProjectCategoryType, string> = {
[ProjectCategoryType.ALL]: '전체',
[ProjectCategoryType.APPJAM]: '앱잼',
[ProjectCategoryType.SOPKATHON]: '솝커톤',
[ProjectCategoryType.SOPTERM]: '솝텀',
[ProjectCategoryType.STUDY]: '스터디',
[ProjectCategoryType.JOINTSEMINAR]: '합동 세미나',
[ProjectCategoryType.ETC]: '기타',
};
2 changes: 2 additions & 0 deletions src/lib/types/universal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ type TabTypeOption<T> = {

export type TabType = TabTypeOption<Part>;
export type ExtraTabType = TabTypeOption<ExtraPart>;

export type LabelKeyType = string | number | symbol;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question;

key 값의 타입으로 symbol이 오는 경우는 어떤게 있을까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Record에서 key 자리에 올 수 있는 타입이 string, number, symbol이라서 일단 이렇게 적어두었어요 ..!
저희는 enum값으로 string이나 number만 쓰게 될 것 같긴 합니다!

24 changes: 21 additions & 3 deletions src/views/ProjectPage/ProjectPage.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import { useState } from 'react';
import PageLayout from '@src/components/common/PageLayout';
import Select from '@src/components/common/Select';
import { activeProjectCategoryList, projectCategoryLabel } from '@src/lib/constants/project';
import { ProjectCategoryType } from '@src/lib/types/project';
import { ProjectFilter, ProjectList } from './components';
import { ProjectList } from './components';
import useFetch from './hooks/useFetch';
import { ContentWrapper, Root, SectionTitle } from './styles';

function Projects() {
const [selectedCategory, setCategory] = useState<ProjectCategoryType>(ProjectCategoryType.ALL);
const state = useFetch(selectedCategory);

return (
<PageLayout showScrollTopButton>
<ProjectFilter selectedCategory={selectedCategory} setCategory={setCategory} />
<ProjectList state={state} selectedCategory={selectedCategory} />
<Root>
<ContentWrapper>
<SectionTitle>SOPT에서 진행된 프로젝트 둘러보기</SectionTitle>
<Select
options={activeProjectCategoryList}
labels={projectCategoryLabel}
baseLabel="활동"
selectedValue={selectedCategory}
setSelectedValue={setCategory}
baseValue={ProjectCategoryType.ALL}
/>
<ProjectList
state={state}
selectedCategory={selectedCategory ?? ProjectCategoryType.ALL}
/>
</ContentWrapper>
</Root>
</PageLayout>
);
}
Expand Down
49 changes: 0 additions & 49 deletions src/views/ProjectPage/components/filter/DesktopFilter.tsx

This file was deleted.

27 changes: 0 additions & 27 deletions src/views/ProjectPage/components/filter/MobileFilter.tsx

This file was deleted.

55 changes: 0 additions & 55 deletions src/views/ProjectPage/components/filter/MobileFilterModal.tsx

This file was deleted.

Loading
Loading