diff --git a/lib/axiosConfig.ts b/lib/axiosConfig.ts index 6937d1c7..215eb589 100644 --- a/lib/axiosConfig.ts +++ b/lib/axiosConfig.ts @@ -19,6 +19,7 @@ export const postFetch = (url: string, data = {}) => { data, }).then((response) => response.data); }; + export const patchFetch = (url: string, data = {}) => { return instance({ method: 'PATCH', @@ -26,6 +27,15 @@ export const patchFetch = (url: string, data = {}) => { data, }).then((response) => response.data); }; + +export const putFetch = (url: string, data = {}) => { + return instance({ + method: 'PUT', + url, + data, + }).then((response) => response.data); +}; + export const deleteFetch = (url: string) => { return instance({ method: 'DELETE', diff --git a/locales/en/surveyCreate.json b/locales/en/surveyCreate.json index 39d3be6c..35e4544c 100644 --- a/locales/en/surveyCreate.json +++ b/locales/en/surveyCreate.json @@ -2,7 +2,12 @@ "title": "Create Survey", "content": "Create Survey - FormsLab", "heading": "Create new survey", + "editHeading": "Edit survey", "buttonCreate": "Create Survey", + "editNote": "Some action like adding and removing questions or changing answers are not available in edit mode.", + "editNoteTitle": "Note", + "buttonSave": "Save changes", + "discardChanges": "Discard changes", "surveyTitleLable": "Survey Title", "surveyTitlePlaceholder": "Survey Title...", "questionPlaceholder": "Question...", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 564d63fc..9dedcb60 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -71,8 +71,8 @@ model Survey { description String? questions Question[] answers Answer[] - oneQuestionPerStep Boolean? - displayTitle Boolean? + oneQuestionPerStep Boolean + displayTitle Boolean user User @relation(fields: [userId], references: [id], onDelete: Cascade) } @@ -86,6 +86,7 @@ model Question { type QuestionType isRequired Boolean options String[] + order Int survey Survey @relation(fields: [surveyId], references: [id], onDelete: Cascade) } diff --git a/public/images/creator.webp b/public/images/creator.webp index cd904dac..70ad1093 100644 Binary files a/public/images/creator.webp and b/public/images/creator.webp differ diff --git a/src/features/surveys/components/AddQuestionButton/AddQuestionButton.tsx b/src/features/surveys/components/AddQuestionButton/AddQuestionButton.tsx index 1daeac90..2e0c6ad2 100644 --- a/src/features/surveys/components/AddQuestionButton/AddQuestionButton.tsx +++ b/src/features/surveys/components/AddQuestionButton/AddQuestionButton.tsx @@ -14,7 +14,7 @@ interface AddQuestionButtonProps { export const AddQuestionButton = ({ onClick }: AddQuestionButtonProps) => { const { closeModal, isModalOpen, openModal } = useModal(); return ( -
+
@@ -135,7 +138,15 @@ export default function QuestionBlockWrapper({ {expanded && (
- {children} + {isEditMode ? ( +
+
+ + {children} +
+ ) : ( + children + )}
- + {link}
diff --git a/src/features/surveys/components/SurveyRow/SurveyRow.tsx b/src/features/surveys/components/SurveyRow/SurveyRow.tsx index 5ebe9703..e80a670a 100644 --- a/src/features/surveys/components/SurveyRow/SurveyRow.tsx +++ b/src/features/surveys/components/SurveyRow/SurveyRow.tsx @@ -46,7 +46,7 @@ export default function SurveyRow({ return (
-
+
{question}
diff --git a/src/features/surveys/managers/createSurveyManager.ts b/src/features/surveys/managers/createSurveyManager.ts index 97fd4684..4784f0a1 100644 --- a/src/features/surveys/managers/createSurveyManager.ts +++ b/src/features/surveys/managers/createSurveyManager.ts @@ -4,9 +4,11 @@ import toast from 'react-hot-toast'; import useCopyToClipboard from 'shared/hooks/useCopyToClipboard'; import useTranslation from 'next-translate/useTranslation'; import { QuestionType } from '@prisma/client'; -import { postFetch } from '../../../../lib/axiosConfig'; +import { postFetch, putFetch } from '../../../../lib/axiosConfig'; import { defaultQuestions } from 'shared/constants/surveysConfig'; import { DRAFT_SURVEY_SESSION_STORAGE } from 'shared/constants/app'; +import { SurveyWithQuestions } from 'types/SurveyWithQuestions'; +import { Question as QuestionDto } from '@prisma/client'; export interface Question { id: string; @@ -22,19 +24,32 @@ export interface SurveyOptions { displayTitle: boolean; } -export const useCreateSurveyManager = () => { - const [title, setTitle] = useState(''); - const [questions, setQuestions] = useState(defaultQuestions); +export const useCreateSurveyManager = (initialData?: SurveyWithQuestions) => { + const [isEditMode] = useState(!!initialData); + + const [title, setTitle] = useState(initialData?.title ?? ''); + + const mapQuestionsWithExpanded = (questions?: QuestionDto[]) => { + return questions?.map((question) => ({ + ...question, + expanded: false, + })); + }; + + const [questions, setQuestions] = useState( + mapQuestionsWithExpanded(initialData?.questions) ?? defaultQuestions + ); + const [surveyOptions, setSurveyOptions] = useState({ + oneQuestionPerStep: initialData?.oneQuestionPerStep ?? true, + displayTitle: initialData?.displayTitle ?? true, + }); + const [error, setError] = useState(''); const [isCreating, setIsCreating] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false); const router = useRouter(); const { copy } = useCopyToClipboard(); const { t } = useTranslation('surveyCreate'); - const [surveyOptions, setSurveyOptions] = useState({ - oneQuestionPerStep: true, - displayTitle: true, - }); const signInToCreateSurvey = () => { router.push('/login'); @@ -249,6 +264,40 @@ export const useCreateSurveyManager = () => { setIsCreating(false); }; + const confirmEditSurvey = async () => { + if (!isSurveyValid() || !initialData) return; + + setIsCreating(true); + + try { + const newSurvey = await putFetch(`/api/survey/${initialData.id}`, { + title, + oneQuestionPerStep: surveyOptions.oneQuestionPerStep, + displayTitle: surveyOptions.displayTitle, + questions: questions.map((question) => ({ + id: question.id, + title: question.title, + options: question.options, + type: question.type, + isRequired: question.isRequired, + })), + }); + + await router.push(`/survey/answer/${newSurvey.id}`, undefined, { + scroll: false, + }); + } catch (error) { + toast.error(t('surveyCreationFailed')); + } + setIsCreating(false); + }; + + const discardChanges = () => { + router.push(`/survey/answer/${initialData?.id}`, undefined, { + scroll: false, + }); + }; + const reorderQuestion = (startIndex: number, endIndex: number) => { const newOrderedQuestions = Array.from(questions); @@ -288,5 +337,8 @@ export const useCreateSurveyManager = () => { surveyOptions, updateSurveyOptions, signInToCreateSurvey, + isEditMode, + confirmEditSurvey, + discardChanges, }; }; diff --git a/src/layout/Footer/Footer.tsx b/src/layout/Footer/Footer.tsx index 15b83fa9..ad9f4e07 100644 --- a/src/layout/Footer/Footer.tsx +++ b/src/layout/Footer/Footer.tsx @@ -3,7 +3,7 @@ import React from 'react'; export default function Footer() { return (
- FormsLab © 2023 + FormsLab © 2024
); } diff --git a/src/pages/api/answer/[id].ts b/src/pages/api/answer/[id].ts index 1f1b63bf..b8f2c941 100644 --- a/src/pages/api/answer/[id].ts +++ b/src/pages/api/answer/[id].ts @@ -7,18 +7,27 @@ interface AnswerData { answersData: { questionId: string; answer?: string }[]; } -export async function getSurveyData(surveyId: string) { - const survey = await prismadb.survey.findUnique({ - where: { - id: surveyId, - }, - include: { - questions: true, - answers: false, - }, - }); +export async function getSurveyData(surveyId: string, userId?: string) { + try { + const survey = await prismadb.survey.findFirst({ + where: { + id: surveyId, + userId, + }, + include: { + questions: { + orderBy: { + order: 'asc', + }, + }, + answers: false, + }, + }); - return survey; + return survey; + } catch (error) { + return null; + } } const isAnswerDataValid = (answerData: AnswerData) => { diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts index c691c369..b63ab164 100644 --- a/src/pages/api/auth/[...nextauth].ts +++ b/src/pages/api/auth/[...nextauth].ts @@ -60,9 +60,20 @@ export const authOptions: NextAuthOptions = { }, callbacks: { session: async ({ session, token }) => { - if (session?.user) { + if (session?.user && token.uid) { + const isSessionValid = await prismadb.user.findUnique({ + where: { + id: token.uid as string, + }, + }); + + if (!isSessionValid) { + return Promise.reject(new Error('Session not valid')); + } + session.user.id = token.uid; } + return session; }, jwt: async ({ user, token }) => { diff --git a/src/pages/api/survey/[id].ts b/src/pages/api/survey/[id].ts index ad0f2f5c..36384ca6 100644 --- a/src/pages/api/survey/[id].ts +++ b/src/pages/api/survey/[id].ts @@ -2,6 +2,7 @@ import { NextApiRequest, NextApiResponse } from 'next'; import prismadb from '../../../../lib/prismadb'; import serverAuth from '../../../../lib/serverAuth'; +import { SurveyData, isSurveyValid } from '.'; export enum SurveyActionTypes { UPDATE_ACTIVE = 'UPDATE_ACTIVE', @@ -11,26 +12,35 @@ interface SurveyPatchPayloadI { } export async function getSurveyWithAnswers(surveyId: string, userId: string) { - const survey = await prismadb.survey.findFirst({ - where: { - id: surveyId, - userId: userId, - }, - include: { - questions: true, - answers: { - include: { - answerData: true, + try { + const survey = await prismadb.survey.findFirst({ + where: { + id: surveyId, + userId: userId, + }, + include: { + questions: { + orderBy: { + order: 'asc', + }, }, - orderBy: { - createdAt: 'desc', + answers: { + include: { + answerData: true, + }, + orderBy: { + createdAt: 'desc', + }, }, }, - }, - }); + }); - return survey; + return survey; + } catch (error) { + return null; + } } + export async function updateSurveyActiveStatus({ surveyId, isActive, @@ -46,6 +56,7 @@ export async function updateSurveyActiveStatus({ }); return survey; } + export async function handlePatch(req: NextApiRequest, res: NextApiResponse) { const surveyId = String(req.query.id); const { actionType } = req.body as SurveyPatchPayloadI; @@ -74,6 +85,7 @@ export async function handlePatch(req: NextApiRequest, res: NextApiResponse) { } } } + export default async function handler( req: NextApiRequest, res: NextApiResponse @@ -81,6 +93,7 @@ export default async function handler( try { const requestMethod = req.method; const session = await serverAuth(req, res); + const userId = session.currentUser.id; const { id } = req.query; switch (requestMethod) { @@ -92,7 +105,6 @@ export default async function handler( return res.status(200).json(survey); } - case 'DELETE': { const survey = await prismadb.survey.findFirst({ where: { @@ -116,6 +128,72 @@ export default async function handler( case 'PATCH': { return handlePatch(req, res); } + case 'PUT': { + const { + title, + description, + questions, + oneQuestionPerStep, + displayTitle, + } = req.body as SurveyData; + if (!isSurveyValid(req.body)) { + return res.status(400).end(); + } + + const surveyFound = await prismadb.survey.findFirst({ + where: { id: id as string, userId }, + }); + + if (!surveyFound?.id) { + return res.status(404).end(); + } + + const surveyQuestions = await prismadb.question.findMany({ + where: { + surveyId: id as string, + }, + }); + + const newQuestions = []; + + surveyQuestions.forEach(async (question) => { + const foundQuestionIndex = questions.findIndex( + (q) => q.id === question.id + ); + + if (foundQuestionIndex === -1) return; + + const questionFound = questions[foundQuestionIndex]; + + const newQuestion = await prismadb.question.update({ + where: { + id: question.id, + }, + data: { + title: questionFound.title, + description: questionFound.description, + isRequired: questionFound.isRequired, + order: foundQuestionIndex, + }, + }); + + newQuestions.push(newQuestion); + }); + + const survey = await prismadb.survey.update({ + where: { + id: id as string, + }, + data: { + title, + description, + oneQuestionPerStep, + displayTitle, + }, + }); + + return res.status(200).json({ id: survey.id }); + } default: return res.status(405).end(); } diff --git a/src/pages/api/survey/index.ts b/src/pages/api/survey/index.ts index b745c206..6dd765df 100644 --- a/src/pages/api/survey/index.ts +++ b/src/pages/api/survey/index.ts @@ -11,7 +11,7 @@ import { MIN_QUESTIONS, } from 'shared/constants/surveysConfig'; -interface SurveyData { +export interface SurveyData { title: string; description: string; questions: Question[]; @@ -38,7 +38,7 @@ export async function getAllUserSurveys(userId: string) { return surveys; } -const isSurveyValid = (survey: SurveyData) => { +export const isSurveyValid = (survey: SurveyData) => { if ( survey.title.trim() === '' || survey.title.length > MAX_TITLE_LENGTH || @@ -70,8 +70,13 @@ export default async function handler( return res.status(200).json({ surveys }); } case 'POST': { - const { title, description, questions, oneQuestionPerStep, displayTitle } = - req.body as SurveyData; + const { + title, + description, + questions, + oneQuestionPerStep, + displayTitle, + } = req.body as SurveyData; if (!isSurveyValid(req.body)) { return res.status(400).end(); @@ -86,12 +91,13 @@ export default async function handler( oneQuestionPerStep, displayTitle, questions: { - create: questions.map((question) => ({ + create: questions.map((question, index) => ({ type: question.type, title: question.title, description: question.description, options: question.options, isRequired: question.isRequired, + order: index, })), }, }, diff --git a/src/pages/survey/answer/[surveyId]/index.tsx b/src/pages/survey/answer/[surveyId]/index.tsx index 6bc476b6..fd935f7d 100644 --- a/src/pages/survey/answer/[surveyId]/index.tsx +++ b/src/pages/survey/answer/[surveyId]/index.tsx @@ -1,5 +1,11 @@ -import { RefreshIcon, ShareIcon, TrashIcon } from '@heroicons/react/outline'; import Toggle from 'shared/components/Toggle/Toggle'; +import { + PencilIcon, + RefreshIcon, + ShareIcon, + TrashIcon, +} from '@heroicons/react/outline'; + import Head from 'next/head'; import withAnimation from 'shared/HOC/withAnimation'; import withProtectedRoute from 'shared/HOC/withProtectedRoute'; @@ -16,6 +22,7 @@ import ResultComponent from 'features/surveys/components/ResultsComponents/Resul import useModal from 'features/surveys/hooks/useModal'; import DeleteSurveyModal from 'features/surveys/components/DeleteSurveyModal/DeleteSurveyModal'; import ShareSurveyModal from 'features/surveys/components/ShareSurveryModal/ShareSurveyModal'; +import { useRouter } from 'next/router'; export async function getServerSideProps(context: NextPageContext) { const session = await getSession(context); @@ -77,6 +84,12 @@ function SurveyResultsPage({ const { t } = useTranslation('surveyAnswer'); + const router = useRouter(); + + const handleEditSurvey = () => { + router.push(`/survey/create/${surveyId}`); + }; + return ( <> @@ -100,6 +113,13 @@ function SurveyResultsPage({ />
+ - ) : ( - - )} +
+ {questions.length < MAX_QUESTIONS && !isEditMode && ( + + )} + + {user ? ( +
+ {isEditMode && ( + + )} + +
+ ) : ( + + )} +
setShowPassword(!showPassword)} > - {showPassword ? : } + {showPassword ? ( + + ) : ( + + )} )}
diff --git a/src/shared/components/StyledDialog/StyledDialog.tsx b/src/shared/components/StyledDialog/StyledDialog.tsx index 18eda005..d55c4c1b 100644 --- a/src/shared/components/StyledDialog/StyledDialog.tsx +++ b/src/shared/components/StyledDialog/StyledDialog.tsx @@ -50,19 +50,22 @@ export default function StyledDialog({ contentClassName )} > - - {title} - - + {!!title && ( + + {title} + + + )} + {content}