diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index c8da29701..f14a088be 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,14 +11,14 @@ import Labels from "./components/Labels/Labels"; import NewIssue from "./components/pages/NewIssuePage"; import { ThemeProvider } from "styled-components"; import theme from "styles/theme"; -import store from "./MyRecoil/store"; - -export const globalStateRoot = createContext(); +// import store from "./MyRecoil"; +// export const globalStateRoot = createContext(); function App() { - const globalState = useRef(store); + // const globalState = useRef(store); return ( - + // + @@ -26,11 +26,12 @@ function App() { - {/* */} + - + + // {/* */} ); } diff --git a/frontend/src/MyRecoil/atom.js b/frontend/src/MyRecoil/atom.js index 6c751256c..8022e62a6 100644 --- a/frontend/src/MyRecoil/atom.js +++ b/frontend/src/MyRecoil/atom.js @@ -1,28 +1,39 @@ -function atom({ key, initialState }) { - return { key, initialState }; -} +// // function atom({ key, initialState }) { +// // return { key, initialState }; +// // } -export const issueAtomState = atom({ - key: "issueAtomState", - initialState: true, -}); +// import { atom } from "./index"; +// export const issueState = atom({ +// key: "issueState", +// initialState: true, +// }); -export const milestoneAtomState = atom({ - key: "milestoneAtomState", - initialState: [], -}); +// export const milestoneState = atom({ +// key: "milestoneState", +// initialState: [], +// }); -export const asigneeAtomState = atom({ - key: "asigneeAtomState", - initialState: [], -}); +// export const asigneeState = atom({ +// key: "asigneeState", +// initialState: [], +// }); -export const labelAtomState = atom({ - key: "labelAtomState", - initialState: [], -}); +// export const labelState = atom({ +// key: "labelState", +// initialState: [], +// }); -export const authorAtomState = atom({ - key: "authorAtomState", - initialState: [], -}); +// export const authorState = atom({ +// key: "authorState", +// initialState: [], +// }); + +// export const selectedIssueCntState = atom({ +// key: "selectedIssueCntState", +// initialState: 0, +// }); + +// export const clickedFilterState = atom({ +// key: "clickedFilterState", +// initialState: null, +// }); diff --git a/frontend/src/MyRecoil/index.js b/frontend/src/MyRecoil/index.js new file mode 100644 index 000000000..765821a97 --- /dev/null +++ b/frontend/src/MyRecoil/index.js @@ -0,0 +1,86 @@ +// import { useContext, useState, useCallback, useEffect } from "react"; +// import { globalStateRoot } from "App"; + +// export const atom = ({ key, initialState }) => { +// return { key, initialState }; +// }; + +// export const useRecoilState = atom => { +// const { key } = atom; +// const store = useContext(globalStateRoot).current; +// const [, setReRender] = useState({}); +// store.setInitState(atom); + +// const forceUpdate = useCallback(() => { +// console.log("forced Update"); +// setReRender({}); +// }, []); + +// useEffect(() => { +// store.subscribe({ key, fn: forceUpdate }); +// }, []); + +// return [store.getState(key), store.setState(key)]; +// }; +// export const useRecoilValue = atom => { +// const { key } = atom; +// const store = useContext(globalStateRoot).current; +// const [, setReRender] = useState({}); +// store.setInitState(atom); + +// const forceUpdate = useCallback(() => { +// console.log("forced Update"); +// setReRender({}); +// }, []); + +// useEffect(() => { +// store.subscribe({ key, fn: forceUpdate }); +// }, []); + +// return store.getState(key); +// }; + +// export const useSetRecoilState = atom => { +// const { key } = atom; +// const store = useContext(globalStateRoot).current; +// const [, setReRender] = useState({}); +// store.setInitState(atom); + +// const forceUpdate = useCallback(() => { +// console.log("forced Update"); +// setReRender({}); +// }, []); + +// useEffect(() => { +// store.subscribe({ key, fn: forceUpdate }); +// }, []); + +// return store.setState(key); +// }; + +// export const store = { +// observers: {}, +// state: {}, +// subscribe({ key, fn }) { +// this.observers[key] = this.observers[key] || []; +// this.observers[key].push(fn); +// }, + +// setInitState({ key, initialState }) { +// if (this.state[key]) return; +// this.state[key] = initialState; +// }, + +// getState(key) { +// return this.state[key]; +// }, + +// setState(key) { +// return fn => { +// this.state[key] = fn(this.state[key]); +// this.observers[key].forEach(callback => callback()); +// }; +// }, +// }; + +// export default store; diff --git a/frontend/src/MyRecoil/store.js b/frontend/src/MyRecoil/store.js deleted file mode 100644 index 84fd34f74..000000000 --- a/frontend/src/MyRecoil/store.js +++ /dev/null @@ -1,26 +0,0 @@ -const store = { - observers: {}, - state: {}, - subscribe({ key, fn }) { - this.observers[key] = this.observers[key] || []; - this.observers[key].push(fn); - }, - - setInitState({ key, initialState }) { - if (this.state[key]) return; - this.state[key] = initialState; - }, - - getState(key) { - return this.state[key]; - }, - - setState(key) { - return fn => { - this.state[key] = fn(this.state[key]); - this.observers[key].forEach(callback => callback()); - }; - }, -}; - -export default store; diff --git a/frontend/src/MyRecoil/useRecoilState.js b/frontend/src/MyRecoil/useRecoilState.js deleted file mode 100644 index ed97086f4..000000000 --- a/frontend/src/MyRecoil/useRecoilState.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useContext, useState, useCallback, useEffect } from "react"; -import { globalStateRoot } from "App"; - -export const useRecoilState = atom => { - const { key } = atom; - const store = useContext(globalStateRoot).current; - const [, setReRender] = useState({}); - store.setInitState(atom); - - const forceUpdate = useCallback(() => { - console.log("forced Update"); - setReRender({}); - }, []); - - useEffect(() => { - store.subscribe({ key, fn: forceUpdate }); - }, []); - - return [store.getState(key), store.setState(key)]; -}; diff --git a/frontend/src/MyRecoil/useRecoilValue.js b/frontend/src/MyRecoil/useRecoilValue.js deleted file mode 100644 index 84e664981..000000000 --- a/frontend/src/MyRecoil/useRecoilValue.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useContext, useState, useCallback, useEffect } from "react"; -import { globalStateRoot } from "App"; - -export const useRecoilValue = atom => { - const { key } = atom; - const store = useContext(globalStateRoot).current; - const [, setReRender] = useState({}); - store.setInitState(atom); - - const forceUpdate = useCallback(() => { - console.log("forced Update"); - setReRender({}); - }, []); - - useEffect(() => { - store.subscribe({ key, fn: forceUpdate }); - }, []); - - return store.getState(key); -}; diff --git a/frontend/src/MyRecoil/useSetRecoilState.js b/frontend/src/MyRecoil/useSetRecoilState.js deleted file mode 100644 index 6808ce1ab..000000000 --- a/frontend/src/MyRecoil/useSetRecoilState.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useContext, useState, useCallback, useEffect } from "react"; -import { globalStateRoot } from "App"; - -export const useSetRecoilState = atom => { - const { key } = atom; - const store = useContext(globalStateRoot).current; - const [, setReRender] = useState({}); - store.setInitState(atom); - - const forceUpdate = useCallback(() => { - console.log("forced Update"); - setReRender({}); - }, []); - - useEffect(() => { - store.subscribe({ key, fn: forceUpdate }); - }, []); - - return store.setState(key); -}; diff --git a/frontend/src/RecoilStore/Atoms.jsx b/frontend/src/RecoilStore/Atoms.jsx index 9bfcf9173..52c71deb9 100644 --- a/frontend/src/RecoilStore/Atoms.jsx +++ b/frontend/src/RecoilStore/Atoms.jsx @@ -1,26 +1,67 @@ import { atom } from "recoil"; -export const issueAtomState = atom({ - key: "issueFilter", - default: "열린 이슈", +export const selectedIssueCntState = atom({ + key: "selectedIssueCntState", + default: 0, }); -export const assigneeAtomState = atom({ - key: "assigneeFilter", - default: [], +export const clickedFilterState = atom({ + key: "clickedFilterState", + default: null, }); -export const labelAtomState = atom({ - key: "labelFilter", - default: [], +export const filterBarInputState = atom({ + key: "filterBarInputState", + default: { + placeholder: "is:issue is:open", + assignee: null, + label: null, + milestone: null, + author: null, + issue: null, + }, }); -export const milestoneAtomState = atom({ - key: "milestoneFilter", - default: [], +export const labelButtonFlagState = atom({ + key: "labelButtonFlagState", + default: true, }); -export const authorAtomState = atom({ - key: "authorFilter", - default: [], +export const milestoneButtonFlagState = atom({ + key: "milestoneButtonFlag", + default: false, +}); + +export const milestoneAddButtonFlagState = atom({ + key: "milestoneAddButtonFlagState", + default: false, +}); + +export const milestoneUpdateState = atom({ + key: "milestoneAddButtonFlagState", + default: false, +}); + +export const labelInitialData = atom({ + key: "labelInitialData", + default: null, +}); +export const labelAddButtonFlagState = atom({ + key: "labelAddButtonFlagState", + default: false, +}); + +export const labelEditButtonFlagState = atom({ + key: "labelEditButtonFlagState", + default: false, +}); + +export const navigatorAddButtonFlagState = atom({ + key: "navigatorAddButtonFlagState", + default: false, +}); + +export const milestoneCountState = atom({ + key: "milestoneCountState", + default: 0, }); diff --git a/frontend/src/components/Issues/IssueList/IssueCard.jsx b/frontend/src/components/Issues/IssueList/IssueCard.jsx index 22c239d86..f6b64af90 100644 --- a/frontend/src/components/Issues/IssueList/IssueCard.jsx +++ b/frontend/src/components/Issues/IssueList/IssueCard.jsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from "react"; import styled from "styled-components"; import theme from "styles/theme"; import { Link } from "react-router-dom"; @@ -5,13 +6,57 @@ import { ReactComponent as UserImageSmall } from "images/UserImageSmall.svg"; import { ReactComponent as Alert } from "images/alert-circle.svg"; import { ReactComponent as Milestone } from "images/milestone.svg"; import getTimeStamp from "util/getTimeStamp"; +// import { useRecoilState } from "MyRecoil"; +// import { selectedIssueCntState } from "MyRecoil/atom"; +import { selectedIssueCntState } from "RecoilStore/Atoms"; +import { useRecoilState } from "recoil"; +import { StyledGridCard } from "styles/StyledCards"; -const IssueCard = ({ issue }) => { +const IssueCard = ({ + issue, + setIsAnyIssueSelected, + isAllIssueSelected, + setIsAllIssueSelected, + selectedCards, + setSelectedCards, +}) => { + const [isChecked, setIsChecked] = useState(false); + const [selectedIssues, setSelectedIssues] = useRecoilState( + selectedIssueCntState + ); const { title, id, labelId, milestoneId, author, createdAt } = issue; + const handleCheck = () => { + setIsChecked(!isChecked); + if (isChecked) { + setSelectedIssues(selectedIssues - 1); + selectedCards.delete(id); + } + if (!isChecked) { + setSelectedIssues(selectedIssues + 1); + setSelectedCards(selectedCards.add(id)); + } + }; + // console.log(selectedCards); + // console.log(selectedIssues); + useEffect(() => { + isChecked ? setIsAnyIssueSelected(true) : setIsAnyIssueSelected(false); + }, [isChecked]); + + useEffect(() => { + if (isAllIssueSelected) { + setIsChecked(true); + setSelectedCards(selectedCards.add(id)); + } + if (!isAllIssueSelected) { + setIsChecked(false); + selectedCards.delete(id); + } + }, [isAllIssueSelected]); + return ( - + - +
-
@@ -19,36 +64,35 @@ const IssueCard = ({ issue }) => { {title} - {labelId} {/* labelId 로 라벨 정보 fetch 해와서 title, 박스 렌더링 */} + + 라벨 위치 + {/* labelId 로 라벨 정보 fetch 해와서 title, 라벨 렌더링 */} + #{id} - - 이 이슈가 {getTimeStamp(createdAt)}, {author}님에 의해 작성되었습니다 - - - - - {milestoneId} {/* milestoneId 로 마일스톤 정보 fetch 해와서 title만 뿌리기 */} + + 이 이슈가 {getTimeStamp(createdAt)}, {author}님에 의해 + 작성되었습니다 + + + + + + 마일스톤 이름 + {/* milestoneId 로 마일스톤 정보 fetch 해와서 title만 뿌리기 */} + -
+ ); }; export default IssueCard; -const StyleCard = styled.div` - display: grid; - grid-template-columns: 0.5fr 9fr 1fr; - background-color: ${theme.grayScale.off_white}; - border: 1px solid ${theme.grayScale.line}; - height: 100px; -`; - const CheckBox = styled.div` display: flex; justify-content: space-evenly; @@ -68,8 +112,8 @@ const Contents = styled.div` flex-direction: column; `; -const TextTagDivider = styled.div` - display: flex; - /* justify-content: space-between; */ - /* width: 30%; */ +const TextTagDivider = styled.div``; + +const Padder = styled.span` + padding: 0 5px; `; diff --git a/frontend/src/components/Issues/IssueList/IssueList.jsx b/frontend/src/components/Issues/IssueList/IssueList.jsx index 90c5f42f8..83154d7a2 100644 --- a/frontend/src/components/Issues/IssueList/IssueList.jsx +++ b/frontend/src/components/Issues/IssueList/IssueList.jsx @@ -1,17 +1,51 @@ -import { useContext, useState } from "react"; +import { useContext, useState, useEffect } from "react"; import styled from "styled-components"; import IssuesHeader from "./IssuesHeader"; import IssueCard from "./IssueCard"; import { issues } from "data"; +// import { selectedIssueCntState } from "MyRecoil/atom"; +import { useRecoilState } from "recoil"; +import { selectedIssueCntState } from "RecoilStore/Atoms"; +// import { useRecoilState } from "MyRecoil"; const IssueList = () => { - const issueList = issues.map((issue) => ); - // const [isIssueSelected, setIsIssueSelected] = useState(false); // 상태 위치 협의 후 수정 - // const [isAllIssueSelected, setIsAllIssueSelected] = useState(false); + const [isAnyIssueSelected, setIsAnyIssueSelected] = useState(false); // 상태 위치 협의 후 수정 + const [isAllIssueSelected, setIsAllIssueSelected] = useState(false); + const [_, setSelectedIssues] = useRecoilState(selectedIssueCntState); + const [selectedCards, setSelectedCards] = useState(new Set()); + + useEffect(() => { + if (!isAllIssueSelected && !isAnyIssueSelected && !selectedCards.size) + setSelectedIssues(() => 0); + }, [isAnyIssueSelected]); + + const issueList = issues.map((issue) => ( + + )); + return ( - - {issueList} + + {issueList.length ? ( + issueList + ) : ( + 검색과 일치하는 결과가 없습니다 + )} ); }; @@ -23,3 +57,12 @@ const StyledIssueList = styled.div` flex-direction: column; width: 100%; `; + +const ErrorCard = styled.div` + display: flex; + justify-content: center; + align-items: center; + background-color: ${({ theme }) => theme.grayScale.off_white}; + border: 1px solid ${({ theme }) => theme.grayScale.line}; + height: 100px; +`; diff --git a/frontend/src/components/Issues/IssueList/IssuesHeader.jsx b/frontend/src/components/Issues/IssueList/IssuesHeader.jsx index 0bcb768b1..3900d721f 100644 --- a/frontend/src/components/Issues/IssueList/IssuesHeader.jsx +++ b/frontend/src/components/Issues/IssueList/IssuesHeader.jsx @@ -1,80 +1,137 @@ -import { useState } from "react"; +import { useState, useEffect, useCallback } from "react"; import styled from "styled-components"; import { ReactComponent as Archive } from "images/archive.svg"; import { ReactComponent as Alert } from "images/alert-circle.svg"; import { ReactComponent as DownArrow } from "images/chevron_down.svg"; import theme from "styles/theme"; -import { useEffect } from "react"; +import DropDownButton from "components/common/Button/DropDownButton"; +import FilterModal from "components/common/FilterModal"; +import { filter } from "data"; +import { StyledGridTitleCard } from "styles/StyledCards"; + +// import { selectedIssueCntState, clickedFilterState } from "MyRecoil/atom"; +// import { useRecoilState } from "MyRecoil"; +import { useRecoilState, useSetRecoilState } from "recoil"; +import { + selectedIssueCntState, + clickedFilterState, + filterBarInputState, +} from "RecoilStore/Atoms"; + +const IssuesHeader = ({ + isAnyIssueSelected, + setIsAnyIssueSelected, + isAllIssueSelected, + setIsAllIssueSelected, + issuesCnt, + selectedCards, +}) => { + const [selectedIssues, setSelectedIssues] = useRecoilState( + selectedIssueCntState + ); + const buttonNames = ["담당자", "레이블", "마일스톤", "작성자"]; + const setClickedFilterState = useSetRecoilState(clickedFilterState); + // const setFilterBarInputState = useSetRecoilState(filterBarInputState); + + //----------중복 코드from MeuFilter -------- + const [isFilterClicked, setIsFilterClicked] = useState(false); + const handleClick = useCallback(e => { + isFilterClicked === false + ? setIsFilterClicked(true) + : setIsFilterClicked(false); + console.dir(e.target.textContent); + console.dir(e.target); + setClickedFilterState(e.target.textContent); + // setFilterBarInputState(e.target.textContent); //여기 + }); + + useEffect(() => { + window.addEventListener("click", closeFilterModal); + return function cleanup() { + window.removeEventListener("click", closeFilterModal); + }; + }, [isFilterClicked]); + + const closeFilterModal = e => { + const target = e.target; + if ( + isFilterClicked && + !target.closest(".filter-modal") && + !target.closest(".issue-header-button") + ) + setIsFilterClicked(false); + }; + //----------여기 까지 중복 코드from MeuFilter --------/ -const IssuesHeader = ({}) => { - const [isIssueOpenFilter, setIsIssueOpenFilter] = useState(true); // means issueOpen clicked - const [isIssueSelected, setIsIssueSelected] = useState(false); // 상태 위치 협의 후 수정 - const [isAllIssueSelected, setIsAllIssueSelected] = useState(false); const checkAllIssue = () => { setIsAllIssueSelected(!isAllIssueSelected); + isAllIssueSelected ? setSelectedIssues(0) : setSelectedIssues(issuesCnt); }; useEffect(() => { - isAllIssueSelected && console.log("issue checkbox all selected"); - }, [isAllIssueSelected]); + if (selectedIssues === 0) setIsAnyIssueSelected(false); + else setIsAnyIssueSelected(true); + }, [selectedIssues]); return ( - + - - - 열린 이슈(n) - - - 닫힌 이슈(n) - - + {isAnyIssueSelected ? ( +
{selectedIssues}개 이슈 선택
+ ) : ( + + + 열린 이슈(n) + + + 닫힌 이슈(n) + + + )} - {isIssueSelected ? ( + {isAnyIssueSelected ? ( - 상태 수정 - + theme.buttonWidths.lg} + border={"none"} + > + {isFilterClicked && } ) : ( - - 담당자 - - - - 레이블 - - - - 마일스톤 - - - - 작성자 - - + {buttonNames.map((filter, idx) => ( + + theme.buttonWidths.small} + border={"none"} + > + + ))} + + {isFilterClicked && } + {/* 클릭된게 어떤 필터인지를 modal이 알아야 함 useRecoilState 쓰려다 오류나서 state props로 내림 */} + {/* 🔥recoil로 수정 필요 */} + {/* 🔥회살표 클릭해도 필터 제대로 뜨도록 수정필요 */} )} -
+ ); }; export default IssuesHeader; -const StyledIssuesHeader = styled.div` - display: grid; - align-items: center; - height: 64px; - border: 1px solid ${theme.grayScale.line}; - border-radius: ${theme.border_radius.lg} ${theme.border_radius.lg} 0px 0px; - grid-template-columns: 0.5fr 1.5fr 8.3fr; -`; - const FilterOpenClose = styled.div` display: flex; justify-content: space-between; @@ -89,12 +146,14 @@ const OpenCloseEdit = styled.div` display: flex; width: 200px; justify-content: center; + /* position: relative; */ `; const FiltersWrapper = styled.div` display: flex; - width: 500px; justify-content: space-around; + position: relative; + /* outline: red 1px solid; */ `; export const CheckBox = styled.div` @@ -103,7 +162,5 @@ export const CheckBox = styled.div` `; const TextIconDivider = styled.div` - display: flex; - justify-content: space-between; - align-items: center; + /* position: relative; */ `; diff --git a/frontend/src/components/Issues/Menu/MenuFilterBar.jsx b/frontend/src/components/Issues/Menu/MenuFilterBar.jsx index cc99f5c21..b5e5acf73 100644 --- a/frontend/src/components/Issues/Menu/MenuFilterBar.jsx +++ b/frontend/src/components/Issues/Menu/MenuFilterBar.jsx @@ -1,70 +1,94 @@ import styled from "styled-components"; import { ReactComponent as SearchIcon } from "images/search.svg"; -import DropDownButton from "components/common/DropDownButton"; +import DropDownButton from "components/common/Button/DropDownButton"; import theme from "styles/theme"; import FilterModal from "components/common/FilterModal"; import { useState, useEffect } from "react"; +import { clickedFilterState, filterBarInputState } from "RecoilStore/Atoms"; +import { useSetRecoilState, useRecoilValue } from "recoil"; const MenuFilterBar = () => { - const [isFilterClicked, setIsFilterClicked] = useState(false); //isFilterClicked말고 flag에 쓸만한 이름 추천좀.. - const handleClick = () => { + const [isFilterClicked, setIsFilterClicked] = useState(false); + const setClickedFilterState = useSetRecoilState(clickedFilterState); + const filterBarInput = useRecoilValue(filterBarInputState); + + const getFilterBarString = () => { + return Object.entries(filterBarInput).reduce((acc, item) => { + if (item[1]) { + if (item[0] === "placeholder") acc += `${item[1]} `; + else acc += `${item[0]}:${item[1]} `; + } + return acc; + }, ""); + }; + + const handleClick = e => { isFilterClicked === false ? setIsFilterClicked(true) : setIsFilterClicked(false); + setClickedFilterState(e.target.textContent); }; - //토글 하면됨 useEffect(() => { - document.body.addEventListener("click", closePopup); + window.addEventListener("click", closeFilterModal); return function cleanup() { - window.removeEventListener("click", closePopup); + window.removeEventListener("click", closeFilterModal); }; - }, []); + }, [isFilterClicked]); - const closePopup = e => { + const closeFilterModal = e => { const target = e.target; - // console.log(target); - !target.closest(".filter-modal") && setIsFilterClicked(false); + if (isFilterClicked && !target.closest(".filter-modal")) + setIsFilterClicked(false); }; + const handleInputChange = () => {}; return ( <> - + theme.buttonWidths.base} + radius={"left"} + /> + - + - is:issue is:open - + + {getFilterBarString(filterBarInput)} - {} + {isFilterClicked && } ); }; const MenuFilterLayout = styled.div` display: flex; - width: 601px; + width: 700px; height: 40px; position: relative; + margin-bottom: 24px; `; + const FilterInputContainer = styled.div` + display: flex; width: 100%; height: 100%; + padding: 11px; border: 1px solid ${theme.grayScale.line}; + background-color: ${theme.grayScale.input_background}; border-radius: ${theme.border_radius_mix.right}; `; +const FilterInputIconContainer = styled.div``; const FilterInput = styled.div` - display: flex; width: 100%; - height: 100%; - padding: 2.3%; + padding-left: 10px; + border: none; background-color: ${theme.grayScale.input_background}; - border-radius: ${theme.border_radius_mix.right}; -`; -const FilterInputText = styled.div` - padding: 0.4% 2%; color: ${theme.grayScale.placeholder}; + border-radius: ${theme.border_radius_mix.right}; `; export default MenuFilterBar; diff --git a/frontend/src/components/Issues/Menu/MenuTab.jsx b/frontend/src/components/Issues/Menu/MenuTab.jsx index 22bc825bb..43660569d 100644 --- a/frontend/src/components/Issues/Menu/MenuTab.jsx +++ b/frontend/src/components/Issues/Menu/MenuTab.jsx @@ -1,12 +1,42 @@ -import ButtonGroup from "components/common/ButtonGroup"; -import AddButton from "components/common/AddButton"; +import styled from "styled-components"; +import ButtonGroup from "components/common/Button/ButtonGroup"; +import AddButton from "components/common/Button/BlueButtons"; +import { Link } from "react-router-dom"; +import { useSetRecoilState } from "recoil"; +import { + labelButtonFlagState, + milestoneButtonFlagState, +} from "RecoilStore/Atoms"; const MenuTab = () => { + const setMilestoneFlag = useSetRecoilState(milestoneButtonFlagState); + const setLabelFlag = useSetRecoilState(labelButtonFlagState); + + const handleMilestoneClick = () => { + setMilestoneFlag(true); + setLabelFlag(false); + }; + const handleLabelClick = () => { + setMilestoneFlag(false); + setLabelFlag(true); + }; return ( -
- - -
+ + + + + + ); }; +const Wrapper = styled.div` + display: flex; +`; + export default MenuTab; diff --git a/frontend/src/components/Labels/LabelCard.jsx b/frontend/src/components/Labels/LabelCard.jsx index e69de29bb..19a7fc4b9 100644 --- a/frontend/src/components/Labels/LabelCard.jsx +++ b/frontend/src/components/Labels/LabelCard.jsx @@ -0,0 +1,82 @@ +import styled from "styled-components"; +import { StyledGridCard } from "styles/StyledCards"; +import LabelBadge from "components/common/LabelBadge"; +import { ReactComponent as EditIcon } from "images/edit.svg"; +import { ReactComponent as TrashIcon } from "images/trash.svg"; +import theme from "styles/theme"; +import { CenterJcAi, CenterAi } from "styles/StyledLayout "; +import { labelInitialData, labelEditButtonFlagState } from "RecoilStore/Atoms"; +import { useRecoilValue, useSetRecoilState, useRecoilState } from "recoil"; +import API from "util/API"; +import fetchData from "util/fetchData"; +import LabelInput from "./LabelInput"; +import { useEffect } from "react"; +const LabelCard = ({ initialData }) => { + const { id, name, description, colors } = initialData; //최초로 겟해온 데이터 + const { backgroundColor, textColor } = colors; + + const [editBtnFlag, setLabelEditBtnFlag] = useRecoilState( + labelEditButtonFlagState + ); + const setLabelInitialData = useSetRecoilState(labelInitialData); + const handleEditButton = () => { + setLabelEditBtnFlag(true); + }; + const handleDeleteButton = async () => { + await fetchData(API.labelsId(id), "DELETE"); + const { labels } = await fetchData(API.labels(), "GET"); + setLabelInitialData(labels); + }; + + return ( + <> + {editBtnFlag ? ( + + ) : ( + + + + + + {description} + + + + theme.grayScale.label} + onClick={handleEditButton} + > + 편집 + + + theme.colors.red} + onClick={handleDeleteButton} + > + 삭제 + + + + )} + + ); +}; + +const LabelDescription = styled.div` + color: ${({ theme }) => theme.grayScale.label}; +`; + +const ButtonText = styled.div` + font-weight: bold; + font-size: ${({ theme }) => theme.fontSizes.xxs}; + color: ${props => props._color}; + padding: 3%; + margin-right: 24px; + cursor: pointer; +`; + +export default LabelCard; diff --git a/frontend/src/components/Labels/LabelInput.jsx b/frontend/src/components/Labels/LabelInput.jsx index e69de29bb..577a27461 100644 --- a/frontend/src/components/Labels/LabelInput.jsx +++ b/frontend/src/components/Labels/LabelInput.jsx @@ -0,0 +1,309 @@ +import styled from "styled-components"; +import { DisplayFlex, CenterAi } from "styles/StyledLayout "; +import { ReactComponent as RefreshIcon } from "images/refresh-ccw.svg"; +import { useEffect, useReducer } from "react"; +import LabelBadge from "components/common/LabelBadge"; +import theme from "styles/theme"; +import { labelData } from "data"; +import API from "util/API"; +import fetchData from "util/fetchData"; +import CancelButton from "components/common/Button/WhiteButtons"; +import SubmitButton from "components/common/Button/BlueButtons"; +import { + labelInitialData, + labelEditButtonFlagState, + labelAddButtonFlagState, + navigatorAddButtonFlagState, +} from "RecoilStore/Atoms"; + +import { useSetRecoilState, useRecoilState, useRecoilValue } from "recoil"; + +const LabelInput = ({ initialData }) => { + const setLabelInitialData = useSetRecoilState(labelInitialData); + const [editBtnFlag, setLabelEditBtnFlag] = useRecoilState( + labelEditButtonFlagState + ); + const setLabelAddBtnFlag = useSetRecoilState(labelAddButtonFlagState); + const setNavigatorAddBtnFlag = useSetRecoilState(navigatorAddButtonFlagState); + + const { + creatorTitle, + editorTitle, + nameTitle, + descriptionTitle, + backgroundColorTitle, + textColorTitles, + buttons, + } = labelData; + + const { id, name, description, colors } = initialData; + // const { backgroundColor, textColor } = colors; + const backgroundColor = "#000"; + const textColor = "#000"; + + const initLabelState = { + name: "", + description: "", + backgroundColor: theme.grayScale.input_background, + textColor: "#000000", + }; + + const reducer = (state, { type, payload }) => { + switch (type) { + case "name": + return { ...state, name: payload }; + case "description": + return { ...state, description: payload }; + case "backgroundColor": + return { ...state, backgroundColor: payload }; + case "textColor": + return { ...state, textColor: payload }; + } + }; + const [labelState, dispatch] = useReducer(reducer, initLabelState); + + //dispatch모아서 처리하게끔 refactoring 필요 + const handleClickRadioButton = event => { + const currentTextColor = + event.target.value === + dispatch({ type: "textColor", payload: event.target.value }); + }; + const handleChangeColor = event => { + //디바운스 필요(유저가 입력하고 1초 뒤에 set 하도록) + //const test = useDebounce(labelState.description, 1000); + dispatch({ type: "backgroundColor", payload: event.target.value }); + }; + + const handleChangeName = event => { + dispatch({ type: "name", payload: event.target.value }); + }; + + const handleChangeDescription = event => { + dispatch({ type: "description", payload: event.target.value }); + }; + const handleClose = () => { + setLabelEditBtnFlag(!editBtnFlag); + }; + + const handleAdd = async () => { + const { name, description, backgroundColor, textColor } = labelState; + + const requestBody = { + name: name, + description: description, + colors: { + backgroundColor: backgroundColor, + textColor: textColor, + }, + }; + + await fetchData(API.labels(), "POST", requestBody); + setLabelAddBtnFlag(x => !x); + setNavigatorAddBtnFlag(x => !x); + const { labels } = await fetchData(API.labels(), "GET"); + setLabelInitialData(labels); + }; + + const handleEdit = async () => { + //현재 내가 클릭한 카드의 데이터, id를 보여줘야 함 how? + + const { name, description, backgroundColor, textColor } = labelState; + const requestBody = { + name: name, + description: description, + colors: { + backgroundColor: backgroundColor, + textColor: textColor, + }, + }; + + const { labels } = await fetchData(API.labelsId("id"), "PUT", requestBody); //PUT요청, body수정 필요 + setLabelEditBtnFlag(!editBtnFlag); + setLabelInitialData(labels); //상태바뀐걸로 리렌더 + }; + + const changeColor = () => { + const randomColor = Math.floor(Math.random() * 16777215).toString(16); + dispatch({ + type: "backgroundColor", + payload: `#${randomColor}`, + }); + }; + + return ( + + {editBtnFlag ? editorTitle : creatorTitle} + + + { + + } + + + + {/* 타이틀 부분 나눠서 디스크립션 부분은 온 체인지 풀기 */} + + + {nameTitle} + + + + + {descriptionTitle} + + + + + + {backgroundColorTitle} + + + + + {textColorTitles} + + {buttons.radio.map((button, idx) => { + return ( + <> + + {button.text} + + ); + })} + + + + + {editBtnFlag && ( + + )} + {editBtnFlag ? ( + + ) : ( + + )} + + + + + ); +}; + +const LabelInputLayout = styled.div` + height: 345px; + background-color: ${({ theme }) => theme.grayScale.off_white}; + border: 1px solid ${({ theme }) => theme.grayScale.line}; + border-radius: ${props => (props.editBtnFlag ? "none" : "16px")}; + margin-bottom: ${props => (props.editBtnFlag ? 0 : "2%")}; +`; + +const MainLayout = styled.div` + display: flex; + position: relative; +`; + +const ButtonContainer = styled.div` + display: flex; + justify-content: flex-end; +`; + +const PreviewContainer = styled.div` + width: 20%; + display: flex; + justify-content: center; + align-items: center; +`; + +const SettingContainer = styled.div` + width: 80%; + padding: 0 50px; +`; + +const TextInputContainer = styled(CenterAi)` + display: flex; + padding: 0px 24px; + margin-right: 1%; + margin-bottom: 16px; + background: ${({ theme }) => theme.grayScale.input_background}; + border-radius: 11px; + width: ${props => props._width}; + height: 40px; + border: none; + color: ${({ theme }) => theme.grayScale.title_active}; +`; + +const RadioButton = styled.input` + cursor: pointer; +`; + +const Title = styled.div` + font-size: ${({ theme }) => theme.fontSizes.xxxl}; + padding: 32px; +`; + +const SubTitle = styled(CenterAi)` + width: 80px; + height: 40px; + font-weight: 500; + font-size: 12px; + line-height: 20px; + color: ${({ theme }) => theme.grayScale.label}; +`; + +const TextInput = styled.input` + background: ${({ theme }) => theme.grayScale.input_background}; + width: 100%; + height: 100%; + + color: ${({ theme }) => theme.grayScale.title_active}; + border: none; +`; +const RadioText = styled.div` + color: ${({ theme }) => theme.grayScale.body}; + font-size: ${({ theme }) => theme.fontSizes.xs}; + line-height: 28px; + padding: 0; +`; + +const Icon = styled(RefreshIcon)` + cursor: pointer; +`; + +export default LabelInput; diff --git a/frontend/src/components/Labels/Labels.jsx b/frontend/src/components/Labels/Labels.jsx index 3b82caeef..0276c9278 100644 --- a/frontend/src/components/Labels/Labels.jsx +++ b/frontend/src/components/Labels/Labels.jsx @@ -1,5 +1,48 @@ +import styled from "styled-components"; +import LabelCard from "./LabelCard"; +import LabelInput from "./LabelInput"; +import { StyledGridTitleCard } from "styles/StyledCards"; +import { useState, useEffect } from "react"; +import API from "util/API"; +import fetchData from "util/fetchData"; +import { + labelInitialData, + labelAddButtonFlagState, + labelEditButtonFlagState, +} from "RecoilStore/Atoms"; +import { useSetRecoilState, useRecoilValue, useRecoilState } from "recoil"; + const Labels = () => { - return
레이블
; + const [initialData, setLabelInitialData] = useRecoilState(labelInitialData); + const labelAddBtnFlag = useRecoilValue(labelAddButtonFlagState); + + const getLabelData = async () => { + const { labels } = await fetchData(API.labels(), "GET"); + setLabelInitialData(labels); + }; + + useEffect(() => { + getLabelData(); + }, []); + + return ( + <> + {labelAddBtnFlag ? : <>} + + N개의 레이블 + + {initialData && + initialData.map(data => ( + + ))} + + ); }; +const HeaderTitle = styled.div` + font-weight: bold; + color: ${({ theme }) => theme.grayScale.label}; + align-items: center; + padding: 0 6%; +`; export default Labels; diff --git a/frontend/src/components/Milestones/MilestoneCard.jsx b/frontend/src/components/Milestones/MilestoneCard.jsx index e69de29bb..542412754 100644 --- a/frontend/src/components/Milestones/MilestoneCard.jsx +++ b/frontend/src/components/Milestones/MilestoneCard.jsx @@ -0,0 +1,190 @@ +import { useState } from "react"; +import styled from "styled-components"; +import { StyledFlexCard } from "styles/StyledCards"; +import { ReactComponent as Milestone } from "images/milestone.svg"; +import { ReactComponent as Archive } from "images/archive.svg"; +import { ReactComponent as Trash } from "images/trash.svg"; +import { ReactComponent as EditIcon } from "images/edit.svg"; +import getPercent from "util/getPercent"; +import theme from "styles/theme"; +import API from "util/API"; +import fetchData from "util/fetchData"; +import { + milestoneAddButtonFlagState, + milestoneUpdateState, +} from "RecoilStore/Atoms"; +import { useRecoilState } from "recoil"; +import MilestoneInput from "./MilestoneInput"; + +const MilestoneCard = ({ data }) => { + const { id, title, description, dueDate, openIssues, closedIssues } = data; + const [update, forceUpdate] = useRecoilState(milestoneUpdateState); + const [editMode, setEditMode] = useState(false); + const [milestoneAddBtn, setMilestoneAddBtn] = useRecoilState( + milestoneAddButtonFlagState + ); + const deleteMilestone = async () => { + await fetchData(API.milestonesId(id), "DELETE"); + setMilestoneAddBtn(false); + }; + + const handleClose = () => {}; + + const handleEdit = () => { + setEditMode(true); + }; + + const handleDelete = () => { + deleteMilestone(); + forceUpdate(!update); + }; + + return ( + <> + {editMode ? ( + + ) : ( + + + + + + + {title} + + {dueDate} + + {description} + + + + theme.grayScale.label} + onClick={handleClose} + > + + 닫기 + + theme.grayScale.label} + onClick={handleEdit} + > + + 편집 + + theme.colors.red} + onClick={handleDelete} + > + + 삭제 + + +
+ + + + + {getPercent(openIssues, closedIssues)}% + + 열린 이슈 {openIssues}{" "} + 닫힌 이슈 {closedIssues} + + +
+
+
+
+ )} + + ); +}; + +export default MilestoneCard; + +const Info = styled.div` + display: flex; + flex-direction: column; + justify-content: space-evenly; +`; + +const Edit = styled.div` + display: flex; + flex-direction: column; + justify-content: space-evenly; + align-items: flex-end; + width: 20%; +`; + +const Detail = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; +`; + +const ContentsWrapper = styled.div` + display: flex; + justify-content: space-between; + width: 97%; +`; + +const EditBlock = styled.div` + display: flex; + justify-content: flex-end; +`; + +const Block = styled.div` + display: flex; + justify-content: space-between; + /* width: 50%; */ +`; + +const IssueCnt = styled.span` + padding: 0 2px; + color: ${({ theme }) => theme.grayScale.label}; + font-size: ${({ theme }) => theme.fontSizes.xs}; +`; + +const Span = styled.span` + padding: 0 ${({ theme }) => theme.fontSizes.xl}; + color: ${({ theme }) => theme.grayScale.label}; + font-size: ${({ theme }) => theme.fontSizes.small}; + align-self: center; +`; + +const GrayFont = styled.div` + color: ${({ theme }) => theme.grayScale.label}; + font-size: ${({ theme }) => theme.fontSizes.small}; +`; + +const ProgressWrapper = styled.div` + display: flex; + justify-content: flex-end; + padding-bottom: ${({ theme }) => theme.fontSizes.xs}; + /* margin-left: 30px; */ +`; + +const EditBtn = styled.button` + padding: 0 ${({ theme }) => theme.fontSizes.xl}; + color: ${props => props._color}; + :last-child { + padding-right: 0; + padding-left: ${({ theme }) => theme.fontSizes.xl}; + } + background-color: ${({ theme }) => theme.grayScale.off_white}; + :hover { + background-color: ${({ theme }) => theme.grayScale.background}; + } +`; diff --git a/frontend/src/components/Milestones/MilestoneInput.jsx b/frontend/src/components/Milestones/MilestoneInput.jsx index e69de29bb..b621987c6 100644 --- a/frontend/src/components/Milestones/MilestoneInput.jsx +++ b/frontend/src/components/Milestones/MilestoneInput.jsx @@ -0,0 +1,150 @@ +import styled from "styled-components"; +import { useState } from "react"; +import AddButton from "components/common/Button/BlueButtons"; +import API from "util/API"; +import { useSetRecoilState, useRecoilState } from "recoil"; +import { + milestoneAddButtonFlagState, + milestoneUpdateState, +} from "RecoilStore/Atoms"; +import fetchData from "util/fetchData"; + +const MilestoneInput = ({ + title, + dueDate, + description, + id, + editMode, + setEditMode, +}) => { + const [inputData, setInputData] = useState({ + title: title, + dueDate: dueDate, + description: description, + }); + const setMilestoneAddButtonFlag = useSetRecoilState( + milestoneAddButtonFlagState + ); + const [update, forceUpdate] = useRecoilState(milestoneUpdateState); + + const [titleInput, setTitleInput] = useState(title); + const [dateInput, setDateInput] = useState(dueDate); + const [descInput, setDescInput] = useState(description); + + const putMilestone = async () => { + await fetchData(API.milestonesId(id), "PUT", inputData); + setMilestoneAddButtonFlag(false); + setEditMode(false); + forceUpdate(); + }; + + const postMilestone = async () => { + await fetchData(API.milestones(), "POST", inputData); + setMilestoneAddButtonFlag(false); + }; + + const handleTitleChange = (e) => { + setTitleInput(e.target.value); + setInputData({ + ...inputData, + title: e.target.value, + }); + }; + + const handleDateChange = (e) => { + setDateInput(e.target.value); + setInputData({ + ...inputData, + dueDate: e.target.value, + }); + }; + + const handleDescChange = (e) => { + setDescInput(e.target.value); + setInputData({ + ...inputData, + description: e.target.value, + }); + }; + + console.log(editMode); + + return ( + +
{editMode ? "마일스톤 편집" : "새로운 마일스톤 추가"}
+ + + + + + + + + + + + +
+ ); +}; + +export default MilestoneInput; + +const Header = styled.div` + font-size: ${({ theme }) => theme.fontSizes.xxxl}; +`; + +const CardWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: space-evenly; + height: 288px; + background-color: ${({ theme }) => theme.grayScale.off_white}; + padding: 1%; + border: 1px solid ${({ theme }) => theme.grayScale.line}; + border-radius: ${({ theme }) => theme.border_radius.lg}; +`; + +const BtnWrapper = styled.div` + display: flex; + justify-content: flex-end; +`; + +const InputWrapper = styled.div` + display: flex; + justify-content: space-between; +`; + +const InputHalf = styled.div` + width: 49%; +`; + +const Input = styled.input` + width: 100%; + height: 40px; + background-color: ${({ theme }) => theme.grayScale.input_background}; + border: none; + border-radius: ${({ theme }) => theme.border_radius.base}; + font-size: ${({ theme }) => theme.fontSizes.base}; + padding: 0 ${({ theme }) => theme.paddings.small}; +`; diff --git a/frontend/src/components/Milestones/Milestones.jsx b/frontend/src/components/Milestones/Milestones.jsx index 33a85980c..531d7614c 100644 --- a/frontend/src/components/Milestones/Milestones.jsx +++ b/frontend/src/components/Milestones/Milestones.jsx @@ -1,5 +1,50 @@ +import { useState, useEffect } from "react"; +import MilestonesHeader from "./MilestonesHeader"; +import MilestoneCard from "./MilestoneCard"; +import MilestoneInput from "./MilestoneInput"; +import { + milestoneAddButtonFlagState, + milestoneUpdateState, + milestoneCountState, +} from "RecoilStore/Atoms"; +import { useRecoilValue, useRecoilState } from "recoil"; +import API from "util/API"; +import fetchData from "util/fetchData"; + const Milestones = () => { - return
마일스톤
; + const [milestoneAddBtn, setMilestoneAddBtn] = useRecoilState( + milestoneAddButtonFlagState + ); + const [milestone, setMilestone] = useState(); + const [milestoneCount, setMilestoneCount] = + useRecoilState(milestoneCountState); + + const fetchMilestones = async () => { + const { milestones } = await fetchData(API.milestones(), "GET"); + setMilestone(milestones); + setMilestoneCount(milestones.length); + }; + + useEffect(() => { + fetchMilestones(); + }, [milestoneAddBtn]); + + return ( + <> + {milestoneAddBtn ? ( + <> + + + + ) : ( + <> + )} + {milestone && + milestone.map((milestone, i) => ( + + ))} + + ); }; export default Milestones; diff --git a/frontend/src/components/Milestones/MilestonesHeader.jsx b/frontend/src/components/Milestones/MilestonesHeader.jsx new file mode 100644 index 000000000..d8f2234cd --- /dev/null +++ b/frontend/src/components/Milestones/MilestonesHeader.jsx @@ -0,0 +1,42 @@ +import { useState } from "react"; +import styled from "styled-components"; +import { StyledGridTitleCard } from "styles/StyledCards"; +import { ReactComponent as MilestoneIcon } from "images/milestone.svg"; +import { ReactComponent as ArchiveIcon } from "images/archive.svg"; +import theme from "styles/theme"; + +const MilestonesHeader = () => { + const [showOpenedOnes, setShowOpendOnes] = useState(true); + + const getColor = () => { + return showOpenedOnes ? "#000000" : `${theme.grayScale.label}`; + }; + + return ( + + + + <MilestoneIcon fill={getColor()} /> + 열린 마일스톤(N) + + + <ArchiveIcon /> + 닫힌 마일스톤(N) + + + + ); +}; + +export default MilestonesHeader; + +const Contents = styled.div` + display: flex; + justify-content: space-between; + width: 30%; + align-items: center; +`; + +const Title = styled.div` + padding: ${({ theme }) => `0 ${theme.paddings.xxxl}`}; +`; diff --git a/frontend/src/components/NewIssues/NewIssueForm.jsx b/frontend/src/components/NewIssues/NewIssueForm.jsx index e69de29bb..759a09f22 100644 --- a/frontend/src/components/NewIssues/NewIssueForm.jsx +++ b/frontend/src/components/NewIssues/NewIssueForm.jsx @@ -0,0 +1,96 @@ +import { Link } from "react-router-dom"; +import styled from "styled-components"; +import IssueCategoryList from "components/common/IssueCategoryList"; +import CommentInput from "components/common/CommentInput"; +import SubmitBtn from "components/common/Button/BlueButtons"; +import { ReactComponent as Xsquare } from "images/x-square.svg"; +import getUserInfo from "util/getUserInfo"; + +const NewIssueForm = () => { + const userInfo = getUserInfo(); + const handleSubmit = () => { + console.log("작성완료"); + }; + + return ( + <> + + + {userInfo.gitHubId} + + + + + + + + + + + + + + + 작성 취소 + + + + + + + ); +}; + +export default NewIssueForm; + +const Wrapper = styled.div` + display: grid; + grid-template-columns: 0.15fr 3fr 1fr; + padding: ${({ theme }) => `${theme.fontSizes.xxxl} 0`}; + border-bottom: 1px solid ${({ theme }) => theme.grayScale.line}; +`; + +const ImgWrapper = styled.div` + width: 44px; + height: 44px; + border: 1px solid ${({ theme }) => theme.grayScale.line}; + border-radius: 50%; + overflow: hidden; + img { + width: 100%; + height: 100%; + } +`; + +const ContentsWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; +`; + +const ButtonWrapper = styled.div` + display: flex; + justify-content: flex-end; + padding: ${({ theme }) => `${theme.fontSizes.titleSize} 0`}; +`; + +const InputTitleWrapper = styled.div` + padding: 1%; + margin-bottom: ${({ theme }) => theme.paddings.xxl}; + border-radius: 14px; + background-color: ${({ theme }) => theme.grayScale.input_background}; +`; +const Input = styled.input` + border: none; + width: 100%; + background-color: ${({ theme }) => theme.grayScale.input_background}; +`; + +const CancelBtn = styled.div` + /* display: flex; + align-items: center; + justify-content: space-between; */ + align-self: center; + margin-right: 1rem; +`; diff --git a/frontend/src/components/NewIssues/NewIssueInput.jsx b/frontend/src/components/NewIssues/NewIssueInput.jsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/components/common/AddButton.jsx b/frontend/src/components/common/AddButton.jsx deleted file mode 100644 index 2cf1e1e8a..000000000 --- a/frontend/src/components/common/AddButton.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import { AddBtn } from "styles/StyledButtons"; -import { ReactComponent as PlusIcon } from "images/plus.svg"; -import styled from "styled-components"; -import theme from "styles/theme"; -const AddButton = ({ text, clickEvent }) => { - return ( - <> - - - {text} - - - ); -}; -const ButtonText = styled.div` - padding: 0 4%; -`; - -export default AddButton; diff --git a/frontend/src/components/common/Button/BlueButtons.jsx b/frontend/src/components/common/Button/BlueButtons.jsx new file mode 100644 index 000000000..308ce51f1 --- /dev/null +++ b/frontend/src/components/common/Button/BlueButtons.jsx @@ -0,0 +1,63 @@ +import Button from "@material-ui/core/Button"; +import { ReactComponent as EditIcon } from "images/edit.svg"; +import { ReactComponent as PlusIcon } from "images/plus.svg"; +import styled from "styled-components"; +import theme from "styles/theme"; + +const BlueButtons = ({ text, icon, size, clickHandler }) => { + const getIcon = () => { + switch (icon) { + case "plus": { + return ; + } + case "edit": { + return ; + } + case "none": { + return <>; + } + default: { + console.error("unhandled icon Type🤦‍♀️"); + } + } + }; + + const getWidth = () => { + switch (size) { + case "m": { + return theme.buttonWidths.baseS; + } + case "l": { + return theme.buttonWidths.lg; + } + default: { + console.error("unhandled width Type🤦‍♀️"); + } + } + }; + + return ( + + {getIcon()} + {text} + + ); +}; + +const BlueButton = styled(Button)` + width: ${props => props._width}; + height: ${({ theme }) => theme.buttonHeights.base}; + font-size: ${({ theme }) => theme.fontSizes.xs}; + font-weight: bold; + color: ${({ theme }) => theme.grayScale.off_white}; + background-color: ${({ theme }) => theme.colors.blue}; + border-radius: ${({ theme }) => theme.border_radius.base}; + cursor: pointer; + &:hover { + background-color: ${({ theme }) => theme.colors.dark_blue}; + } +`; +const ButtonText = styled.div` + padding: 0 4%; +`; +export default BlueButtons; diff --git a/frontend/src/components/common/Button/ButtonGroup.jsx b/frontend/src/components/common/Button/ButtonGroup.jsx new file mode 100644 index 000000000..b229bf9b1 --- /dev/null +++ b/frontend/src/components/common/Button/ButtonGroup.jsx @@ -0,0 +1,63 @@ +import { TabButton } from "styles/StyledButtons"; +import styled from "styled-components"; +import { ReactComponent as LabelIcon } from "images/tag.svg"; +import { ReactComponent as MileStoneIcon } from "images/milestone.svg"; +import theme from "styles/theme"; +import { Link } from "react-router-dom"; +import { useRecoilValue } from "recoil"; +import { + labelButtonFlagState, + milestoneButtonFlagState, +} from "RecoilStore/Atoms"; + +const ButtonGroup = ({ + milestoneCount, + milestoneClickEvent, + labelCount, + labelClickEvent, + isMainPage, +}) => { + const milestoneFlag = useRecoilValue(milestoneButtonFlagState); + const labelFlag = useRecoilValue(labelButtonFlagState); + + return ( + + + theme.buttonWidths.base} + _radius={"left"} + bgColor={labelFlag} + isMainPage={isMainPage} + > + + 레이블 ({labelCount}) + + + + theme.buttonWidths.base} + _radius={"right"} + bgColor={milestoneFlag} + isMainPage={isMainPage} + > + + 마일스톤 ({milestoneCount}) + + + + ); +}; + +const ButtonGroupLayout = styled.div` + display: flex; + justify-content: center; + margin-bottom: 24px; +`; + +const ButtonText = styled.div` + padding: 0 4%; +`; + +export default ButtonGroup; diff --git a/frontend/src/components/common/DropDownButton.jsx b/frontend/src/components/common/Button/DropDownButton.jsx similarity index 65% rename from frontend/src/components/common/DropDownButton.jsx rename to frontend/src/components/common/Button/DropDownButton.jsx index 38b79721d..95b9798de 100644 --- a/frontend/src/components/common/DropDownButton.jsx +++ b/frontend/src/components/common/Button/DropDownButton.jsx @@ -2,10 +2,15 @@ import styled from "styled-components"; import { TabButton } from "styles/StyledButtons"; import { ReactComponent as ArrowIcon } from "images/arrow_down.svg"; import theme from "styles/theme"; -const DropDownButton = ({ text, clickEvent }) => { +const DropDownButton = ({ text, clickEvent, width, border, radius }) => { return ( <> - + {text} @@ -14,6 +19,6 @@ const DropDownButton = ({ text, clickEvent }) => { }; const ButtonText = styled.div` - padding-right: 60px; + padding-right: 30%; `; export default DropDownButton; diff --git a/frontend/src/components/common/Button/WhiteButtons.jsx b/frontend/src/components/common/Button/WhiteButtons.jsx new file mode 100644 index 000000000..ba3b2f9b7 --- /dev/null +++ b/frontend/src/components/common/Button/WhiteButtons.jsx @@ -0,0 +1,70 @@ +import Button from "@material-ui/core/Button"; +import { ReactComponent as XIcon } from "images/x-square.svg"; +import { ReactComponent as EditIcon } from "images/edit.svg"; +import { ReactComponent as PaperIcon } from "images/paperclip.svg"; +import { ReactComponent as Archive } from "images/archive.svg"; +import styled from "styled-components"; +import theme from "styles/theme"; + +const WhiteButtons = ({ text, icon, size, clickHandler }) => { + const getIcon = () => { + switch (icon) { + case "edit": { + return ; + } + case "cancel": { + return ; + } + case "paper": { + return ; + } + case "archive": { + return ; + } + case "none": { + return <>; + } + default: { + console.error("unhandled icon Type🤦‍♀️"); + } + } + }; + + const getWidth = () => { + switch (size) { + case "m": { + return theme.buttonWidths.baseS; + } + case "l": { + return theme.buttonWidths.lg; + } + default: { + console.error("unhandled width Type🤦‍♀️"); + } + } + }; + + return ( + + {getIcon()} + {text} + + ); +}; + +const WhiteButton = styled(Button)` + width: ${props => props._width}; + height: ${({ theme }) => theme.buttonHeights.base}; + font-size: ${({ theme }) => theme.fontSizes.xs}; + font-weight: bold; + color: ${({ theme }) => theme.colors.blue}; + background: ${({ theme }) => theme.colors.off_white}; + border: 2px solid ${({ theme }) => theme.colors.blue}; + border-radius: 11px; + cursor: pointer; +`; +const ButtonText = styled.div` + padding: 0 4%; +`; + +export default WhiteButtons; diff --git a/frontend/src/components/common/ButtonGroup.jsx b/frontend/src/components/common/ButtonGroup.jsx deleted file mode 100644 index 65e8059ff..000000000 --- a/frontend/src/components/common/ButtonGroup.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import { ButtonGroupLeftBtn, ButtonGroupRightBtn } from "styles/StyledButtons"; -import styled from "styled-components"; -import { ReactComponent as LabelIcon } from "images/tag.svg"; -import { ReactComponent as MileStoneIcon } from "images/milestone.svg"; -import theme from "styles/theme"; -const ButtonGroup = ({ - milestoneCount, - milestoneClickEvent, - labelCount, - labelClickEvent, -}) => { - return ( - <> - - - 마일스톤 ({milestoneCount}) - - - - 레이블 ({labelCount}) - - - ); -}; - -const ButtonText = styled.div` - padding: 0 4%; -`; - -export default ButtonGroup; diff --git a/frontend/src/components/common/CommentInput.jsx b/frontend/src/components/common/CommentInput.jsx index e69de29bb..e4092d0f3 100644 --- a/frontend/src/components/common/CommentInput.jsx +++ b/frontend/src/components/common/CommentInput.jsx @@ -0,0 +1,34 @@ +import styled from "styled-components"; +import { ReactComponent as Clip } from "images/paperclip.svg"; + +const CommentInput = () => { + return ( + + +