Skip to content

Commit

Permalink
#104 feat: 필터바 드롭다운 기능 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
saejinpark authored and silvertae committed Aug 17, 2023
1 parent f4d7e26 commit b0eba37
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 128 deletions.
2 changes: 1 addition & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import Components from './pages/Components';
import Login from './pages/Login';
import Register from './pages/Register';
import Callback from './pages/GitHubCallback';
import Main from './pages/Main';
import Issues from './pages/Issues';
import AddIssue from './pages/AddIssue';
import IssueDetail from './pages/IssueDetail';
Expand Down Expand Up @@ -51,6 +50,7 @@ function App() {
<Route path="/issues" element={<Issues />} />
<Route path="/addIssue" element={<AddIssue />} />
<Route path="/issueDetail" element={<IssueDetail />} />
<Route path="/Issues" element={<Issues />} />

{/* protected routes */}
<Route element={<RequireAuth />}>
Expand Down
60 changes: 47 additions & 13 deletions frontend/src/components/common/DropdownIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,52 @@
import { styled } from 'styled-components';
import Icons from '../../design/Icons';
import DropdownPanel from './DropdownPanel';
import { useState } from 'react';
import DropdownPanelElement from '../../types/DropdownPanelElement';
import ButtonComponent from './button/BaseButton';

export default function DropdownIndicator({
text,
label,
elements,
...props
}: {
text: string;
label: string;
elements: DropdownPanelElement[];
}) {
const [isOpenPanel, setIsOpenPanel] = useState(false);

function submitHandler(e: React.FormEvent) {
e.preventDefault();
console.log(e);
}

export default function DropdownIndicator({ text, ...rest }: { text: string }) {
const Icon = Icons['chevronDown'];
return (
<Container {...rest}>
<Text>{text}</Text>
<Icon />
<Container onSubmit={submitHandler} {...props}>
<Button
type="button"
ghost
flexible
onClick={() => setIsOpenPanel((bool) => !bool)}>
<Text>{text}</Text>
<Icons.chevronDown />
</Button>
<Panel>
{isOpenPanel && (
<DropdownPanel {...{ label, baseElements: elements }} />
)}
</Panel>
</Container>
);
}

const Container = styled.button`
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border: none;
background: transparent;
gap: 4px;
const Container = styled.form`
position: relative;
z-index: 1;
`;

const Button = styled(ButtonComponent)`
&:hover {
opacity: ${({ theme }) => theme.objectStyles.opacity.hover};
}
Expand All @@ -34,3 +62,9 @@ const Text = styled.span`
${({ theme }) => theme.font.available.medium[16]};
color: ${({ theme }) => theme.color.neutral.text.default};
`;

const Panel = styled.div`
position: absolute;
left: -1px;
top: calc(100% + 8px);
`;
111 changes: 78 additions & 33 deletions frontend/src/components/common/DropdownPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,81 @@
import { styled } from 'styled-components';
import Icons from '../../design/Icons';
import { v4 as uuidV4 } from 'uuid';

enum Option {
Available,
Selected,
}

// const dummy = [
// ['label', Option.Selected],
// ['label', Option.Available],
// ] as [string, Option][];
import Option from '../../constant/Option';
import DropdownPanelElement from '../../types/DropdownPanelElement';
import { User } from '../../types';
import { useState } from 'react';

export default function DropdownPanel({
elements,
user,
label,
...rest
baseElements,
}: {
elements: [text: string, option: Option][];
user?: User;
label: string;
baseElements: DropdownPanelElement[];
}) {
const [elements, setElements] =
useState<DropdownPanelElement[]>(baseElements);

const itemCheckHandler = (e: React.ChangeEvent, index: number) => {
const input = e.target as HTMLInputElement;
const form = input.closest('form');
const formObject = formDataToObject(new FormData(form!));
setElements((elements) =>
elements.map((element, _index) => {
if (_index === index) {
element.option = !input.checked ? Option.Available : Option.Selected;
}
return element;
})
);
console.log(formObject);
};
return (
<Container {...rest}>
<Container>
<Header>{label}</Header>
<DropdownElements>
{elements.map(([text, option]) => {
const key = uuidV4();
const Icon = Icons.userImageSmall;
return (
<li key={key}>
<Element htmlFor={key} $option={option}>
<Icon />
<Text>{text}</Text>
<input
type="checkbox"
id={key}
checked={option ? true : false}
/>
{option ? <Icons.checkOnCircle /> : <Icons.checkOffCircle />}
</Element>
</li>
);
})}
{elements.map(({ text, option }, index) => (
<li key={text}>
<Element htmlFor={text} $option={option}>
{user && <img src={user.profileImg} alt={user.name + '아바타'} />}
<Text>{text}</Text>
<input
type="checkbox"
id={text}
name={textToName(text)}
onChange={(e) => itemCheckHandler(e, index)}
/>
{option === Option.Selected ? (
<Icons.checkOnCircle />
) : (
<Icons.checkOffCircle />
)}
</Element>
</li>
))}
</DropdownElements>
</Container>
);
}

function textToName(text: string) {
switch (text) {
case '열린 이슈':
return 'status[open]';
case '내가 작성한 이슈':
return 'status[author]';
case '나에게 할당된 이슈':
return 'status[assignee]';
case '내가 댓글을 남긴 이슈':
return 'status[commented]';
case '닫힌 이슈':
return 'status[closed]';
default:
return '';
}
}

const Container = styled.article`
width: 240px;
border-radius: ${({ theme }) => theme.objectStyles.radius.large};
Expand All @@ -64,6 +93,7 @@ const Header = styled.h3`

const DropdownElements = styled.ul`
& > li {
background-color: ${({ theme }) => theme.color.neutral.surface.strong};
padding: 8px 16px;
display: flex;
align-items: center;
Expand Down Expand Up @@ -95,3 +125,18 @@ const Element = styled.label<{ $option: Option }>`
const Text = styled.span`
width: 100%;
`;

function formDataToObject(formData: FormData) {
const object: Record<string, (string | number)[]> = {};

for (const [name] of formData.entries()) {
const [key, value]: string[] = name.split(/\[|\]/).filter(Boolean);

if (!object[key]) {
object[key] = [];
}
object[key] = [...object[key], value];
}

return object;
}
73 changes: 49 additions & 24 deletions frontend/src/components/common/FilterBar.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,55 @@
import { useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import { styled } from 'styled-components';
import DropdownIndicator from './DropdownIndicator';
import Icons from '../../design/Icons';
import React from 'react';
import Option from '../../constant/Option';
import { AppContext } from '../../main';

type FilterBarProps = {
filterValue: string;
openPanel: () => void;
};
export default function FilterBar({ baseKeyword = 'status:open' }) {
const [keyword, setKeyword] = useState(baseKeyword);
const { util } = useContext(AppContext);
const filter = util.getFilter() as (keyword: string) => void;

export default function FilterBar({ filterValue, openPanel }: FilterBarProps) {
// const [filterValue, setFilterValue] = useState<string>(
// 'default filter option'
// );
const SearchIcon = Icons['search'];
useEffect(() => {
filter(keyword);
}, [filter, keyword]);

// const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// setFilterValue(e.target.value);
// };
function submitHandler(e: React.FormEvent) {
e.preventDefault();
filter(keyword);
}

return (
<Bar>
<StyledDropdownIndicator text="Button" onClick={openPanel} />
<TextFilter>
<SearchIcon />
<TextInput value={filterValue}></TextInput>
<Container>
<h3 className='blind'>필터바</h3>
<StyledDropdownIndicator
text="필터"
label="이슈 필터"
elements={[
{ text: '열린 이슈', option: Option.Selected },
{ text: '내가 작성한 이슈', option: Option.Available },
{ text: '나에게 할당된 이슈', option: Option.Available },
{ text: '내가 댓글을 남긴 이슈', option: Option.Available },
{ text: '닫힌 이슈', option: Option.Available },
]}
/>
<TextFilter onSubmit={submitHandler}>
<Icons.search />
<TextInput
id="filter"
name="filter"
value={keyword}
onChange={(e) => {
const input = e.target as HTMLInputElement;
setKeyword(input.value);
}}></TextInput>
</TextFilter>
</Bar>
</Container>
);
}

const Bar = styled.div`
const Container = styled.section`
width: 560px;
display: flex;
align-items: center;
Expand All @@ -41,16 +61,24 @@ const Bar = styled.div`
&:focus-within {
border-color: ${({ theme }) => theme.color.neutral.border.active};
background: ${({ theme }) => theme.color.neutral.surface.strong};
}
&:focus > form > input {
color: ${({ theme }) => theme.color.neutral.text.default};
}
`;

const StyledDropdownIndicator = styled(DropdownIndicator)`
height: 40px;
padding: 0 24px;
background: ${({ theme }) => theme.color.neutral.surface.default};
border-right: ${({ theme }) => theme.objectStyles.border.default};
border-color: ${({ theme }) => theme.color.neutral.border.default};
border-radius: ${({ theme }) => theme.objectStyles.radius.medium} 0 0
${({ theme }) => theme.objectStyles.radius.medium};
& > button {
padding: 8px 24px;
}
`;

const TextFilter = styled.form`
Expand All @@ -71,7 +99,4 @@ const TextInput = styled.input`
color: ${({ theme }) => theme.color.neutral.text.weak};
${({ theme }) => theme.font.display.medium[16]};
&:focus {
color: ${({ theme }) => theme.color.neutral.text.default};
}
`;
6 changes: 4 additions & 2 deletions frontend/src/components/common/button/BaseButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,13 @@ const RealButton = styled.button<StyledButtonProps>`
`;

const TextLabel = styled.span<{ $ghost?: boolean }>`
text-align: center;
display: flex;
gap: 4px;
align-items: center;
${({ $ghost }) => {
if (!$ghost) {
return 'padding: 0 8px;';
}
return 'padding: 0;';
return '';
}}
`;
24 changes: 20 additions & 4 deletions frontend/src/components/issues/IssueTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,32 @@ export default function IssueTable({ issues }: { issues: Issue[] }) {
</Left>
<Right>
<li>
<DropdownIndicator text="담당자" />
<DropdownIndicator
text="담당자"
label="담당자 필터"
elements={[]}
/>
</li>
<li>
<DropdownIndicator text="레이블" />
<DropdownIndicator
text="레이블"
label="레이블 필터"
elements={[]}
/>
</li>
<li>
<DropdownIndicator text="마일스톤" />
<DropdownIndicator
text="마일스톤"
label="마일스톤 필터"
elements={[]}
/>
</li>
<li>
<DropdownIndicator text="작성자" />
<DropdownIndicator
text="작성자"
label="작성자 필터"
elements={[]}
/>
</li>
</Right>
</Buttons>
Expand Down
Loading

0 comments on commit b0eba37

Please sign in to comment.