diff --git a/.github/ISSUE_TEMPLATE/Bug-Report.yml b/.github/ISSUE_TEMPLATE/Bug-Report.yml index 779477d..382a38e 100644 --- a/.github/ISSUE_TEMPLATE/Bug-Report.yml +++ b/.github/ISSUE_TEMPLATE/Bug-Report.yml @@ -3,6 +3,17 @@ description: 기능 개선을 위해 버그를 제보해주세요. title: "fix: " labels: ["bug"] body: +- type: dropdown + attributes: + label: 심각성 + description: 해당 버그가 서비스에 미치는 영향의 심각성을 평가해 주세요. + multiple: false + options: + - A + - B + - C + validations: + required: true - type: textarea attributes: label: 버그 설명 diff --git a/.github/ISSUE_TEMPLATE/Feature-Request.yml b/.github/ISSUE_TEMPLATE/Feature-Request.yml new file mode 100644 index 0000000..c52b8ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature-Request.yml @@ -0,0 +1,63 @@ +name: 피처 요청 템플릿 +description: 서비스에 필요한 새로운 피처 명세를 작성하고 개발자에게 요청하세요. +title: "request: " +labels: ["request"] +body: +- type: input + attributes: + label: 피처 개요 + description: 필요한 피처에 대해서 한줄로 설명해주세요. + validations: + required: true +- type: textarea + attributes: + label: 요청 사유 + description: 해당 피처가 필요한 이유, 필요한 상황에 대해서 기술해주세요. + validations: + required: true +- type: dropdown + attributes: + label: 중요도 + description: 해당 피처의 중요도를 선택해주세요. + multiple: false + options: + - A + - B + - C + validations: + required: true +- type: dropdown + attributes: + label: 난이도 + description: 해당 피처의 구현 난이도(구현 시간도 고려하여)을 선택해주세요. + multiple: false + options: + - 상 + - 중 + - 하 + validations: + required: true +- type: textarea + attributes: + label: 피처 설명 + description: 필요한 피처에 대해서 자세히 설명해주세요. + validations: + required: false +- type: textarea + attributes: + label: 체크리스트 + description: 작업에 필요한 마이크로 피처들을 작성해 이슈를 링크해주세요. + validations: + required: false +- type: input + attributes: + label: 관련 정보 + description: 관련된 Project Wiki 페이지가 있다면 링크해주세요. + validations: + required: false +- type: checkboxes + attributes: + label: 담당자 지정 + options: + - label: assignee에 담당자를 제대로 지정하였나요? + required: true diff --git a/.github/ISSUE_TEMPLATE/Feature.yml b/.github/ISSUE_TEMPLATE/Feature.yml index f4be209..3c48703 100644 --- a/.github/ISSUE_TEMPLATE/Feature.yml +++ b/.github/ISSUE_TEMPLATE/Feature.yml @@ -3,10 +3,32 @@ description: 피처 구현을 위한 이슈를 작성하세요. title: "feat: " labels: ["feature"] body: -- type: textarea +- type: input attributes: label: 피처 개요 - description: 필요한 피처에 대해서 간단히 설명해주세요. + description: 필요한 피처에 대해서 한줄로 설명해주세요. + validations: + required: true +- type: dropdown + attributes: + label: 중요도 + description: 해당 피처의 중요도를 선택해주세요. + multiple: false + options: + - A + - B + - C + validations: + required: true +- type: dropdown + attributes: + label: 난이도 + description: 해당 피처의 구현 난이도(구현 시간도 고려하여)을 선택해주세요. + multiple: false + options: + - 상 + - 중 + - 하 validations: required: true - type: textarea @@ -26,4 +48,4 @@ body: label: 관련 정보 description: 관련된 Project Wiki 페이지가 있다면 링크해주세요. validations: - required: false \ No newline at end of file + required: false diff --git a/.github/pull-request-template.md b/.github/pull-request-template.md index 9aeae00..e4e1bfb 100644 --- a/.github/pull-request-template.md +++ b/.github/pull-request-template.md @@ -1,17 +1,13 @@ - ## 개요 -무엇에 관한 내용인지 + ## 세부 내용 -- A - - description - - screenshot -- B - - 변경 전 - - 변경 후 -- C ## 공유 + - 고민과 질문 + - 해결 과정의 기록은 정리해서 위키에 수록 후 링크 달기 -## relevant issue number + +## 관련 이슈 + - 관련된 이슈 넘버가 있으면 이곳에 기입해주세요 diff --git a/.github/workflows/CLIENT_BUILD.yml b/.github/workflows/CLIENT_BUILD.yml index 8703161..7850337 100644 --- a/.github/workflows/CLIENT_BUILD.yml +++ b/.github/workflows/CLIENT_BUILD.yml @@ -13,6 +13,10 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Runner 타임존 설정 + run: | + sudo timedatectl set-timezone 'Asia/Seoul' + - name: Node 설정 uses: actions/setup-node@v1 with: @@ -30,6 +34,25 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: yarn + - name: 테스트 실행 + working-directory: ./client + run: | + yarn -s test + + - name: 테스트 결과 발행 + uses: n-ryu/test-reporter@v0.0.2 + if: always() + with: + name: JEST Tests + path: client/reports/*.xml + reporter: jest-junit + + - name: Code Coverage 리포트 발행 + if: always() + uses: codecov/codecov-action@v3 + with: + flags: unittests + - name: Client 소스 빌드 working-directory: "./client" run: yarn build @@ -61,7 +84,7 @@ jobs: username: ${{ secrets.RELEASE_USERNAME }} password: ${{ secrets.RELEASE_PASSWORD }} port: ${{ secrets.RELEASE_PORT }} - source: "docker-compose.yml" + source: "docker-compose.production.yml" target: "oao" - name: 운영 서버에서 Docker Compose 실행 @@ -74,11 +97,10 @@ jobs: script: | echo ${{secrets.CONTAINER_REGISTRY_TOKEN}} | docker login ghcr.io -u kumsil1006 --password-stdin docker pull ghcr.io/kumsil1006/oao-client - docker pull ghcr.io/kumsil1006/oao-proxy cd oao - docker-compose up -d + docker-compose -f docker-compose.production.yml up -d docker image prune - name: 실패시 슬랙 메시지 전송 diff --git a/.github/workflows/CLIENT_DEV_BUILD.yml b/.github/workflows/CLIENT_DEV_BUILD.yml index c011ae4..fdc3166 100644 --- a/.github/workflows/CLIENT_DEV_BUILD.yml +++ b/.github/workflows/CLIENT_DEV_BUILD.yml @@ -3,7 +3,7 @@ on: push: branches: - main - + paths: - "client/**" @@ -14,6 +14,10 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Runner 타임존 설정 + run: | + sudo timedatectl set-timezone 'Asia/Seoul' + - name: Node 설정 uses: actions/setup-node@v1 with: @@ -31,6 +35,25 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: yarn + - name: 테스트 실행 + working-directory: ./client + run: | + yarn -s test + + - name: 테스트 결과 발행 + uses: n-ryu/test-reporter@v0.0.2 + if: always() + with: + name: JEST Tests + path: client/reports/*.xml + reporter: jest-junit + + - name: Code Coverage 리포트 발행 + if: always() + uses: codecov/codecov-action@v3 + with: + flags: unittests + - name: Client 소스 build working-directory: "./client" run: yarn build @@ -49,7 +72,7 @@ jobs: tags: ghcr.io/kumsil1006/oao-dev-client context: ./client - - name: Docker Compose 파일 운영 서버로 복사 + - name: Docker Compose 파일 개발 서버로 복사 uses: appleboy/scp-action@master with: host: ${{ secrets.ANOTHER_HOST }} @@ -59,7 +82,7 @@ jobs: source: "docker-compose.yml" target: "oao" - - name: 운영 서버에서 Docker Compose 실행 + - name: 개발 서버에서 Docker Compose 실행 uses: appleboy/ssh-action@master with: host: ${{ secrets.ANOTHER_HOST }} @@ -69,7 +92,6 @@ jobs: script: | echo ${{secrets.CONTAINER_REGISTRY_TOKEN}} | docker login ghcr.io -u kumsil1006 --password-stdin docker pull ghcr.io/kumsil1006/oao-dev-client - docker pull ghcr.io/kumsil1006/oao-dev-proxy cd oao diff --git a/.github/workflows/PRODUCTION_RELEASE.yml b/.github/workflows/PRODUCTION_RELEASE.yml index 0f4a9de..7b7695c 100644 --- a/.github/workflows/PRODUCTION_RELEASE.yml +++ b/.github/workflows/PRODUCTION_RELEASE.yml @@ -1,4 +1,4 @@ -name: Release Github Application Version +name: Release Github Application Version Update on: pull_request: branches: diff --git a/.github/workflows/PROXY_BUILD.yml b/.github/workflows/PROXY_BUILD.yml new file mode 100644 index 0000000..f9d34dc --- /dev/null +++ b/.github/workflows/PROXY_BUILD.yml @@ -0,0 +1,68 @@ +name: Proxy Dockerfile Build +on: + pull_request: + branches: + - release + paths: + - "nginx/Dockerfile.production" + +jobs: + proxy-build: + name: proxy-build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: GitHub Container Registry 로그인 + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: kumsil1006 + password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + + - name: Docker images build 및 GitHub Container Registry로 push + uses: docker/build-push-action@v3.2.0 + with: + push: true + tags: ghcr.io/kumsil1006/oao-proxy + context: ./nginx + file: ./nginx/Dockerfile.production + + - name: Docker Compose 파일 운영 서버로 복사 + uses: appleboy/scp-action@master + with: + host: ${{ secrets.RELEASE_HOST }} + username: ${{ secrets.RELEASE_USERNAME }} + password: ${{ secrets.RELEASE_PASSWORD }} + port: ${{ secrets.RELEASE_PORT }} + source: "docker-compose.production.yml" + target: "oao" + + - name: 운영 서버에서 Docker Compose 실행 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.RELEASE_HOST }} + username: ${{ secrets.RELEASE_USERNAME }} + password: ${{ secrets.RELEASE_PASSWORD }} + port: ${{ secrets.RELEASE_PORT }} + script: | + echo ${{secrets.CONTAINER_REGISTRY_TOKEN}} | docker login ghcr.io -u kumsil1006 --password-stdin + docker pull ghcr.io/kumsil1006/oao-proxy + + cd oao + + docker-compose -f docker-compose.production.yml up -d + docker image prune + + - name: 실패시 슬랙 메시지 전송 + if: ${{ failure() }} + uses: ./.github/actions/slack-notify + with: + slack_incoming_url: ${{ secrets.SLACK_INCOMING_URL }} + + - name: 성공시 슬랙 메시지 전송 + if: ${{ success() }} + uses: ./.github/actions/slack-notify + with: + status: success + slack_incoming_url: ${{ secrets.SLACK_INCOMING_URL }} diff --git a/.github/workflows/PROXY_DEV_BUILD.yml b/.github/workflows/PROXY_DEV_BUILD.yml new file mode 100644 index 0000000..df337eb --- /dev/null +++ b/.github/workflows/PROXY_DEV_BUILD.yml @@ -0,0 +1,67 @@ +name: Proxy Dev Dockerfile Build +on: + pull_request: + branches: + - main + paths: + - "nginx/Dockerfile" + +jobs: + proxy-build: + name: proxy-build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: GitHub Container Registry 로그인 + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: kumsil1006 + password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + + - name: Docker images build 및 GitHub Container Registry로 push + uses: docker/build-push-action@v3.2.0 + with: + push: true + tags: ghcr.io/kumsil1006/oao-dev-proxy + context: ./nginx + + - name: Docker Compose 파일 개발 서버로 복사 + uses: appleboy/scp-action@master + with: + host: ${{ secrets.ANOTHER_HOST }} + username: ${{ secrets.ANOTHER_USERNAME }} + password: ${{ secrets.ANOTHER_PASSWORD }} + port: ${{ secrets.ANOTHER_PORT }} + source: "docker-compose.yml" + target: "oao" + + - name: 개발 서버에서 Docker Compose 실행 + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.ANOTHER_HOST }} + username: ${{ secrets.ANOTHER_USERNAME }} + password: ${{ secrets.ANOTHER_PASSWORD }} + port: ${{ secrets.ANOTHER_PORT }} + script: | + echo ${{secrets.CONTAINER_REGISTRY_TOKEN}} | docker login ghcr.io -u kumsil1006 --password-stdin + docker pull ghcr.io/kumsil1006/oao-dev-proxy + + cd oao + + docker-compose up -d + docker image prune + + - name: 실패시 슬랙 메시지 전송 + if: ${{ failure() }} + uses: ./.github/actions/slack-notify + with: + slack_incoming_url: ${{ secrets.SLACK_INCOMING_URL }} + + - name: 성공시 슬랙 메시지 전송 + if: ${{ success() }} + uses: ./.github/actions/slack-notify + with: + status: success + slack_incoming_url: ${{ secrets.SLACK_INCOMING_URL }} diff --git a/.github/workflows/SERVER_BUILD.yml b/.github/workflows/SERVER_BUILD.yml index 0b0f24a..e20a8e6 100644 --- a/.github/workflows/SERVER_BUILD.yml +++ b/.github/workflows/SERVER_BUILD.yml @@ -53,7 +53,7 @@ jobs: username: ${{ secrets.RELEASE_USERNAME }} password: ${{ secrets.RELEASE_PASSWORD }} port: ${{ secrets.RELEASE_PORT }} - source: "docker-compose.yml" + source: "docker-compose.production.yml" target: "oao" - name: 운영 서버에서 Docker Compose 실행 @@ -66,11 +66,10 @@ jobs: script: | echo ${{secrets.CONTAINER_REGISTRY_TOKEN}} | docker login ghcr.io -u kumsil1006 --password-stdin docker pull ghcr.io/kumsil1006/oao-server - docker pull ghcr.io/kumsil1006/oao-proxy cd oao - docker-compose up -d + docker-compose -f docker-compose.production.yml up -d docker image prune - name: 실패시 슬랙 메시지 전송 diff --git a/.github/workflows/SERVER_DEV_BUILD.yml b/.github/workflows/SERVER_DEV_BUILD.yml index 4520002..1a3dfbd 100644 --- a/.github/workflows/SERVER_DEV_BUILD.yml +++ b/.github/workflows/SERVER_DEV_BUILD.yml @@ -40,7 +40,7 @@ jobs: push: true tags: ghcr.io/kumsil1006/oao-dev-server - - name: Docker Compose 파일 운영 서버로 복사 + - name: Docker Compose 파일 개발 서버로 복사 uses: appleboy/scp-action@master with: host: ${{ secrets.ANOTHER_HOST }} @@ -50,7 +50,7 @@ jobs: source: "docker-compose.yml" target: "oao" - - name: 운영 서버에서 Docker Compose 실행 + - name: 개발 서버에서 Docker Compose 실행 uses: appleboy/ssh-action@master with: host: ${{ secrets.ANOTHER_HOST }} @@ -60,7 +60,6 @@ jobs: script: | echo ${{secrets.CONTAINER_REGISTRY_TOKEN}} | docker login ghcr.io -u kumsil1006 --password-stdin docker pull ghcr.io/kumsil1006/oao-dev-server - docker pull ghcr.io/kumsil1006/oao-dev-proxy cd oao diff --git a/.github/workflows/TEST_REPORT.yml b/.github/workflows/TEST_REPORT.yml index 3ad4453..513b180 100644 --- a/.github/workflows/TEST_REPORT.yml +++ b/.github/workflows/TEST_REPORT.yml @@ -1,3 +1,4 @@ +name: test-report on: pull_request: branches: @@ -33,8 +34,8 @@ jobs: yarn -s test - name: 테스트 결과 발행 - uses: n-ryu/test-reporter@v0.0.1 - if: success() || failure() + uses: n-ryu/test-reporter@v0.0.2 + if: always() with: name: JEST Tests # Name of the check run which will be created path: client/reports/*.xml # Path to test results @@ -44,4 +45,4 @@ jobs: if: always() uses: codecov/codecov-action@v3 with: - flags: unittests # opti \ No newline at end of file + flags: unittests # opti diff --git a/client/codecov b/client/codecov deleted file mode 100755 index 116b23b..0000000 Binary files a/client/codecov and /dev/null differ diff --git a/client/default.conf b/client/default.conf index c3f2f6d..802684d 100644 --- a/client/default.conf +++ b/client/default.conf @@ -1,7 +1,7 @@ server { listen 3000; + root /dist; location / { - root /dist; - index index.html index.htm; + try_files $uri $uri/ $uri.html /index.html; } -} +} \ No newline at end of file diff --git a/client/index.html b/client/index.html index b0d477b..a21e4f2 100644 --- a/client/index.html +++ b/client/index.html @@ -3,7 +3,13 @@ + + + OAO + + +
diff --git a/client/src/App.tsx b/client/src/App.tsx index c11f7d0..2cad04c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,36 +1,98 @@ -import { ReactElement } from 'react'; -import { BrowserRouter, Routes, Route } from 'react-router-dom'; -import { Provider } from 'jotai'; +import { ReactElement, Suspense, useState, useEffect } from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import styled from 'styled-components'; import { ToastContainer } from 'react-toastify'; +import { useAtomValue } from 'jotai'; -import Header from './container/Header'; -import Menubar from './container/Menubar'; +import Header from '@container/Header'; +import Menubar from '@container/Menubar'; +import Main from '@page/Main'; import Todos from '@page/Todos'; -import Main from './page/Main'; +import DiagramPage from '@page/DiagramPage'; import OverLay from '@components/OverLay'; +import TodoController from '@container/TodoController'; + +import { TutorialImage } from '@components/tutorial/TutorialImage'; +import { isTutorialAtom } from '@util/GlobalState'; +import { PRIMARY_COLORS } from '@util/Constants'; + +const RowWrapper = styled.div` + position: relative; + width: calc(100%); + display: flex; +`; const Wrapper = styled.div` + position: relative; + width: calc(100% - 64px); + display: flex; + flex-direction: column; +`; + +const TutorialRadialOverlay = styled.div` + position: absolute; width: 100%; + height: 100%; + left: 0px; + top: 0px; + + background: linear-gradient(180deg, ${PRIMARY_COLORS.green}00 73.51%, #93c692 127.82%); + + pointer-events: none; + + p { + position: absolute; + left: 10vh; + bottom: 10vh; + font-size: 80px; + opacity: 0.7; + font-family: 'Roboto'; + color: ${PRIMARY_COLORS.green}; + } `; const App = (): ReactElement => { + const isTutorial = useAtomValue(isTutorialAtom); + const [isOver, setIsOver] = useState(false); + const isShow = isTutorial && !isOver; + + useEffect(() => { + if (!isTutorial) { + setIsOver(false); + } + }, [isTutorial]); + + const isCorrectURL = isTutorial && location.pathname.includes('tutorials'); return ( - + loading App}> - - - -
- - }> - }> - - + + + +
+ + }> + }> + }> + : }> + : }> + : }> + + + + {isShow && } + {isTutorial && ( + + +

튜토리얼 중입니다...

+
+
+ )} + - + ); }; diff --git a/client/src/components/Bubble.tsx b/client/src/components/Bubble.tsx new file mode 100644 index 0000000..f605faf --- /dev/null +++ b/client/src/components/Bubble.tsx @@ -0,0 +1,55 @@ +import { ReactElement, ReactNode } from 'react'; +import styled from 'styled-components'; +import { PRIMARY_COLORS } from '@util/Constants'; + +const { white, gray } = PRIMARY_COLORS; + +const Wrapper = styled.div<{ color?: string; backgroundColor?: string }>` + position: relative; + display: flex; + align-items: center; + justify-content: center; + width: max-content; + height: max-content; + border-radius: 8px; + padding-block: 12px; + padding-inline: 12px; + color: ${(props) => props.color ?? white}; + background-color: ${(props) => props.backgroundColor ?? gray}; + gap: 10px; + filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25)); + transform: translate(20px, -50%); + + &::after { + content: ''; + position: absolute; + width: 0; + height: 0; + right: 100%; + top: 50%; + transform: translate(0, -50%); + border-top: 7px solid transparent; + border-right: 8.5px solid ${(props) => props.backgroundColor ?? gray}; + border-bottom: 7px solid transparent; + border-left: 0px solid transparent; + filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25)); + } +`; + +const Bubble = ({ + children, + color, + backgroundColor, +}: { + children: ReactNode; + color?: string; + backgroundColor?: string; +}): ReactElement => { + return ( + + {children} + + ); +}; + +export default Bubble; diff --git a/client/src/components/Button.tsx b/client/src/components/Button.tsx index cead737..0802465 100644 --- a/client/src/components/Button.tsx +++ b/client/src/components/Button.tsx @@ -1,4 +1,4 @@ -import { FC, ReactElement, memo } from 'react'; +import { FC, ReactElement, memo, ReactNode } from 'react'; import styled from 'styled-components'; interface StyleProps { @@ -9,10 +9,11 @@ interface StyleProps { borderRadius?: string; margin?: string; flexGrow?: number; + children?: ReactNode; } interface Props extends StyleProps { - context: string | ReactElement; + context?: string | ReactElement; onClick?: (e: React.MouseEvent) => void; } @@ -26,10 +27,11 @@ const StyledButton = styled.button` flex-grow: ${({ flexGrow }) => flexGrow}; `; -const Button: FC = ({ context, onClick, ...props }) => { +const Button: FC = ({ context, onClick, children, ...props }) => { return ( {context} + {children} ); }; diff --git a/client/src/components/ElapsedTimeText.tsx b/client/src/components/ElapsedTimeText.tsx new file mode 100644 index 0000000..4d645fa --- /dev/null +++ b/client/src/components/ElapsedTimeText.tsx @@ -0,0 +1,17 @@ +import Text from '@components/Text'; + +import { displayTimeAtom, elapsedTimeAtom } from '@util/GlobalState'; +import { useAtom, useAtomValue } from 'jotai'; +import { ReactElement, memo, useEffect } from 'react'; + +const ElapsedTimeText = ({ color }: { color: string }): ReactElement => { + const elapsedTime = useAtomValue(elapsedTimeAtom); + const [displayTime, setDisplayTime] = useAtom(displayTimeAtom); + + useEffect(() => { + setDisplayTime(); + }, [elapsedTime]); + + return ; +}; +export default memo(ElapsedTimeText); diff --git a/client/src/components/Modal.tsx b/client/src/components/Modal.tsx new file mode 100644 index 0000000..4bce217 --- /dev/null +++ b/client/src/components/Modal.tsx @@ -0,0 +1,99 @@ +import { memo, ReactElement, ReactNode, SetStateAction, Dispatch, MutableRefObject, useRef } from 'react'; +import styled from 'styled-components'; + +import { PRIMARY_COLORS } from '@util/Constants'; +import { getModalValues } from '@util/Common'; + +import Button from '@components/Button'; +import Text from '@components/Text'; +import OverLay from '@components/OverLay'; + +import 'react-toastify/dist/ReactToastify.css'; + +const { offWhite, red, blue, darkGray, lightGray } = PRIMARY_COLORS; + +interface WrapperProps { + ref: any; +} + +const Wrapper = styled.div` + width: 50vw; + height: max-content; + max-height: 80vh; + overflow-y: auto; + position: relative; + background-color: ${offWhite}; + box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + border-radius: 20px; + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 30px; + gap: 10px; + + input { + width: 100%; + color: ${darkGray}; + font-family: 'SanSerif'; + font-size: 15px; + padding: 5px; + border: 1px solid ${lightGray}; + border-radius: 5px; + outline: none; + } + + textarea { + resize: none; + } +`; + +const ButtonWrapper = styled.div` + width: 100%; + display: flex; + gap: 15px; + padding-top: 15px; + text-align: right; + justify-content: flex-end; +`; + +interface ModalProps { + modalHeader: string; + action: Function; + setIsModalOpen: Dispatch>; + children?: ReactNode; + ref?: MutableRefObject; + editingTodoId?: string; +} + +const Modal = ({ modalHeader, action, setIsModalOpen, children, editingTodoId }: ModalProps): ReactElement => { + const modalWrapper = useRef(); + + const handleOnConfirm = (): void => { + if (modalWrapper.current === undefined) { + return; + } + const inputData = getModalValues(modalWrapper.current); + action(inputData, setIsModalOpen, editingTodoId); + }; + + return ( + + + + {children} + +