diff --git a/client/package-lock.json b/client/package-lock.json index d4b2103..54fe172 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -28,6 +28,7 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.4.3", "react-scripts": "5.0.1", + "react-toastify": "^9.1.1", "styled-components": "^5.3.6", "styled-reset": "^4.4.2", "typescript": "^4.8.4", @@ -5659,6 +5660,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -14621,6 +14630,18 @@ } } }, + "node_modules/react-toastify": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz", + "integrity": "sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==", + "dependencies": { + "clsx": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -21478,6 +21499,11 @@ "wrap-ansi": "^7.0.0" } }, + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -27766,6 +27792,14 @@ "workbox-webpack-plugin": "^6.4.1" } }, + "react-toastify": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz", + "integrity": "sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==", + "requires": { + "clsx": "^1.1.1" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/client/package.json b/client/package.json index 7fd21cb..9839305 100644 --- a/client/package.json +++ b/client/package.json @@ -23,6 +23,7 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.4.3", "react-scripts": "5.0.1", + "react-toastify": "^9.1.1", "styled-components": "^5.3.6", "styled-reset": "^4.4.2", "typescript": "^4.8.4", diff --git a/client/src/api/responseApi.ts b/client/src/api/responseApi.ts index 379d95b..db0ec80 100644 --- a/client/src/api/responseApi.ts +++ b/client/src/api/responseApi.ts @@ -20,6 +20,10 @@ const responseApi = { const { data } = await axios.get(`${API.RESPONSE}/${formId}/${responseId}`, { withCredentials: true }); return data.answerList; }, + checkDuplicateResponse: async (formId: string | undefined) => { + const { data } = await axios.get(`${API.RESPONSE}/isSubmitted/${formId}`, { withCredentials: true }); + return data; + }, }; export default responseApi; diff --git a/client/src/components/Modal/LoginModal/index.tsx b/client/src/components/Modal/LoginModal/index.tsx new file mode 100644 index 0000000..d4ec42a --- /dev/null +++ b/client/src/components/Modal/LoginModal/index.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import Button from "components/common/Button"; +import theme from "styles/theme"; +import * as S from "./style"; + +function LoginModal({ closeModal }: { closeModal: () => void }) { + const navigate = useNavigate(); + + const onClickLogin = () => { + closeModal(); + navigate("/login"); + }; + + return ( + + 계속 하려면 로그인 + 이 설문지를 작성하려면 로그인해야 합니다. 신원은 익명으로 유지됩니다. + + + + + ); +} + +export default LoginModal; diff --git a/client/src/components/Modal/LoginModal/style.ts b/client/src/components/Modal/LoginModal/style.ts new file mode 100644 index 0000000..a5eca16 --- /dev/null +++ b/client/src/components/Modal/LoginModal/style.ts @@ -0,0 +1,41 @@ +import styled from "styled-components"; + +const Container = styled.div` + position: absolute; + top: 35%; + left: 50%; + transform: translate(-50%, -50%); + + width: 400px; + border-radius: 3px; + padding: 24px; + + z-index: 2; + background-color: ${({ theme }) => theme.colors.white}; +`; + +const Title = styled.h2` + margin-bottom: 20px; + font-size: ${({ theme }) => theme.fontSize.sz20}; + font-weight: 400; +`; + +const Text = styled.p` + margin-bottom: 16px; + font-size: 14px; +`; + +const Input = styled.input` + width: 100%; + padding: 5px 10px; + border: 1px solid ${({ theme }) => theme.colors.grey3}; + border-radius: 3px; + margin-bottom: 24px; +`; + +const ButtonContainer = styled.div` + display: flex; + justify-content: right; +`; + +export { Container, ButtonContainer, Input, Title, Text }; diff --git a/client/src/components/common/Button/index.tsx b/client/src/components/common/Button/index.tsx index f440289..8092bf0 100644 --- a/client/src/components/common/Button/index.tsx +++ b/client/src/components/common/Button/index.tsx @@ -2,9 +2,8 @@ import React from "react"; import theme from "styles/theme"; import ButtonComponent from "./style"; -interface ButtonProps { +interface ButtonProps extends React.ButtonHTMLAttributes { children: React.ReactNode; - type: "button" | "submit" | "reset"; color?: string; backgroundColor?: string; hover?: string; @@ -12,7 +11,6 @@ interface ButtonProps { active?: boolean; border?: string; custom?: string; - onClick: () => void; } function Button({ diff --git a/client/src/components/common/Pagination/index.tsx b/client/src/components/common/Pagination/index.tsx index 1ca90c3..5c359df 100644 --- a/client/src/components/common/Pagination/index.tsx +++ b/client/src/components/common/Pagination/index.tsx @@ -6,11 +6,11 @@ import * as S from "./style"; function Pagination({ currentPage, lastPage, - setPage, + callback, }: { currentPage: number; lastPage: number; - setPage: React.Dispatch>; + callback: (pageNumber: number) => void; }) { const [pageNumbers, setPageNumbers] = useState([]); @@ -30,7 +30,7 @@ function Pagination({ setPage((prev) => prev - 1)} + onClick={() => callback(currentPage - 1)} disabled={currentPage === 1} icon="left" fill={theme.colors.grey5} @@ -38,7 +38,7 @@ function Pagination({ /> {pageNumbers.map((number) => ( - setPage(number)}> + callback(number)}> {number} ))} @@ -46,7 +46,7 @@ function Pagination({ setPage((prev) => prev + 1)} + onClick={() => callback(currentPage + 1)} disabled={currentPage === lastPage} icon="right" fill={theme.colors.grey5} diff --git a/client/src/components/common/Skeleton/index.tsx b/client/src/components/common/Skeleton/index.tsx index b2703ce..6a1657d 100644 --- a/client/src/components/common/Skeleton/index.tsx +++ b/client/src/components/common/Skeleton/index.tsx @@ -1,5 +1,6 @@ import React from "react"; import * as S from "./style"; +import SkeletonType from "./type"; function SkeletonContainer({ children, custom = "" }: { children: React.ReactNode; custom?: string }) { return {children}; @@ -8,7 +9,7 @@ SkeletonContainer.defaultProps = { custom: "", }; -function Element({ type }: { type: string }) { +function Element({ type }: { type: SkeletonType }) { return ; } diff --git a/client/src/components/common/Skeleton/style.ts b/client/src/components/common/Skeleton/style.ts index 6f6e730..c61ed13 100644 --- a/client/src/components/common/Skeleton/style.ts +++ b/client/src/components/common/Skeleton/style.ts @@ -1,11 +1,13 @@ import styled, { css } from "styled-components"; +import SkeletonType from "./type"; -const getSkeletonTypeCss = (type: string) => { +const getSkeletonTypeCss = (type: SkeletonType) => { switch (type) { case "text": return css` width: 100%; height: 12px; + margin: 10px 0; `; case "title": @@ -13,6 +15,41 @@ const getSkeletonTypeCss = (type: string) => { width: 50%; height: 20px; margin-bottom: 15px; + margin-top: 10px; + `; + + case "formTitle": + return css` + width: 33%; + height: 27px; + padding: 5px 0; + margin: 10px 0 20px; + `; + + case "formCategoryBox": + return css` + width: 150px; + height: 38px; + `; + + case "formQuestionTitleEdit": + return css` + width: 33%; + height: 16px; + margin: 30px 0 20px; + `; + + case "formQuestionTitle": + return css` + width: 33%; + height: 16px; + margin-bottom: 20px; + `; + + case "button": + return css` + width: 55px; + height: 30px; `; default: @@ -31,9 +68,8 @@ const Container = styled.div<{ custom: string }>` ${({ custom }) => custom} `; -const Element = styled.div<{ type: string }>` +const Element = styled.div<{ type: SkeletonType }>` background-color: #ddd; - margin: 10px 0; border-radius: 3px; ${({ type }) => getSkeletonTypeCss(type)} diff --git a/client/src/components/common/Skeleton/type.ts b/client/src/components/common/Skeleton/type.ts new file mode 100644 index 0000000..5658eed --- /dev/null +++ b/client/src/components/common/Skeleton/type.ts @@ -0,0 +1,10 @@ +type SkeletonType = + | "text" + | "title" + | "formTitle" + | "formCategoryBox" + | "formQuestionTitle" + | "button" + | "formQuestionTitleEdit"; + +export default SkeletonType; diff --git a/client/src/hooks/useModal/index.tsx b/client/src/hooks/useModal/index.tsx index 279cc32..3feb705 100644 --- a/client/src/hooks/useModal/index.tsx +++ b/client/src/hooks/useModal/index.tsx @@ -3,7 +3,7 @@ import { createPortal } from "react-dom"; import * as S from "./style"; import ModalPortalProps from "./type"; -const useModal = () => { +const useModal = (option?: { setBackgroundClickClose: boolean }) => { const [modalOpen, setModalOpen] = useState(false); const modalRoot = document.getElementById("modal-root") as HTMLElement; const [windowOffsetY, setWindowOffsetY] = useState(0); @@ -25,12 +25,17 @@ const useModal = () => { setModalOpen(false); }; + const onClickBackgroundCloseModal = () => { + if (option && !option.setBackgroundClickClose) setModalOpen(false); + if (!option) setModalOpen(false); + }; + function ModalPortal({ children }: ModalPortalProps) { if (modalOpen) return createPortal( {children} - + , modalRoot ); diff --git a/client/src/pages/Edit/index.tsx b/client/src/pages/Edit/index.tsx index fb874e6..8a6c99b 100644 --- a/client/src/pages/Edit/index.tsx +++ b/client/src/pages/Edit/index.tsx @@ -12,7 +12,7 @@ import Icon from "components/common/Icon"; import ToggleButton from "components/common/ToggleButton"; import QuestionRead from "components/Edit/QuestionRead"; import TextDropdown from "components/common/Dropdown/TextDropdown"; - +import Skeleton from "components/common/Skeleton"; import ShareFormModal from "components/Modal/ShareFormModal"; import Button from "components/common/Button"; import IconButton from "components/common/IconButton"; @@ -23,6 +23,9 @@ import formApi from "api/formApi"; import { fromApiToForm, fromFormToApi } from "utils/form"; import useModal from "hooks/useModal"; import { CATEGORY_LIST, QUESTION_TYPE_LIST } from "store/form"; +import useLoadingDelay from "hooks/useLoadingDelay"; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; import * as S from "./style"; const initialState: FormState = { @@ -45,7 +48,11 @@ function Edit() { const { id } = useParams(); const fetchForm = (): Promise => formApi.getForm(id); - const { data, isSuccess } = useQuery({ queryKey: [id], queryFn: fetchForm }); + const { data, isSuccess, isLoading, isError } = useQuery({ + queryKey: [id], + queryFn: fetchForm, + refetchOnWindowFocus: false, + }); const [state, dispatch] = useReducer(writeReducer, initialState); const { form, question } = state; @@ -54,10 +61,11 @@ function Edit() { const [drag, setDrag] = useState(""); const { openModal, closeModal, ModalPortal } = useModal(); + const delayLoading = useLoadingDelay(isLoading); useEffect(() => { if (!id) return; - if (isSuccess) dispatch({ type: "FETCH_DATA", init: fromApiToForm(data) }); + if (isSuccess) dispatch({ type: "FETCH_DATA", init: fromApiToForm(data, "edit") }); }, [data, id, isSuccess]); const onClickTitle = () => { @@ -111,7 +119,19 @@ function Edit() { }; const onClickDeleteQuestion = (questionIndex: number) => { - dispatch({ type: "DELETE_QUESTION", questionIndex }); + const toastCallback = () => { + toast.error("삭제가 불가능합니다!", { + position: "top-right", + autoClose: 2000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: false, + draggable: true, + progress: undefined, + theme: "light", + }); + }; + dispatch({ type: "DELETE_QUESTION", questionIndex, callback: toastCallback }); }; const onClickChangeQuestionEssential = (questionIndex: number) => { @@ -139,13 +159,59 @@ function Edit() { }; const onClickCopyLink = () => { - navigator.clipboard.writeText(`${process.env.REACT_APP_CLIENT_ORIGIN_URL}/forms/${id}/view`); + window.navigator.clipboard.writeText(`${process.env.REACT_APP_CLIENT_ORIGIN_URL}/forms/${id}/view`); + toast.success("링크가 복사되었습니다!", { + position: "top-right", + autoClose: 2000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: false, + draggable: true, + progress: undefined, + theme: "light", + }); }; const onClickSaveForm = () => { if (!id) return; + if (!form.title) { + toast.error("제목을 작성해주세요!", { + position: "top-right", + autoClose: 2000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: false, + draggable: true, + progress: undefined, + theme: "light", + }); + return; + } + if (!form.category) { + toast.error("카테고리를 정해주세요!", { + position: "top-right", + autoClose: 2000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: false, + draggable: true, + progress: undefined, + theme: "light", + }); + return; + } const apiData = fromFormToApi(state); formApi.saveForm(id, apiData); + toast.success("저장이 완료되었습니다.!", { + position: "top-right", + autoClose: 2000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: false, + draggable: true, + progress: undefined, + theme: "light", + }); }; const onDragStart = (initial: DragStart) => { @@ -171,25 +237,27 @@ function Edit() { return false; }; + const checkApiSuccess = () => { + if (!delayLoading && isSuccess) return true; + return false; + }; + const checkApiLoadingOrError = () => { + if (isLoading || delayLoading || isError) return true; + return false; + }; + return ( onClickTitle()}> - {focus !== "title" && ( + {checkApiSuccess() && ( <> - {form.title} - - {form.description ? form.description : "설문지 설명"} - - - {form.category || "카테고리"} - - - )} - {focus === "title" && ( - <> - - + + @@ -200,124 +268,153 @@ function Edit() { )} + {checkApiLoadingOrError() ? ( + <> + + + + + ) : null} {(droppable) => (
- {question.map(({ questionId, title, type, essential }, questionIndex) => ( - - {(draggable) => { - let transform = draggable.draggableProps.style?.transform; - - if (transform) { - transform = transform.replace(/([0-9]+px)/, "0px"); - draggable.draggableProps.style = { - ...draggable.draggableProps.style, - transform, - }; - } - - return ( - onClickQuestion(questionIndex)} - onMouseOver={() => onMouseOverQuestion(questionIndex)} - onMouseOut={() => onMouseOutQuestion()} - {...draggable.draggableProps} - ref={draggable.innerRef} - > - - {showDragIndicator(questionIndex) ? : null} - - {focus === `q${questionIndex}` && ( - <> - - onInputQuestionTitle(e.currentTarget.value, questionIndex)} - value={question[questionIndex].title} - placeholder="질문" - /> - { - const isQuestionType = (str: string): str is QuestionType => - str === "checkbox" || str === "multiple" || str === "paragraph"; - - if (isQuestionType(questionType)) - onClickSetQuestionType(questionType, questionIndex); - }} - items={QUESTION_TYPE_LIST} - defaultValue="선택해주세요" - /> - - - - - - - onClickAddQuestion(questionIndex)} - icon="add" - size="21px" - custom="margin-right: 12px;" - /> - onClickCopyQuestion(questionIndex)} - icon="copy" - size="18px" - custom="margin-right: 12px;" - /> - onClickDeleteQuestion(questionIndex)} - icon="trashcan" - size="18px" - custom="margin-right: 12px;" - /> - - 필수 - onClickChangeQuestionEssential(questionIndex)} + {checkApiSuccess() && + question.map(({ questionId, title, type, essential }, questionIndex) => ( + + {(draggable) => { + let transform = draggable.draggableProps.style?.transform; + + if (transform) { + transform = transform.replace(/([0-9]+px)/, "0px"); + draggable.draggableProps.style = { + ...draggable.draggableProps.style, + transform, + }; + } + + return ( + onClickQuestion(questionIndex)} + onMouseOver={() => onMouseOverQuestion(questionIndex)} + onMouseOut={() => onMouseOutQuestion()} + {...draggable.draggableProps} + ref={draggable.innerRef} + > + + {showDragIndicator(questionIndex) ? : null} + + {focus === `q${questionIndex}` && ( + <> + + onInputQuestionTitle(e.currentTarget.value, questionIndex)} + value={question[questionIndex].title} + placeholder="질문" /> - - - - )} - {focus !== `q${questionIndex}` && ( - <> -
{title}
- - - )} -
- ); - }} -
- ))} + { + const isQuestionType = (str: string): str is QuestionType => + str === "checkbox" || str === "multiple" || str === "paragraph"; + + if (isQuestionType(questionType)) + onClickSetQuestionType(questionType, questionIndex); + }} + items={QUESTION_TYPE_LIST} + defaultValue="선택해주세요" + /> + + + + + + + onClickAddQuestion(questionIndex)} + icon="add" + size="21px" + custom="margin-right: 12px;" + /> + onClickCopyQuestion(questionIndex)} + icon="copy" + size="18px" + custom="margin-right: 12px;" + /> + onClickDeleteQuestion(questionIndex)} + icon="trashcan" + size="18px" + custom="margin-right: 12px;" + /> + + 필수 + onClickChangeQuestionEssential(questionIndex)} + /> + + + + )} + {focus !== `q${questionIndex}` && ( + <> +
{title}
+ + + )} + + ); + }} + + ))} {droppable.placeholder}
)}
+ + {checkApiLoadingOrError() + ? Array.from({ length: 2 }, (_, index) => index).map((value) => ( + + + + + + + + + )) + : null} - + {checkApiSuccess() && ( + + )} + {checkApiLoadingOrError() ? ( + <> + + + + ) : null}
@@ -333,6 +430,19 @@ function Edit() { copyLink={onClickCopyLink} /> + +
); } diff --git a/client/src/pages/Edit/style.ts b/client/src/pages/Edit/style.ts index 1e2bbb8..5869e6b 100644 --- a/client/src/pages/Edit/style.ts +++ b/client/src/pages/Edit/style.ts @@ -75,6 +75,8 @@ const QuestionContainer = styled.div` border-radius: 3px; padding: 0 20px 20px; border: solid 1px ${({ theme }) => theme.colors.grey3}; + position: relative; + overflow: hidden; `; const QuestionHead = styled.div` @@ -158,6 +160,8 @@ const BottomContainer = styled.div` background-color: ${({ theme }) => theme.colors.white}; border-radius: 3px; padding: 20px; + position: relative; + overflow: hidden; `; const DragIndicator = styled.div` diff --git a/client/src/pages/Forum/index.tsx b/client/src/pages/Forum/index.tsx index ca9ffbf..3384447 100644 --- a/client/src/pages/Forum/index.tsx +++ b/client/src/pages/Forum/index.tsx @@ -1,5 +1,5 @@ -import React, { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import React, { useEffect, useState } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; import Layout from "components/template/BannerLayout"; import Button from "components/common/Button"; import theme from "styles/theme"; @@ -29,13 +29,34 @@ interface ForumApi { function Forum() { const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const initPage = Number(searchParams.get("page")) || 1; + const initCategory = searchParams.get("category") || ""; + const initKeyword = searchParams.get("keyword") || ""; + const initOrderBy = searchParams.get("orderBy") || ""; - const [inputSearch, setInputSearch] = useState(""); - const [keyword, setKeyword] = useState(""); - const [category, setCategory] = useState("전체"); - const [orderBy, setOrderBy] = useState("latestAsc"); - const [page, setPage] = useState(1); - // const [loadingDelay, setLoadingDelay] = useState(false); + function isTypeOfCategory(categoryParam: string): categoryParam is ForumCategory { + const Category = CATEGORY_FORUM_LIST as string[]; + return Category.includes(categoryParam); + } + function isTypeOfOrderBy(orderByParam: string): orderByParam is OrderBy { + const ORDER_BY = ["latestAsc", "responseAsc", "responseDesc"]; + return ORDER_BY.includes(orderByParam); + } + + const [inputSearch, setInputSearch] = useState(initKeyword); + const [keyword, setKeyword] = useState(initKeyword); + const [category, setCategory] = useState(isTypeOfCategory(initCategory) ? initCategory : "전체"); + const [orderBy, setOrderBy] = useState(isTypeOfOrderBy(initOrderBy) ? initOrderBy : "latestAsc"); + const [page, setPage] = useState(Number(initPage)); + + useEffect(() => { + setInputSearch(initKeyword); + setKeyword(initKeyword); + setCategory(isTypeOfCategory(initCategory) ? initCategory : "전체"); + setOrderBy(isTypeOfOrderBy(initOrderBy) ? initOrderBy : "latestAsc"); + setPage(Number(initPage)); + }, [initCategory, initKeyword, initOrderBy, initPage]); const fetchFormList = (): Promise => boardApi.getFormList({ title: keyword, category, orderBy, page }); const { data, isLoading, isSuccess, isError } = useQuery({ @@ -45,10 +66,20 @@ function Forum() { const loadingDelay = useLoadingDelay(isLoading); + const checkApiLoadingOrError = () => { + if (isLoading || loadingDelay || isError) return true; + return false; + }; + + const onSubmitSearchKeyword: React.FormEventHandler = (e) => { + e.preventDefault(); + setSearchParams({ page: "1", category, keyword: inputSearch, orderBy }); + }; + return ( - + - + {checkApiSuccess() && + (question.length ? ( + question.map(({ questionId, title, essential }, questionIndex) => ( + +
+ {title} + {essential ? * : null} +
+ +
+ )) + ) : ( + + 설문지 문항이 존재하지 않습니다. + + ))} + {checkApiLoadingOrError() + ? Array.from({ length: 2 }, (_, index) => index).map((value) => ( + + + + + + + + + )) + : null} + {question.length ? ( + + {checkApiSuccess() && ( + + )} + {checkApiLoadingOrError() ? ( + <> + + + + ) : null} + + ) : null} + + + + + ); diff --git a/client/src/pages/View/style.ts b/client/src/pages/View/style.ts index 50c56d9..91d2b95 100644 --- a/client/src/pages/View/style.ts +++ b/client/src/pages/View/style.ts @@ -9,6 +9,8 @@ const HeadContainer = styled.div` background-color: ${({ theme }) => theme.colors.white}; border-radius: 3px; padding: 10px 20px; + position: relative; + overflow: hidden; `; const HeadTitle = styled.div` @@ -41,6 +43,9 @@ const QuestionContainer = styled.div<{ isEssential: boolean }>` css` border: 1px solid ${({ theme }) => theme.colors.red1}; `} + + position: relative; + overflow: hidden; `; const BottomContainer = styled.div` @@ -50,6 +55,8 @@ const BottomContainer = styled.div` background-color: ${({ theme }) => theme.colors.white}; border-radius: 3px; padding: 20px; + position: relative; + overflow: hidden; `; const Essential = styled.span` @@ -57,4 +64,18 @@ const Essential = styled.span` margin-left: 8px; `; -export { Container, HeadContainer, HeadTitle, HeadDescription, QuestionContainer, BottomContainer, Essential }; +const NoResponseForm = styled.div` + font-size: 14px; + font-weight: 400; +`; + +export { + Container, + HeadContainer, + HeadTitle, + HeadDescription, + QuestionContainer, + BottomContainer, + Essential, + NoResponseForm, +}; diff --git a/client/src/reducer/formEdit/index.ts b/client/src/reducer/formEdit/index.ts index d5919c8..c5b1281 100644 --- a/client/src/reducer/formEdit/index.ts +++ b/client/src/reducer/formEdit/index.ts @@ -139,7 +139,12 @@ function formEditReducer(state: FormState, action: FormEditAction) { }; } if (type === "DELETE_QUESTION") { - const { questionIndex } = action; + const { questionIndex, callback } = action; + const { length } = state.question; + if (length === 1) { + callback(); + return { ...state }; + } const leftQuestion = state.question.slice(0, questionIndex); const rightQuestion = state.question.slice(questionIndex + 1); diff --git a/client/src/reducer/formEdit/type.ts b/client/src/reducer/formEdit/type.ts index 7879c8c..5d54ac5 100644 --- a/client/src/reducer/formEdit/type.ts +++ b/client/src/reducer/formEdit/type.ts @@ -8,7 +8,7 @@ type FormEditAction = | { type: "ADD_QUESTION_CHOICE"; questionIndex: number } | { type: "MODIFY_QUESTION_CHOICE"; questionIndex: number; choiceIndex: number; value: string } | { type: "DELETE_QUESTION_CHOICE"; questionIndex: number; choiceIndex: number } - | { type: "DELETE_QUESTION"; questionIndex: number } + | { type: "DELETE_QUESTION"; questionIndex: number; callback: () => void } | { type: "COPY_QUESTION"; questionIndex: number } | { type: "ADD_QUESTION"; questionIndex: number } | { type: "CHANGE_QUESTION_ESSENTIAL"; questionIndex: number } diff --git a/client/src/utils/form.ts b/client/src/utils/form.ts index 8282f42..279f953 100644 --- a/client/src/utils/form.ts +++ b/client/src/utils/form.ts @@ -1,6 +1,6 @@ import { FormState, QuestionState, FormDataApi } from "types/form"; -const fromApiToForm = (api: FormDataApi): FormState => { +const fromApiToForm = (api: FormDataApi, pageType: string): FormState => { const { id, userID, @@ -17,7 +17,7 @@ const fromApiToForm = (api: FormDataApi): FormState => { let formQuestionList: QuestionState[]; let currentQuestionId = 1; - if (!questionList.length) + if (pageType === "edit" && !questionList.length) formQuestionList = [ { questionId: 1, diff --git a/server/src/Response/Response.Controller.ts b/server/src/Response/Response.Controller.ts index e9d05c6..ee5186e 100644 --- a/server/src/Response/Response.Controller.ts +++ b/server/src/Response/Response.Controller.ts @@ -9,9 +9,9 @@ class ResponseController { const userID = Number(req.userID); const { formId } = req.params; - const responsed = await ResponseService.checkAnswerExistence(formId, userID); + const responseId = await ResponseService.checkAnswerExistence(formId, userID); - res.status(200).json({ responsed }); + res.status(200).json({ responseId }); } catch (err) { next(err); } diff --git a/server/src/Response/Response.Routes.ts b/server/src/Response/Response.Routes.ts index edb5f5a..162a4a4 100644 --- a/server/src/Response/Response.Routes.ts +++ b/server/src/Response/Response.Routes.ts @@ -4,7 +4,7 @@ import { authMiddleware, checkAccessTokenExistence } from "../Middlewares/Auth.M const responseRouter = express.Router(); -responseRouter.get("/:formId", authMiddleware, ResponseController.checkResponseExistence); +responseRouter.get("/isSubmitted/:formId", authMiddleware, ResponseController.checkResponseExistence); responseRouter.post("/:formId", checkAccessTokenExistence, ResponseController.saveResponse); diff --git a/server/src/Response/Response.Service.ts b/server/src/Response/Response.Service.ts index 1060de7..fbbe60c 100644 --- a/server/src/Response/Response.Service.ts +++ b/server/src/Response/Response.Service.ts @@ -4,9 +4,13 @@ import { AnswerInterface, AnswerFromRequest } from "./Response.Interface"; class ResponseService { static async checkAnswerExistence(formId: string, userID: number) { - const isExist = await FormResponse.findOne({ form_id: formId, user_id: userID }); + const response = await FormResponse.findOne({ form_id: formId, user_id: userID }); - return !(isExist === null); + if (response !== null) { + return response.id; + } + + return null; } static async saveResponse(formId: string, userID: number | undefined, answerList: Array) { diff --git a/server/src/User/User.Controller.ts b/server/src/User/User.Controller.ts index b0e2b28..9914a3c 100644 --- a/server/src/User/User.Controller.ts +++ b/server/src/User/User.Controller.ts @@ -61,7 +61,7 @@ class UserController { userService .logout(userID) .then(() => { - res.status(204).clearCookie("accessToken").clearCookie("refreshToken"); + res.status(204).clearCookie("accessToken").clearCookie("refreshToken").end(); }) .catch((err) => { next(err);