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 (
+
+
+
+
+ 열린 마일스톤(N)
+
+
+
+ 닫힌 마일스톤(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 (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 작성 취소
+
+
+
+
+
+ >
+ );
+};
+
+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 (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default CommentInput;
+
+const Wrapper = styled.div`
+ padding: 1%;
+ background-color: ${({ theme }) => theme.grayScale.input_background};
+ border-radius: 16px;
+`;
+
+const TextAreaWrapper = styled.div``;
+
+const TextArea = styled.textarea`
+ border: none;
+ border-bottom: 1px dashed ${({ theme }) => theme.grayScale.line};
+ background-color: ${({ theme }) => theme.grayScale.input_background};
+ resize: vertical;
+ width: 100%;
+`;
diff --git a/frontend/src/components/common/FilterModal.jsx b/frontend/src/components/common/FilterModal.jsx
index 980bfd3fb..62bfcd796 100644
--- a/frontend/src/components/common/FilterModal.jsx
+++ b/frontend/src/components/common/FilterModal.jsx
@@ -1,55 +1,116 @@
+import { useState } from "react";
+import { filterBarInputState, clickedFilterState } from "RecoilStore/Atoms";
+import { useRecoilValue, useRecoilState } from "recoil";
+import styled from "styled-components";
+
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import FormControl from "@material-ui/core/FormControl";
import FormLabel from "@material-ui/core/FormLabel";
-import { useState } from "react";
-import styled from "styled-components";
-const data = [
- "열린 이슈",
- "내가 작성한 이슈",
- "나에게 할당한 이슈",
- "나에게 할당된 이슈",
- "내가 댓글을 남긴 이슈",
- "닫힌 이슈",
-];
-//디자인 수정 필요
+import { filterData } from "data";
+
+import getEngKey from "util/getEngKey";
+
const FilterModal = () => {
- const [value, setValue] = useState("female");
+ const [clickedFilter, setClickedFilterState] = useState("");
+ const filterType = useRecoilValue(clickedFilterState);
+ const [filterBarInput, setFilterBarInputState] = useRecoilState(
+ filterBarInputState
+ );
+ const key = getEngKey(filterType);
const handleChange = event => {
- setValue(event.target.value);
- console.log(event.target.value);
+ setClickedFilterState(event.target.value);
+ setFilterStateByType(event.target.value);
};
+ const setFilterStateByType = clickedValue => {
+ const updatedValue =
+ clickedValue === filterBarInput[getEngKey(filterType)]
+ ? null
+ : clickedValue;
+
+ switch (filterType) {
+ case "담당자": {
+ setFilterBarInputState({
+ ...filterBarInput,
+ assignee: updatedValue,
+ });
+ break;
+ }
+ case "레이블": {
+ setFilterBarInputState({
+ ...filterBarInput,
+ label: updatedValue,
+ });
+ break;
+ }
+ case "마일스톤": {
+ setFilterBarInputState({
+ ...filterBarInput,
+ milestone: updatedValue,
+ });
+ break;
+ }
+ case "작성자": {
+ setFilterBarInputState({
+ ...filterBarInput,
+ author: updatedValue,
+ });
+ break;
+ }
+ case "필터": {
+ setFilterBarInputState({
+ ...filterBarInput,
+ issue: updatedValue,
+ });
+ break;
+ }
+ default: {
+ console.error("setFilterStateByType unhandled type");
+ }
+ }
+ };
+
+ const filterDataByType = filterData[getEngKey(filterType)];
+
return (
- 이슈 필터
-
+ {filterType === "필터" ? "" : filterType} 필터
+
+
- {data.map(x => (
- }
- label={x}
- labelPlacement="start"
- />
- ))}
-
+ {filterDataByType &&
+ filterDataByType.map((text, idx) => (
+ }
+ label={text}
+ labelPlacement="start"
+ key={`filter-control-label-${idx}`}
+ checked={filterBarInput[key] === text}
+ />
+ ))}
+
);
};
+
const FilterModalLayout = styled.div`
position: absolute;
- top: 100%;
+ top: 45px;
background-color: white;
text-align: left;
+ border-radius: 16px;
+ border: 1px solid ${({ theme }) => theme.grayScale.line};
`;
const FilterControlLabel = styled(FormControlLabel)`
@@ -58,4 +119,19 @@ const FilterControlLabel = styled(FormControlLabel)`
margin: 0;
`;
+const FilterRadioContainer = styled(RadioGroup)`
+ padding: 8px 16px;
+`;
+
+const FilterTitle = styled(FormLabel)`
+ background-color: ${({ theme }) => theme.grayScale.background};
+ width: 100%;
+ border-bottom: 1px solid ${({ theme }) => theme.grayScale.line};
+ border-radius: 16px 16px 0px 0px;
+ padding: 8px 16px;
+ font-size: 18px;
+ color: ${({ theme }) => theme.grayScale.title_active};
+ line-height: 32px;
+`;
+
export default FilterModal;
diff --git a/frontend/src/components/common/Header.jsx b/frontend/src/components/common/Header.jsx
index 128664f79..f8297ab64 100644
--- a/frontend/src/components/common/Header.jsx
+++ b/frontend/src/components/common/Header.jsx
@@ -1,15 +1,19 @@
import styled from "styled-components";
import { ReactComponent as Logo } from "images/LogotypeMedium.svg";
-import { ReactComponent as UserImg } from "images/UserImageLarge.svg";
import { Link } from "react-router-dom";
+import getUserInfo from "util/getUserInfo";
const Header = () => {
+ const userInfo = getUserInfo();
+
return (
-
+
+
+
);
};
@@ -20,4 +24,17 @@ const StyleHeader = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
+ padding: 27px 0px;
+`;
+
+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%;
+ }
`;
diff --git a/frontend/src/components/common/IssueCategoryList.jsx b/frontend/src/components/common/IssueCategoryList.jsx
index e69de29bb..596321373 100644
--- a/frontend/src/components/common/IssueCategoryList.jsx
+++ b/frontend/src/components/common/IssueCategoryList.jsx
@@ -0,0 +1,5 @@
+const IssueCategoryList = () => {
+ return 이슈 카테고리 리스트
;
+};
+
+export default IssueCategoryList;
diff --git a/frontend/src/components/common/LabelBadge.jsx b/frontend/src/components/common/LabelBadge.jsx
new file mode 100644
index 000000000..29856493e
--- /dev/null
+++ b/frontend/src/components/common/LabelBadge.jsx
@@ -0,0 +1,23 @@
+import styled from "styled-components";
+
+const LabelBadge = ({ text, fontColor, backgroundColor }) => {
+ return (
+
+ {text}
+
+ );
+};
+
+const LabelBadgeLayout = styled.div`
+ position: absolute;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: ${({ theme }) => theme.buttonHeights.small};
+ padding: 0px 16px;
+ border-radius: 30px;
+ background: ${props => props.backgroundColor};
+ color: ${props => props.fontColor};
+`;
+
+export default LabelBadge;
diff --git a/frontend/src/components/common/Navigator.jsx b/frontend/src/components/common/Navigator.jsx
index 83e411001..7eb187974 100644
--- a/frontend/src/components/common/Navigator.jsx
+++ b/frontend/src/components/common/Navigator.jsx
@@ -1,5 +1,89 @@
+import styled from "styled-components";
+import ButtonGroup from "./Button/ButtonGroup";
+import AddButton from "./Button/BlueButtons";
+import CancelButton from "components/common/Button/WhiteButtons";
+import { useRecoilState, useRecoilValue } from "recoil";
+import {
+ labelButtonFlagState,
+ milestoneButtonFlagState,
+ milestoneAddButtonFlagState,
+ labelAddButtonFlagState,
+ navigatorAddButtonFlagState,
+} from "RecoilStore/Atoms";
+import { useState } from "react";
+import { milestoneCountState } from "RecoilStore/Atoms";
+
const Navigator = () => {
- return 네비게이터
;
+ // const [isAddButton, setIsAddButton] = useState(true);
+ const [addButtonFlag, setAddButtonFlag] = useRecoilState(
+ navigatorAddButtonFlagState
+ );
+ const [milestoneFlag, setMilestoneFlag] = useRecoilState(
+ milestoneButtonFlagState
+ );
+ const [labelFlag, setLabelFlag] = useRecoilState(labelButtonFlagState);
+ const [milestoneAddBtnFlag, setMilestoneAddBtnFlag] = useRecoilState(
+ milestoneAddButtonFlagState
+ );
+ const [labelAddBtnFlag, setLabelAddBtnFlag] = useRecoilState(
+ labelAddButtonFlagState
+ );
+ //밑에 하나로 묶어도 될 것 같음
+
+ const milestoneCountValue = useRecoilValue(milestoneCountState);
+
+
+ const handleMilestoneClick = () => {
+ setMilestoneFlag(true);
+ setLabelFlag(false);
+ };
+ const handleLabelClick = () => {
+ setMilestoneFlag(false);
+ setLabelFlag(true);
+ };
+ //Refactoring
+ //리렌더 2번 일어남
+ const handleClick = () => {
+ milestoneFlag
+ ? setMilestoneAddBtnFlag(!milestoneAddBtnFlag)
+ : setLabelAddBtnFlag(!labelAddBtnFlag);
+ setAddButtonFlag(!addButtonFlag);
+
+ };
+
+ return (
+
+ {/* 버튼 그룹 카운트 자리에 데이터 길이 넣어주기 */}
+
+
+ {addButtonFlag ? (
+
+ ) : (
+
+ )}
+
+ );
};
+const NavigatorLayout = styled.div`
+ display: flex;
+ justify-content: space-between;
+`;
+
export default Navigator;
diff --git a/frontend/src/components/pages/AnotherTest.jsx b/frontend/src/components/pages/AnotherTest.jsx
index e7e4b51bd..64805d1d8 100644
--- a/frontend/src/components/pages/AnotherTest.jsx
+++ b/frontend/src/components/pages/AnotherTest.jsx
@@ -1,8 +1,8 @@
import { useRecoilValue } from "MyRecoil/useRecoilValue";
-import { milestoneAtomState } from "MyRecoil/atom";
+import { milestoneState } from "MyRecoil/atom";
export const AnotherTest = () => {
- const milestoneState = useRecoilValue(milestoneAtomState);
+ const milestoneState = useRecoilValue(milestoneState);
const milestoneFilterList = milestoneState.map((filter, i) => (
{filter}
));
diff --git a/frontend/src/components/pages/IssueDetailPage.jsx b/frontend/src/components/pages/IssueDetailPage.jsx
index 09fc54fc0..44b91c0ec 100644
--- a/frontend/src/components/pages/IssueDetailPage.jsx
+++ b/frontend/src/components/pages/IssueDetailPage.jsx
@@ -1,5 +1,74 @@
+import styled from "styled-components";
+import { IssueHeader } from "styles/StyledLayout ";
+import EditButton from "components/common/Button/WhiteButtons";
+import CloseButton from "components/common/Button/WhiteButtons";
+
const IssueDetailPage = () => {
- return 이슈디테일
;
+ return (
+ <>
+
+
+
+ 타이틀
+ 이슈번호
+
+
+
+ {}}
+ />{" "}
+
+ {}}
+ />
+
+
+
+ 열린 이슈
+ 이 이슈가 n분 전에 n님에 의해 열렸습니다
+ - 코멘트 n개
+
+
+ >
+ );
};
export default IssueDetailPage;
+
+const Titles = styled.div`
+ display: flex;
+
+ .issue_num {
+ color: ${({ theme }) => theme.grayScale.label};
+ padding: 0 ${({ theme }) => theme.paddings.base};
+ ::before {
+ content: "#";
+ }
+ }
+`;
+
+const Top = styled.div`
+ display: grid;
+ width: 100%;
+ /* justify-content: space-between; */
+ grid-template-columns: 1fr 1fr;
+`;
+
+const Buttons = styled.div`
+ display: flex;
+ justify-content: flex-end;
+`;
+
+const Bottom = styled.div`
+ display: flex;
+`;
+
+const ButtonWrapper = styled.div`
+ padding: 0 1rem;
+`;
diff --git a/frontend/src/components/pages/LabelsPage.jsx b/frontend/src/components/pages/LabelsPage.jsx
index 3f1c40b1f..e9b642e7d 100644
--- a/frontend/src/components/pages/LabelsPage.jsx
+++ b/frontend/src/components/pages/LabelsPage.jsx
@@ -1,5 +1,11 @@
+import Labels from "components/Labels/Labels";
+
const LabelsPage = () => {
- return ;
+ return (
+ <>
+
+ >
+ );
};
export default LabelsPage;
diff --git a/frontend/src/components/pages/LoginLoadingPage.jsx b/frontend/src/components/pages/LoginLoadingPage.jsx
index 8afdd6244..2911d2c8c 100644
--- a/frontend/src/components/pages/LoginLoadingPage.jsx
+++ b/frontend/src/components/pages/LoginLoadingPage.jsx
@@ -7,18 +7,19 @@ const LoginLoadingPage = () => {
const getToken = async () => {
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
- console.log(code);
const res = await fetch(API.login(code));
const json = await res.json();
const { accessToken, tokenType } = json;
- localStorage.setItem("accessToken", accessToken);
- localStorage.setItem("tokeType", tokenType);
- setIsLogin(true);
+ accessToken && localStorage.setItem("accessToken", accessToken);
+ tokenType && localStorage.setItem("tokeType", tokenType);
+ if (res.ok) setIsLogin(true);
+ if (!res.ok) setIsLogin(false);
};
useEffect(() => {
getToken();
}, []);
- return <>${isLogin ? : 로딩
}>;
+
+ return <>{isLogin ? : <>Loading..>}>;
};
export default LoginLoadingPage;
diff --git a/frontend/src/components/pages/MainPage.jsx b/frontend/src/components/pages/MainPage.jsx
index 6900eea12..6e05f7ec8 100644
--- a/frontend/src/components/pages/MainPage.jsx
+++ b/frontend/src/components/pages/MainPage.jsx
@@ -1,48 +1,38 @@
-import { useState } from "react";
-import { Route, Switch, Link, Redirect } from "react-router-dom";
+import { Route, Switch, Redirect } from "react-router-dom";
import styled from "styled-components";
import NewIssue from "./NewIssuePage";
import NoMatch from "./NoMatchPage";
import IssueDetailPage from "./IssueDetailPage";
-import Labels from "components/Labels/Labels";
-import Milestones from "components/Milestones/Milestones";
+import LabelsPage from "./LabelsPage";
+import MilestonesPage from "./MilestonesPage";
import Header from "components/common/Header";
import Navigator from "components/common/Navigator";
import Issues from "components/Issues/Issues";
-import { Test } from "components/pages/Test";
-import { AnotherTest } from "./AnotherTest";
const MainPage = () => {
const { pathname } = window.location;
-
return localStorage.getItem("accessToken") ? (
-
{(pathname === "/main/labels" || pathname === "/main/milestones") && (
)}
{pathname === "/main" && }
-
-
-
+
+
-
) : (
-
+
);
};
const MainPageLayout = styled.div`
- /* display: flex;
- justify-content: center;
- align-items: center;
- padding: 0 80px; */
+ padding: 0 5%;
`;
export default MainPage;
diff --git a/frontend/src/components/pages/MilestonesPage.jsx b/frontend/src/components/pages/MilestonesPage.jsx
index 9b367f208..a1834fc0e 100644
--- a/frontend/src/components/pages/MilestonesPage.jsx
+++ b/frontend/src/components/pages/MilestonesPage.jsx
@@ -1,5 +1,8 @@
+import { useState, useEffect } from "react";
+import Milestones from "components/Milestones/Milestones";
+
const MilestonesPage = () => {
- return ;
+ return <>{}>;
};
export default MilestonesPage;
diff --git a/frontend/src/components/pages/NewIssuePage.jsx b/frontend/src/components/pages/NewIssuePage.jsx
index fe36492fe..15f2d3c04 100644
--- a/frontend/src/components/pages/NewIssuePage.jsx
+++ b/frontend/src/components/pages/NewIssuePage.jsx
@@ -1,5 +1,14 @@
+import styled from "styled-components";
+import NewIssueForm from "components/NewIssues/NewIssueForm";
+import { IssueHeader } from "styles/StyledLayout ";
+
function NewIssue() {
- return 새로운 이슈 글
;
+ return (
+ <>
+ 새로운 이슈 작성
+
+ >
+ );
}
export default NewIssue;
diff --git a/frontend/src/components/pages/Test.jsx b/frontend/src/components/pages/Test.jsx
index 0222d8adf..b9a7da2d6 100644
--- a/frontend/src/components/pages/Test.jsx
+++ b/frontend/src/components/pages/Test.jsx
@@ -1,23 +1,22 @@
import { useState } from "react";
import { useRecoilState } from "MyRecoil/useRecoilState";
-import { labelAtomState } from "MyRecoil/atom";
+import { labelState } from "MyRecoil/atom";
import { AnotherTest } from "./AnotherTest";
export const Test = () => {
const [labelFilterInput, setLabelFilterInput] = useState();
- const [labelFilterState, setLabelFilterState] =
- useRecoilState(labelAtomState);
+ const [labelFilterState, setLabelFilterState] = useRecoilState(labelState);
const labelFilterList = labelFilterState.map((filter, i) => (
{filter}
));
- const setLabelFilterInputVal = e => {
+ const setLabelFilterInputVal = (e) => {
setLabelFilterInput(e.target.value);
};
const setLabelFilter = () => {
- setLabelFilterState(labelFilterState => [
+ setLabelFilterState((labelFilterState) => [
...labelFilterState,
labelFilterInput,
]);
diff --git a/frontend/src/data.js b/frontend/src/data.js
index 61f8e22e8..5d6eb895d 100644
--- a/frontend/src/data.js
+++ b/frontend/src/data.js
@@ -5,7 +5,7 @@ export const issues = [
labelId: 2,
milestoneId: 4,
author: "goody",
- createdAt: "2021-06-10",
+ createdAt: "2021-06-10T23:50:14",
isOpen: true,
},
{
@@ -14,7 +14,39 @@ export const issues = [
labelId: 2,
milestoneId: 4,
author: "daisy",
- createdAt: "2021-06-11",
+ createdAt: "2021-06-14T15:00:00",
isOpen: false,
},
];
+
+export const filterData = {
+ issue: [
+ "열린 이슈",
+ "내가 작성한 이슈",
+ "나에게 할당된 이슈",
+ "내가 댓글을 남긴 이슈",
+ "닫힌 이슈",
+ ],
+ assignee: ["daisy", "goody"],
+ label: ["bug", "document"],
+ milestone: ["마일스톤이 없는 필터", "마스터즈 코스"],
+ author: ["daisy", "goody"],
+ openClose: ["선택된 이슈 열기", "선택된 이슈 닫기"],
+};
+
+export const labelData = {
+ creatorTitle: "새로운 레이블 추가",
+ editorTitle: "레이블 편집",
+ nameTitle: "레이블 이름",
+ descriptionTitle: "설명(선택)",
+ backgroundColorTitle: "배경 색상",
+ textColorTitles: "텍스트 색상",
+ buttons: {
+ cancel: "취소",
+ submit: "완료",
+ radio: [
+ { value: "#FEFEFE", text: "밝은 색" },
+ { value: "#000000", text: "어두운 색" },
+ ],
+ },
+};
diff --git a/frontend/src/hooks/useDebounce.jsx b/frontend/src/hooks/useDebounce.jsx
new file mode 100644
index 000000000..89542fdd6
--- /dev/null
+++ b/frontend/src/hooks/useDebounce.jsx
@@ -0,0 +1,14 @@
+import { useState, useEffect } from "react";
+const useDebounce = (value, timeout) => {
+ const [state, setState] = useState(value);
+
+ useEffect(() => {
+ const handler = setTimeout(() => setState(value), timeout);
+
+ return () => clearTimeout(handler);
+ }, [value, timeout]);
+
+ return state;
+};
+
+export default useDebounce;
diff --git a/frontend/src/hooks/useFetch.jsx b/frontend/src/hooks/useFetch.jsx
new file mode 100644
index 000000000..318e79cf8
--- /dev/null
+++ b/frontend/src/hooks/useFetch.jsx
@@ -0,0 +1,53 @@
+import { useState, useEffect } from "react";
+
+import fetchData from "util/fetchData";
+
+const useFetch = (url, method, callback, reqData = null) => {
+ const [status, setStatus] = useState("대기 중");
+ const [error, setError] = useState(null);
+ console.log(method, "initiated");
+
+ const option =
+ method === "GET"
+ ? {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
+ },
+ }
+ : {
+ method: method,
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${localStorage.getItem("accessToken")}`,
+ },
+ body: JSON.stringify(reqData),
+ };
+
+ const fetchData = async () => {
+ console.log("fetchData func initiated");
+ try {
+ setStatus(`fetch start from ${url}`);
+ const res = await fetch(url, option);
+ const resData = await res.json();
+ if (!res.ok) throw new Error(res.status);
+ else {
+ console.log("in useFetch:", resData);
+ callback(resData);
+ setStatus(`fetch complete from ${url}`);
+ return { resData, status };
+ }
+ } catch (error) {
+ setStatus(`fetch failed from ${url} due to ${error}`);
+ setError(error);
+ }
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, [url]);
+
+ return { status, fetchData };
+};
+
+export default useFetch;
diff --git a/frontend/src/images/archive.svg b/frontend/src/images/archive.svg
index 0f27a4d95..09b3f9d4f 100644
--- a/frontend/src/images/archive.svg
+++ b/frontend/src/images/archive.svg
@@ -1,8 +1,8 @@