Skip to content

Commit

Permalink
Feat: 탭UI 필터모달 라디오버튼 -> 체크박스 버튼으로 수정 #115
Browse files Browse the repository at this point in the history
  • Loading branch information
jeonyeonkyu committed Jun 17, 2021
1 parent d178ba1 commit 05e94fd
Show file tree
Hide file tree
Showing 12 changed files with 204 additions and 73 deletions.
15 changes: 11 additions & 4 deletions fe/client/src/Pages/IssueListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import React, { useCallback } from 'react'
import React, { useCallback, useEffect } from 'react'
import HeadContent from '@components/issueList/HeadContent';
import { ListWrapper } from '@components/common/baseStyle/baseStyle';
import ListHeader from '@components/issueList/ListHeader';
import ListItem from '@components/issueList/ListItem';
import { issueListItemAtom } from '@components/common/atoms/issueListAtom';
import { filterAtom, FilterBooleanType } from '@/components/common/atoms/filterAtom';
import { IssueListItemType } from '@components/common/types/APIType';
import API from '@/utils/API';
import useFetch, { AsyncState } from '@/utils/hook/useFetch';
import { IssueListItemType } from '@components/common/types/APIType';
import { filterAtom, FilterBooleanType } from '@/components/common/atoms/filterAtom';
import { useRecoilState } from '@/utils/myRecoil/useRecoilState';

const IssueListPage = () => {
const [issueItems] = useFetch(API.get.issues);
const { data, loading, error }: AsyncState<any, any> = issueItems;
const [, setFilterModalState] = useRecoilState(filterAtom);
const [, setFilteredList] = useRecoilState(issueListItemAtom)

useEffect(() => {
if (!data) return;
setFilteredList(data);
}, [data]);

const handleClickShowFilterModal = useCallback((title: string) => () => {
setFilterModalState((filterModalState: FilterBooleanType) => ({ ...filterModalState, [title]: true }));
Expand All @@ -23,7 +30,7 @@ const IssueListPage = () => {
<HeadContent {...{ handleClickShowFilterModal }} />
<ListWrapper wrapWidth="100%">
{
data && <>
data && <>
<ListHeader issueItems={data} {...{ handleClickShowFilterModal }} />
{data.map((issueItem: IssueListItemType) => {
return <ListItem key={issueItem.id} {...{ issueItem }} />
Expand Down
104 changes: 64 additions & 40 deletions fe/client/src/components/common/FilterTab.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import React, { useCallback, useEffect } from 'react'
import styled from 'styled-components';
import { useRecoilState } from '@/utils/myRecoil/useRecoilState';
import { filterAtom, filterDefaultCheckerAtom, FilterStringType } from '@components/common/atoms/filterAtom';
import {
filterAtom, filterCheckboxListAtom,
FilterCheckboxListType,
filterRadioButtonListAtom, FilterRadioButtonListType
} from '@components/common/atoms/filterAtom';

type FilterTabType = {
header: string;
filterList: Array<string>;
inputType: string;
}

const filterHeaderNames: { [key: string]: string } = {
Expand All @@ -16,9 +21,10 @@ const filterHeaderNames: { [key: string]: string } = {
writer: '작성자'
}

const FilterTab = ({ header, filterList }: FilterTabType) => {
const FilterTab = ({ header, filterList, inputType }: FilterTabType) => {
const [filterModalState, setFilterModalState] = useRecoilState(filterAtom);
const [defaultCheckerState, setDefaultCheckerState] = useRecoilState(filterDefaultCheckerAtom);
const switchAtom = inputType === 'radio' ? filterRadioButtonListAtom : filterCheckboxListAtom;
const [defaultCheckerState, setDefaultCheckerState] = useRecoilState<any>(switchAtom);

const handleClickHideFilterModal = useCallback((event: MouseEvent) => {
const targetList = (event.target as HTMLElement);
Expand All @@ -33,15 +39,29 @@ const FilterTab = ({ header, filterList }: FilterTabType) => {
})
}, []);

const handleChangeDefaultChecker = useCallback((listItem: string) => () => {
setDefaultCheckerState((state: FilterStringType) => ({ ...state, [header]: listItem }));
const handleChangeDefaultChecker = useCallback((listItem) => () => {
const { name, ...info } = listItem;
if (inputType === 'radio') {
setDefaultCheckerState((state: FilterRadioButtonListType) => ({ ...state, [header]: { name, info } }));
} else {
setDefaultCheckerState((state: FilterCheckboxListType) => {
const presentIndex = state[header].findIndex((item: any) => item.name === name);
if (presentIndex !== -1) {
return {
...state,
[header]: [...state[header].slice(0, presentIndex), ...state[header].slice(presentIndex + 1)]
}
}
return { ...state, [header]: [{ name, info }, ...state[header]] }
});
}
}, []);

useEffect(() => {
if (Object.values(filterModalState).every(v => !v)) return;
document.addEventListener('click', handleClickHideFilterModal);
return () => {
document.removeEventListener('click', handleClickHideFilterModal)
document.removeEventListener('click', handleClickHideFilterModal);
};
}, [filterModalState]);

Expand All @@ -60,13 +80,17 @@ const FilterTab = ({ header, filterList }: FilterTabType) => {
isShow={filterModalState[header]}
{...{ header }} >
<FilterTabHeader>{filterHeaderNames[header]} 필터</FilterTabHeader>
{filterList.map((listItem, idx) => {
{filterList.map((listItem: any, idx) => {

return (<div key={`${header}-${idx}`}>
<FilterLabel>
<input type="radio" name={header}
<input type={inputType} name={header}
onChange={handleChangeDefaultChecker(listItem)}
checked={defaultCheckerState[header] === listItem} />
<span>{listItem}</span>
checked={inputType === 'radio' ?
defaultCheckerState[header].name === listItem.name :
defaultCheckerState[header]
.findIndex((item: any) => item.name === listItem.name) !== -1} />
<span>{listItem.name}</span>
</FilterLabel>
</div>)
})}
Expand All @@ -75,47 +99,47 @@ const FilterTab = ({ header, filterList }: FilterTabType) => {
}

const FilterTabWrapper = styled.div<{ isShow: boolean, header: string }>`
position: absolute;
width: 200px;
top:${({ header }) => header === 'issue' ? '35px' : '30px'};
left: ${({ header }) => header === 'issue' && 0};
right: ${({ header }) => header !== 'issue' && '-8px'};
z-index: 1;
border:1px solid #d9dbe9;
background: #FEFEFE;
border-radius: 11px;
box-shadow:0px 1px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%);
visibility:${({ isShow }) => isShow ? 'visible' : 'hidden'};
> div{
padding: .5rem;
}
div + div{
border-top:1px solid #d9dbe9;
}
&:hover{
cursor:default;
}
position: absolute;
width: 200px;
top:${({ header }) => header === 'issue' ? '35px' : '30px'};
left: ${({ header }) => header === 'issue' && 0};
right: ${({ header }) => header !== 'issue' && '-8px'};
z-index: 1;
border:1px solid #d9dbe9;
background: #FEFEFE;
border-radius: 11px;
box-shadow:0px 1px 1px -1px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%);
visibility:${({ isShow }) => isShow ? 'visible' : 'hidden'};
> div{
padding: .5rem;
}
div + div{
border-top:1px solid #d9dbe9;
}
&:hover{
cursor:default;
}
`

const FilterTabHeader = styled.div`
background:#F7F7FC;
border-radius: 11px 11px 0 0;
background:#F7F7FC;
border-radius: 11px 11px 0 0;
`;


const FilterLabel = styled.label`
display:flex;
justify-content: space-between;
flex-direction: row-reverse;
display:flex;
justify-content: space-between;
flex-direction: row-reverse;
& > span{
color: #A3A1B0;
font-size: 12px;
color: #A3A1B0;
font-size: 12px;
}
& > :checked + span{
color: #000;
color: #000;
}
&:hover{
cursor:pointer;
&:hover{
cursor:pointer;
}
`;

Expand Down
2 changes: 1 addition & 1 deletion fe/client/src/components/common/Label.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import styled from 'styled-components';
import { getHexToRGB, getTextColor } from '@/utils/serviceUtils';
import { pipe } from '@/utils/commonUtils';
import { pipe } from '@/utils/functionalUtils';

type LabelType = {
name: string;
Expand Down
31 changes: 19 additions & 12 deletions fe/client/src/components/common/atoms/filterAtom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,29 @@ export const filterAtom = atom<FilterBooleanType>({
}
})

export type FilterStringType = {
issue: string;
manager: string;
label: string;
milestone: string;
writer: string;
export type FilterRadioButtonListType = {
[key: string]: { name: string, info: any }
}

export const filterDefaultCheckerAtom = atom<FilterStringType>({
export const filterRadioButtonListAtom = atom<FilterRadioButtonListType>({
key: 'filterDefaultCheckerAtom',
default: {
issue: '',
manager: '',
label: '',
milestone: '',
writer:''
issue: { name: '', info: {} },
manager: { name: '', info: {} },
label: { name: '', info: {} },
milestone: { name: '', info: {} },
writer: { name: '', info: {} }
}
})

export type FilterCheckboxListType = {
manager: string[];
label: string[];
milestone: string[];
[key: string]: string[];
}

export const filterCheckboxListAtom = atom<FilterCheckboxListType>({
key: 'filterCheckboxListAtom',
default: { manager: [], label: [], milestone: [] }
});
8 changes: 8 additions & 0 deletions fe/client/src/components/common/atoms/issueListAtom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import atom from '@/utils/myRecoil/atom';
import { IssueListItemType } from '@components/common/types/APIType';

export const issueListItemAtom = atom<IssueListItemType[]>({
key: 'issueListItemAtom',
default: []
});

69 changes: 69 additions & 0 deletions fe/client/src/components/createIssue/FilterItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, { useState, useEffect } from 'react'
import styled from 'styled-components';
import FilterTab from '@components/common/FilterTab';
import ProgressBar from '@components/common/ProgressBar';
import Label from '@components/common/Label';
import { filterCheckboxListAtom } from '@components/common/atoms/filterAtom';
import { useRecoilState } from '@/utils/myRecoil/useRecoilState';
import useFetch, { AsyncState } from '@/utils/hook/useFetch';
import API from '@/utils/API';

const filterNames: { [key: string]: { apiName: string; name: string } } = {
manager: { apiName: 'users', name: '담당자' },
label: { apiName: 'labels', name: '레이블' },
milestone: { apiName: 'milestones', name: '마일스톤' },
}

const FilterItem = ({ header }: { header: string }) => {
const { apiName } = filterNames[header];
const [issueList] = useFetch(API.get[apiName]);
const { data }: AsyncState<any, any> = issueList;
const [checkedItems] = useRecoilState(filterCheckboxListAtom);

return (
<>
<CheckedItemWrapper>
<ImageTag src="https://user-images.githubusercontent.com/61257242/121417591-0d02b480-c9a5-11eb-9c7e-d926e8731bfb.png" alt="" />
<span>Oni</span>
</CheckedItemWrapper>

<CheckedItemWrapper>
<Label name='라벨네임' color='#0049' />
</CheckedItemWrapper>

<ProgressBarWrapper>
<ProgressBar variant="determinate" value={50} />
<span>마스터즈 코스</span>
</ProgressBarWrapper>

{data && <FilterTab
{...{ header }}
inputType='checkbox'
filterList={data} />}
</>
)
}

const CheckedItemWrapper = styled.div`
display: flex;
margin: 0 32px 16px 32px;
color: #6E7191;
place-items: center;
`;

const ImageTag = styled.img`
width: 44px;
height: 44px;
margin-right: 8px;
border-radius: 50%;
`;

const ProgressBarWrapper = styled.div`
margin: 0 32px 16px 32px;
> span {
display:inline-block;
margin-top: 8px;
}
`;

export default React.memo(FilterItem);
8 changes: 2 additions & 6 deletions fe/client/src/components/createIssue/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import React, { useCallback } from 'react'
import styled from 'styled-components';
import FilterTab from '@components/common/FilterTab';
import { filterAtom, FilterBooleanType } from '@components/common/atoms/filterAtom';
import { useRecoilState } from '@/utils/myRecoil/useRecoilState';
import PlusIcon from '@/Icons/Plus.svg';
import FilterItem from './FilterItem';

const tabItems = [
{ name: '담당자', title: 'manager' },
{ name: '레이블', title: 'label' },
{ name: '마일스톤', title: 'milestone' }
];

const mockup = ['비모', '비모2', '비모3'];
const Tabs = () => {
const [, setFilterModalState] = useRecoilState(filterAtom);

const handleClickShowFilterModal = useCallback((title: string) => () => {
setFilterModalState((filterModalState: FilterBooleanType) => ({ ...filterModalState, [title]: true }));
}, []);
Expand All @@ -28,9 +26,7 @@ const Tabs = () => {
<span>{name}</span>
<img src={PlusIcon} alt="" />
</TabContents>
<FilterTab
header={title}
filterList={mockup} />
<FilterItem header={title} />
</TabItems>
)
})}
Expand Down
7 changes: 4 additions & 3 deletions fe/client/src/components/issueList/FilterItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ type FilterItemType = {

const FilterItem = ({ title, handleClickShowFilterModal }: FilterItemType) => {
const { apiName } = filterNames[title];
const [users] = useFetch(API.get[apiName]);
const { data }: AsyncState<any, any> = users;
const [filterList] = useFetch(API.get[apiName]);
const { data }: AsyncState<any, any> = filterList;

return (
<FilterWrapper>
Expand All @@ -30,8 +30,9 @@ const FilterItem = ({ title, handleClickShowFilterModal }: FilterItemType) => {
</div>
{data &&
<FilterTab
inputType='radio'
header={title}
filterList={data.map(({ name }: { name: string }) => name)} />
filterList={data} />
}
</FilterWrapper>
)
Expand Down
1 change: 1 addition & 0 deletions fe/client/src/components/issueList/HeadContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const HeadContent = ({ handleClickShowFilterModal }: HeadContentType) => {
style={{ transform: 'translateY(3px)' }} />
</div>
<FilterTab
inputType='radio'
header="issue"
filterList={issueFilterList} />
</FilterButton>
Expand Down
Loading

0 comments on commit 05e94fd

Please sign in to comment.