diff --git a/apps/form-builder/src/app/forms/[formId]/page.tsx b/apps/form-builder/src/app/forms/[formId]/page.tsx index c6a1235..01163ec 100644 --- a/apps/form-builder/src/app/forms/[formId]/page.tsx +++ b/apps/form-builder/src/app/forms/[formId]/page.tsx @@ -1,7 +1,7 @@ import { getFormById } from '@/server/actions/forms'; import * as React from 'react'; import FormTabs from '@/components/FormTabs'; - +import { Typography } from '@mui/material'; export default async function FormPage({ params, }: { @@ -13,15 +13,26 @@ export default async function FormPage({ return; } + const updatedAtDate: Date = new Date(form.updatedAt); + const createdAtDate: Date = new Date(form.createdAt); + return (
{/* display properties of a form */} -

Title: {form.title}

-

Description: {form.description}

-

Responder Type: {form.responderType}

-

Anonymous: {form.isAnonymous.toLocaleString()}

-

Created At: {form.createdAt.toLocaleString()}

-

Updated At: {form.updatedAt.toLocaleString()}

+ Title: {form.title} + Description: {form.description} + + Responder Type: {form.responderType} + + + Anonymous: {form.isAnonymous.toLocaleString()} + + + Created At: {createdAtDate.toLocaleString()} + + + Last Updated At: {updatedAtDate.toLocaleString()} + {/* //added tabs and a button */}
diff --git a/apps/form-builder/src/app/forms/create/page.tsx b/apps/form-builder/src/app/forms/create/page.tsx index 7a6911b..516bf78 100644 --- a/apps/form-builder/src/app/forms/create/page.tsx +++ b/apps/form-builder/src/app/forms/create/page.tsx @@ -1,3 +1,10 @@ +import UpsertForm from '@/components/UpsertForm/UpsertForm'; +import { Box } from '@mui/material'; + export default async function NewFormPage() { - return

New form page

; + return ( + + + + ); } diff --git a/apps/form-builder/src/components/FileUploadOptions/FileUploadOptions.tsx b/apps/form-builder/src/components/FileUploadOptions/FileUploadOptions.tsx new file mode 100644 index 0000000..56dcd60 --- /dev/null +++ b/apps/form-builder/src/components/FileUploadOptions/FileUploadOptions.tsx @@ -0,0 +1,94 @@ +import { + Box, + Checkbox, + FormControl, + InputLabel, + ListItemText, + MenuItem, + OutlinedInput, + Select, + SelectChangeEvent, + TextField, +} from '@mui/material'; +import React from 'react'; + +export default function FileUploadOptions() { + const [value, setValue] = React.useState('KB'); + + const handleChange = (event: SelectChangeEvent) => { + setValue(event.target.value); + }; + + const fileTypes = [ + 'PDF', + 'TXT', + 'DOC/DOCX', + 'XLS', + 'XLSX', + 'HTML', + 'PPT/PPTX', + 'JPEG', + 'PNG', + 'GIF', + 'MP4', + 'AVI', + 'MOV', + 'MP3', + ]; + + const [fileType, setFileType] = React.useState([]); + + const handleFileChange = (event: SelectChangeEvent) => { + const { + target: { value }, + } = event; + setFileType(typeof value === 'string' ? value.split(',') : value); + }; + + return ( + + + + File Type + + + + + + + + + + File Size + + + + + ); +} diff --git a/apps/form-builder/src/components/FormAnalytics/index.tsx b/apps/form-builder/src/components/FormAnalytics/index.tsx index 91721a1..dd1353a 100644 --- a/apps/form-builder/src/components/FormAnalytics/index.tsx +++ b/apps/form-builder/src/components/FormAnalytics/index.tsx @@ -34,7 +34,18 @@ export default function FormAnalytics(props: FormAnalyticsProps) { {props.question.title} - {props.responses.reduce(numAnswers, 0)} Responses + + {props.responses.reduce(numAnswers, 0) === 1 && ( + + {props.responses.reduce(numAnswers, 0)} Response + + )} + {props.responses.reduce(numAnswers, 0) > 1 && ( + + {props.responses.reduce(numAnswers, 0)} Responses + + )} + {questionType === 'Text' && ( void; +} + +export default function FormQuestionForm({ + formData, + onChange, +}: CreateFormQuestionProps) { + const [value, setValue] = React.useState(''); + + const [questionSettings, setQuestionSettings] = React.useState(''); + + const handleChange = (event: SelectChangeEvent) => { + setValue(event.target.value); + setQuestionSettings(event.target.value); + }; + + return ( + + onChange({ ...formData, title: e.target.value })} + /> + onChange({ ...formData, description: e.target.value })} + /> + + onChange({ ...formData, isRequired: e.target.checked }) + } + /> + } + label="Required?" + /> + + Question Type + + + + {questionSettings === 'Multiple' && ( + + )} + {questionSettings === 'Numeric' && } + {questionSettings === 'Text' && } + {questionSettings === 'FileUpload' && ( + + )} + + + ); +} diff --git a/apps/form-builder/src/components/FormQuestions/index.tsx b/apps/form-builder/src/components/FormQuestions/index.tsx new file mode 100644 index 0000000..5ee3ab8 --- /dev/null +++ b/apps/form-builder/src/components/FormQuestions/index.tsx @@ -0,0 +1,73 @@ +'use client'; +import AddIcon from '@mui/icons-material/Add'; +import { Box, Divider, IconButton, Typography } from '@mui/material'; +import React, { useState } from 'react'; +import FormQuestionForm, { FormQuestionFormData } from '../FormQuestionForm'; +import { + FormQuestionResponse, + FormResponse, +} from '@hack4impact-utk/internal-models'; + +interface FormQuestionsProps { + form?: FormResponse; +} + +function FormQuestionResponsesToFormQuestionFormData( + formQuestionResponses: FormQuestionResponse[] +) { + return formQuestionResponses.map( + (formQuestionResponse): FormQuestionFormData => { + return { + title: formQuestionResponse.title, + description: formQuestionResponse.description, + isRequired: formQuestionResponse.isRequired, + }; + } + ); +} + +export default function FormQuestions(props: FormQuestionsProps) { + const [questionsFormData, setQuestionsFormData] = useState< + FormQuestionFormData[] + >(FormQuestionResponsesToFormQuestionFormData(props.form?.questions || [])); + + function addFormQuestion() { + const newQuestion: FormQuestionFormData = { + isRequired: false, + title: '', + description: '', + }; + setQuestionsFormData((currQuestions) => [...currQuestions, newQuestion]); + } + + function onFormQuestionChange(index: number, newData: FormQuestionFormData) { + setQuestionsFormData((currFormDataArr) => + currFormDataArr.map((formData, i) => (i === index ? newData : formData)) + ); + } + + return ( + + {questionsFormData.map((question, i) => ( + + + Question {i + 1} + + { + onFormQuestionChange(i, formData); + }} + /> + + + ))} + + Add new question + + + + + + ); +} diff --git a/apps/form-builder/src/components/FormSubmissions/MultipleChoiceQuestionAnalytics/Index.tsx b/apps/form-builder/src/components/FormSubmissions/MultipleChoiceQuestionAnalytics/Index.tsx index a33f921..a933040 100644 --- a/apps/form-builder/src/components/FormSubmissions/MultipleChoiceQuestionAnalytics/Index.tsx +++ b/apps/form-builder/src/components/FormSubmissions/MultipleChoiceQuestionAnalytics/Index.tsx @@ -10,6 +10,8 @@ import { ListItemText, Typography, } from '@mui/material'; +import PieChartAnalytics from '../PieChartAnalytics'; +import BarGraphAnalytics from '../BarGraphAnalytics/Index'; // Takes in the question and response as props interface Props { @@ -24,20 +26,41 @@ export default function MultipleChoiceQuestionAnalytics({ }: Props) { // Gets the specific submission to a question from each response from a single form function getAnswers() { - let array = []; + const otherAnswers = []; + const allAnswers: string[] = []; // For a given form, the findIndex function will find the specific question from the list of questionResponses for (let i = 0; i < responses.length; i++) { - let num = responses[i].questionResponses.findIndex( + const num = responses[i].questionResponses.findIndex( (element) => element.question._id.toString() === question._id.toString() ); + console.log(num); if (num === -1) { continue; } else { // Get the answer and options - let answer = responses[i].questionResponses[num].answer; - let options = + const answer = responses[i].questionResponses[num].answer; + const options = responses[i].questionResponses[num].question.multipleChoiceOptions ?.options; + console.log('starting a new response'); + if (Array.isArray(answer)) { + for (const choice of answer) { + console.log(choice, options); + for (let j = 0; j < options!.length; j++) { + if (options?.findIndex((option) => option == choice) === -1) { + array.push(choice); + } + } + } + } else { + // Check if the answer is not in the options array + let answerInOptions = false; + for (let j = 0; j < options!.length; j++) { + if (options![j] == answer) { + answerInOptions = true; + break; + } + allAnswers.push(answer as string); // Check if the answer is not in the options array let answerInOptions = false; @@ -46,35 +69,46 @@ export default function MultipleChoiceQuestionAnalytics({ answerInOptions = true; break; } - } // If the answer is not in the options, add it to the array if (!answerInOptions) { - array.push(answer); + otherAnswers.push(answer); } } } - return array; + return [otherAnswers, allAnswers]; } - + const qType = question.multipleChoiceOptions!.choiceType; // Displays the submissions in a list format with scroll functionality return ( <> - - {'Choice Type: '} - {question.multipleChoiceOptions?.choiceType} - - - - {getAnswers().map((submission, index) => ( - - - {submission} - - - ))} - - + {`${qType} Select`} + {qType === 'Single' && ( + + )} + {qType === 'Multiple' && ( + + )} + {question.multipleChoiceOptions!.allowOther && qType !== 'Multiple' && ( + + {'"Other"'} Answers: + + {getAnswers()[0].map((submission, index) => ( + + + {submission} + + + ))} + + + )} ); } diff --git a/apps/form-builder/src/components/FormSubmissions/PieChartAnalytics.tsx b/apps/form-builder/src/components/FormSubmissions/PieChartAnalytics.tsx index aac6ff9..82b0e1f 100644 --- a/apps/form-builder/src/components/FormSubmissions/PieChartAnalytics.tsx +++ b/apps/form-builder/src/components/FormSubmissions/PieChartAnalytics.tsx @@ -4,7 +4,7 @@ import React from 'react'; interface Props { choices: string[]; - answers: string[][]; + answers: string[]; } //Function to display pie chart @@ -20,19 +20,21 @@ export default function PieChartAnalytics({ choices, answers }: Props) { let totalAnswers = 0; // Loop through each answer and count the occurrences of each choice - answers.forEach((answerSet) => { - answerSet.forEach((answer) => { - // Check if the answer is one of the choices - if (choiceCounts.hasOwnProperty(answer)) { - choiceCounts[answer]++; - } else { - // Increment the count for "Other" - choiceCounts['Other']++; - } - totalAnswers++; - }); + answers.forEach((answer) => { + // Check if the answer is one of the choices + if (choiceCounts.hasOwnProperty(answer)) { + choiceCounts[answer]++; + } else { + // Increment the count for "Other" + choiceCounts['Other']++; + } + totalAnswers++; }); + if (choiceCounts['Other'] === 0) { + delete choiceCounts['Other']; + } + // Extract the counts to an array to use as data for the BarChart, including the count for "Other" const data = Object.keys(choiceCounts).map((key) => ({ label: key, @@ -62,6 +64,7 @@ export default function PieChartAnalytics({ choices, answers }: Props) { }} //Changes size of pie chart height={600} + width={500} /> ); } diff --git a/apps/form-builder/src/components/FormTabs/index.tsx b/apps/form-builder/src/components/FormTabs/index.tsx index 613521f..528891e 100644 --- a/apps/form-builder/src/components/FormTabs/index.tsx +++ b/apps/form-builder/src/components/FormTabs/index.tsx @@ -2,12 +2,16 @@ import { Box, Tabs, Tab, Button, Divider, Typography } from '@mui/material'; import React from 'react'; import FormSubmissionTable from '../FormSubmissions/FormSubmissionTable'; -import { FormResponse } from '@hack4impact-utk/internal-models'; +import FormQuestions from '../FormQuestions'; import FormAnalytics from '../FormAnalytics'; -import { FormQuestionResponse } from '@hack4impact-utk/internal-models'; +import { + FormQuestionResponse, + FormResponse, +} from '@hack4impact-utk/internal-models'; +import FormSettings from '../FormSettings'; -interface FormsTabProp { - form: FormResponse; +interface FormTabsProps { + form?: FormResponse; } interface TabPanelProps { @@ -31,12 +35,11 @@ function CustomTabPanel(props: TabPanelProps) { ); } -export default function FormTabs(props: FormsTabProp) { +export default function FormTabs(props: FormTabsProps) { const [value, setValue] = React.useState(0); const handleChange = (event: React.SyntheticEvent, newValue: number) => { setValue(newValue); }; - return ( <> @@ -51,37 +54,42 @@ export default function FormTabs(props: FormsTabProp) { {/*Submissions*/} {/*Analytics*/} - {props.form.questions.map( - (question: FormQuestionResponse, i: number) => ( - - - Question {i + 1} - - - + {props.form && + props.form.questions.map( + (question: FormQuestionResponse, i: number) => ( + + + Question {i + 1} + + + + + {i !== props.form!.questions.length - 1 && ( + + )} - {i !== props.form.questions.length - 1 && ( - - )} - - ) - )} + ) + )} {/*Questions*/} - + + + {/*Settings*/} - + + + + + + ); +} diff --git a/apps/form-builder/src/components/NumericOptions.tsx b/apps/form-builder/src/components/NumericOptions/NumericOptions.tsx similarity index 100% rename from apps/form-builder/src/components/NumericOptions.tsx rename to apps/form-builder/src/components/NumericOptions/NumericOptions.tsx diff --git a/packages/internal-models/src/db/models/FormBuilder/FormQuestion.ts b/packages/internal-models/src/db/models/FormBuilder/FormQuestion.ts index 2258010..23158c2 100644 --- a/packages/internal-models/src/db/models/FormBuilder/FormQuestion.ts +++ b/packages/internal-models/src/db/models/FormBuilder/FormQuestion.ts @@ -52,7 +52,6 @@ export const FormQuestionSchema = new Schema( }, supportedFileTypes: { type: Schema.Types.String, - enum: fileTypes, required: true, }, }, @@ -71,7 +70,6 @@ export const FormQuestionSchema = new Schema( }, choiceType: { type: Schema.Types.String, - enum: multipleChoiceTypes, required: true, }, },