From b0eba377d2ee0024d9471dc32dafabc2f47495a0 Mon Sep 17 00:00:00 2001 From: Cat Hanbit Date: Thu, 17 Aug 2023 20:48:44 +0900 Subject: [PATCH] =?UTF-8?q?#104=20feat:=20=ED=95=84=ED=84=B0=EB=B0=94=20?= =?UTF-8?q?=EB=93=9C=EB=A1=AD=EB=8B=A4=EC=9A=B4=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 2 +- .../components/common/DropdownIndicator.tsx | 60 ++++++++-- .../src/components/common/DropdownPanel.tsx | 111 ++++++++++++------ frontend/src/components/common/FilterBar.tsx | 73 ++++++++---- .../components/common/button/BaseButton.tsx | 6 +- frontend/src/components/issues/IssueTable.tsx | 24 +++- frontend/src/constant/Option.ts | 6 + frontend/src/pages/Issues.tsx | 76 ++++-------- frontend/src/types/DropdownPanelElement.ts | 5 + 9 files changed, 235 insertions(+), 128 deletions(-) create mode 100644 frontend/src/constant/Option.ts create mode 100644 frontend/src/types/DropdownPanelElement.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a9d49e0a4..b0b873903 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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'; @@ -51,6 +50,7 @@ function App() { } /> } /> } /> + } /> {/* protected routes */} }> diff --git a/frontend/src/components/common/DropdownIndicator.tsx b/frontend/src/components/common/DropdownIndicator.tsx index 1135c70e6..0c10e271b 100644 --- a/frontend/src/components/common/DropdownIndicator.tsx +++ b/frontend/src/components/common/DropdownIndicator.tsx @@ -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 ( - - {text} - + + + + {isOpenPanel && ( + + )} + ); } -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}; } @@ -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); +`; diff --git a/frontend/src/components/common/DropdownPanel.tsx b/frontend/src/components/common/DropdownPanel.tsx index a394f9df3..cff3f351e 100644 --- a/frontend/src/components/common/DropdownPanel.tsx +++ b/frontend/src/components/common/DropdownPanel.tsx @@ -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(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 ( - +
{label}
- {elements.map(([text, option]) => { - const key = uuidV4(); - const Icon = Icons.userImageSmall; - return ( -
  • - - - {text} - - {option ? : } - -
  • - ); - })} + {elements.map(({ text, option }, index) => ( +
  • + + {user && {user.name} + {text} + itemCheckHandler(e, index)} + /> + {option === Option.Selected ? ( + + ) : ( + + )} + +
  • + ))}
    ); } +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}; @@ -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; @@ -95,3 +125,18 @@ const Element = styled.label<{ $option: Option }>` const Text = styled.span` width: 100%; `; + +function formDataToObject(formData: FormData) { + const object: Record = {}; + + 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; +} diff --git a/frontend/src/components/common/FilterBar.tsx b/frontend/src/components/common/FilterBar.tsx index 96ae11d1a..38d5f7085 100644 --- a/frontend/src/components/common/FilterBar.tsx +++ b/frontend/src/components/common/FilterBar.tsx @@ -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( - // 'default filter option' - // ); - const SearchIcon = Icons['search']; + useEffect(() => { + filter(keyword); + }, [filter, keyword]); - // const handleChange = (e: React.ChangeEvent) => { - // setFilterValue(e.target.value); - // }; + function submitHandler(e: React.FormEvent) { + e.preventDefault(); + filter(keyword); + } return ( - - - - - + +

    필터바

    + + + + { + const input = e.target as HTMLInputElement; + setKeyword(input.value); + }}> -
    +
    ); } -const Bar = styled.div` +const Container = styled.section` width: 560px; display: flex; align-items: center; @@ -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` @@ -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}; - } `; diff --git a/frontend/src/components/common/button/BaseButton.tsx b/frontend/src/components/common/button/BaseButton.tsx index 24f0f9a65..e012bd17f 100644 --- a/frontend/src/components/common/button/BaseButton.tsx +++ b/frontend/src/components/common/button/BaseButton.tsx @@ -145,11 +145,13 @@ const RealButton = styled.button` `; 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 ''; }} `; diff --git a/frontend/src/components/issues/IssueTable.tsx b/frontend/src/components/issues/IssueTable.tsx index 3d7777989..9a183ac7a 100644 --- a/frontend/src/components/issues/IssueTable.tsx +++ b/frontend/src/components/issues/IssueTable.tsx @@ -48,16 +48,32 @@ export default function IssueTable({ issues }: { issues: Issue[] }) {
  • - +
  • - +
  • - +
  • - +
  • diff --git a/frontend/src/constant/Option.ts b/frontend/src/constant/Option.ts new file mode 100644 index 000000000..79f2aa8e9 --- /dev/null +++ b/frontend/src/constant/Option.ts @@ -0,0 +1,6 @@ +enum Option { + Available, + Selected, +} + +export default Option; diff --git a/frontend/src/pages/Issues.tsx b/frontend/src/pages/Issues.tsx index f15386d91..9d80f9e18 100644 --- a/frontend/src/pages/Issues.tsx +++ b/frontend/src/pages/Issues.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from 'react'; +import { useCallback, useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { AppContext } from '../main'; import useAuth from '../hooks/useAuth'; @@ -15,12 +15,6 @@ import Layout from '../components/Layout'; import Button from '../components/common/button/BaseButton'; import { Data } from '../types'; import useAxiosPrivate from '../hooks/useAxiosPrivate'; -import DropdownPanel from '../components/common/DropdownPanel'; - -enum Option { - Available, - Selected, -} export default function Issues() { const { util } = useContext(AppContext); @@ -28,7 +22,6 @@ export default function Issues() { const logo = (util.getLogoByTheme() as ContextLogo).medium; const navigate = useNavigate(); const axiosPrivate = useAxiosPrivate(); - const [filterValue, setFilterValue] = useState('status:open'); const [data, setData] = useState({ openCount: 1, @@ -87,29 +80,23 @@ export default function Issues() { ], }); - // useEffect(() => { - // const fetchData = async () => { - // const params = { - // status: 'open', - // labels: '', - // milestone: '', - // writer: '', - // assignees: '', - // comment: '', - // }; + util.getFilter = () => (keywords: string) => { + const fetchData = async () => { + const params = keywordParser(keywords); - // const res = await axiosPrivate.get('/api/issues', { params }); + const res = await axiosPrivate.get('/api/issues', { params }); + + try { + if (res.status === 200) { + setData(res.data.message); + } + } catch (error) { + console.error(error); + } + }; + fetchData(); + }; - // try { - // if (res.status === 200) { - // setData(res.data.message); - // } - // } catch (error) { - // console.error(error); - // } - // }; - // fetchData(); - // }, []); const handleLogout = async () => { if (!auth) { return; @@ -148,22 +135,7 @@ export default function Issues() { - { - console.log('open filterbar'); - }} - /> - + theme.color.neutral.surface.strong}; -`; +function keywordParser(keyword: string) { + const params: Record = {}; + keyword.split(' ').forEach((token) => { + const [key, value] = token.split(':'); + params[key] = value; + }); + return params; +} diff --git a/frontend/src/types/DropdownPanelElement.ts b/frontend/src/types/DropdownPanelElement.ts new file mode 100644 index 000000000..85ba36733 --- /dev/null +++ b/frontend/src/types/DropdownPanelElement.ts @@ -0,0 +1,5 @@ +import Option from '../constant/Option'; + +type DropdownPanelElement = { text: string; option: Option }; + +export default DropdownPanelElement;