From aa648f9b2d5fca680760a16f0c09950e727db3d9 Mon Sep 17 00:00:00 2001 From: Matthias Nannt Date: Fri, 29 Jul 2022 00:21:32 +0200 Subject: [PATCH] Feature/add question types (#9) * add new question types: email, website, phone, number & multiple choice --- README.md | 4 +- components/editorjs/Editor.tsx | 10 + components/editorjs/tools/EmailQuestion.tsx | 135 +++ .../editorjs/tools/MultipleChoiceQuestion.tsx | 130 +++ .../tools/MultipleChoiceQuestionComponent.tsx | 168 +++ components/editorjs/tools/NumberQuestion.tsx | 128 +++ components/editorjs/tools/PhoneQuestion.tsx | 135 +++ components/editorjs/tools/TextQuestion.tsx | 2 +- components/editorjs/tools/WebsiteQuestion.tsx | 138 +++ components/form/FormCode.tsx | 80 +- components/frontend/App.tsx | 115 +- components/results/ResultsSummary.tsx | 12 +- components/results/summary/BaseResults.tsx | 30 +- components/results/summary/ChoiceResults.tsx | 54 + components/results/summary/TextResults.tsx | 4 +- lib/submissionSessions.ts | 28 +- lib/types.ts | 15 +- package.json | 6 +- pages/forms/[id]/results/summary.tsx | 1 - yarn.lock | 1002 +++++++++-------- 20 files changed, 1628 insertions(+), 569 deletions(-) create mode 100644 components/editorjs/tools/EmailQuestion.tsx create mode 100644 components/editorjs/tools/MultipleChoiceQuestion.tsx create mode 100644 components/editorjs/tools/MultipleChoiceQuestionComponent.tsx create mode 100644 components/editorjs/tools/NumberQuestion.tsx create mode 100644 components/editorjs/tools/PhoneQuestion.tsx create mode 100644 components/editorjs/tools/WebsiteQuestion.tsx create mode 100644 components/results/summary/ChoiceResults.tsx diff --git a/README.md b/README.md index 36f82f484c..1ef44aedd1 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ yarn dev ``` -**You can now access the app on [https://localhost:3000](https://localhost:3000)** +**You can now access the app on [https://localhost:3000](https://localhost:3000)**. You will be automatically redirected to the login. To use your local installation of snoopForms, create a new account. ## Deployment @@ -117,7 +117,7 @@ docker compose up -d ``` -You can now access the app on [https://localhost:3000](https://localhost:3000) +You can now access the app on [https://localhost:3000](https://localhost:3000). You will be automatically redirected to the login. To use your local installation of snoopForms, create a new account. ## Contributing diff --git a/components/editorjs/Editor.tsx b/components/editorjs/Editor.tsx index dcfe8ba14a..57a489449b 100644 --- a/components/editorjs/Editor.tsx +++ b/components/editorjs/Editor.tsx @@ -8,8 +8,13 @@ import { Fragment, useCallback, useEffect } from "react"; import { toast } from "react-toastify"; import { persistNoCodeForm, useNoCodeForm } from "../../lib/noCodeForm"; import Loading from "../Loading"; +import EmailQuestion from "./tools/EmailQuestion"; import PageTransition from "./tools/PageTransition"; +import MultipleChoiceQuestion from "./tools/MultipleChoiceQuestion"; import TextQuestion from "./tools/TextQuestion"; +import WebsiteQuestion from "./tools/WebsiteQuestion"; +import PhoneQuestion from "./tools/PhoneQuestion"; +import NumberQuestion from "./tools/NumberQuestion"; interface EditorProps { id: string; @@ -85,6 +90,11 @@ const Editor = ({ defaultBlock: "paragraph", tools: { textQuestion: TextQuestion, + emailQuestion: EmailQuestion, + multipleChoiceQuestion: MultipleChoiceQuestion, + numberQuestion: NumberQuestion, + phoneQuestion: PhoneQuestion, + websiteQuestion: WebsiteQuestion, pageTransition: PageTransition, paragraph: { class: Paragraph, diff --git a/components/editorjs/tools/EmailQuestion.tsx b/components/editorjs/tools/EmailQuestion.tsx new file mode 100644 index 0000000000..e38c00a52c --- /dev/null +++ b/components/editorjs/tools/EmailQuestion.tsx @@ -0,0 +1,135 @@ +import { API, BlockTool, BlockToolData, ToolConfig } from "@editorjs/editorjs"; +import { MailIcon } from "@heroicons/react/solid"; +import ReactDOM from "react-dom"; + +//styles imports in angular.json +interface EmailQuestionData extends BlockToolData { + label: string; + placeholder: string; + required: boolean; +} + +export default class EmailQuestion implements BlockTool { + settings: { name: string; icon: string }[]; + api: API; + data: any; + wrapper: undefined | HTMLElement; + + static get toolbox(): { icon: string; title?: string } { + return { + icon: ` + + `, + title: "Email Question", + }; + } + + constructor({ + data, + }: { + api: API; + config?: ToolConfig; + data?: EmailQuestionData; + }) { + this.wrapper = undefined; + this.settings = [ + { + name: "required", + icon: ``, + }, + ]; + this.data = data; + this.data = { + label: data.label || "", + placeholder: data.placeholder || "your email", + required: data.required !== undefined ? data.required : true, + }; + } + + save(block: HTMLDivElement) { + return { + ...this.data, + label: ( + block.firstElementChild.firstElementChild + .firstElementChild as HTMLInputElement + ).value, + placeholder: ( + block.firstElementChild.lastElementChild + .lastElementChild as HTMLInputElement + ).value, + }; + } + + renderSettings(): HTMLElement { + const wrapper = document.createElement("div"); + + this.settings.forEach((tune) => { + let button = document.createElement("div"); + + button.classList.add("cdx-settings-button"); + button.classList.toggle( + "cdx-settings-button--active", + this.data[tune.name] + ); + button.innerHTML = tune.icon; + wrapper.appendChild(button); + + button.addEventListener("click", () => { + this._toggleTune(tune.name); + button.classList.toggle("cdx-settings-button--active"); + }); + }); + + return wrapper; + } + + /** + * @private + * Click on the Settings Button + * @param {string} tune — tune name from this.settings + */ + _toggleTune(tune) { + this.wrapper.classList.toggle(tune.name, !!this.data[tune.name]); + + if (tune === "required") { + this.data.required = !this.data.required; + this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this + .data.required + ? "*" + : ""; + } + } + + render(): HTMLElement { + this.wrapper = document.createElement("div"); + const toolView = ( +
+
+ +
+ * +
+
+
+
+
+ +
+
+ ); + ReactDOM.render(toolView, this.wrapper); + return this.wrapper; + } +} diff --git a/components/editorjs/tools/MultipleChoiceQuestion.tsx b/components/editorjs/tools/MultipleChoiceQuestion.tsx new file mode 100644 index 0000000000..55faa2c765 --- /dev/null +++ b/components/editorjs/tools/MultipleChoiceQuestion.tsx @@ -0,0 +1,130 @@ +import { API, BlockTool, BlockToolData, ToolConfig } from "@editorjs/editorjs"; +import { default as React } from "react"; +import ReactDOM from "react-dom"; +import MultipleChoiceQuestionComponent from "./MultipleChoiceQuestionComponent"; + +interface MultipleChoiceQuestionData extends BlockToolData { + label: string; + options: string[]; + required: boolean; +} + +export default class MultipleChoiceQuestion implements BlockTool { + settings: { name: string; icon: string }[]; + api: API; + config: ToolConfig; + data: any; + readOnly: boolean; + block: any; + wrapper: undefined | HTMLElement; + nodes: { holder: HTMLElement }; + + static get toolbox(): { icon: string; title?: string } { + return { + icon: ` + + `, + title: "Multiple Choice Question", + }; + } + + static get isReadOnlySupported() { + return true; + } + + constructor({ + data, + config, + api, + readOnly, + }: { + api: API; + config?: ToolConfig; + data?: MultipleChoiceQuestionData; + block?: any; + readOnly: boolean; + }) { + this.api = api; + this.config = config; + this.readOnly = readOnly; + this.data = { + label: data.label || "", + options: data.options || [], + required: data.required || false, + multipleChoice: data.multipleChoice || false, + }; + this.settings = [ + { + name: "required", + icon: ``, + }, + ]; + + this.nodes = { + holder: null, + }; + } + + renderSettings(): HTMLElement { + const wrapper = document.createElement("div"); + + this.settings.forEach((tune) => { + let button = document.createElement("div"); + + button.classList.add("cdx-settings-button"); + button.classList.toggle( + "cdx-settings-button--active", + this.data[tune.name] + ); + button.innerHTML = tune.icon; + wrapper.appendChild(button); + + button.addEventListener("click", () => { + this._toggleTune(tune.name); + button.classList.toggle("cdx-settings-button--active"); + }); + }); + + return wrapper; + } + + /** + * @private + * Click on the Settings Button + * @param {string} tune — tune name from this.settings + */ + _toggleTune(tune) { + //this.wrapper.classList.toggle(tune.name, !!this.data[tune.name]); + + if (tune === "required") { + this.data.required = !this.data.required; + } + } + + render() { + const rootNode = document.createElement("div"); + //rootNode.setAttribute("class", this.CSS.wrapper); + this.nodes.holder = rootNode; + + const onDataChange = (newData) => { + this.data = { + ...newData, + }; + }; + + ReactDOM.render( + , + rootNode + ); + + return this.nodes.holder; + } + + save() { + return this.data; + } +} diff --git a/components/editorjs/tools/MultipleChoiceQuestionComponent.tsx b/components/editorjs/tools/MultipleChoiceQuestionComponent.tsx new file mode 100644 index 0000000000..c51bc99e91 --- /dev/null +++ b/components/editorjs/tools/MultipleChoiceQuestionComponent.tsx @@ -0,0 +1,168 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { Switch } from "@headlessui/react"; +import { TrashIcon } from "@heroicons/react/solid"; +import { default as React } from "react"; +import { v4 as uuidv4 } from "uuid"; +import { classNames } from "../../../lib/utils"; + +const DEFAULT_INITIAL_DATA = () => { + return { + label: "", + required: false, + multipleChoice: false, + options: [ + { + id: uuidv4(), + label: "", + }, + ], + }; +}; + +const SingleChoiceQuestion = (props) => { + const [choiceData, setChoiceData] = React.useState( + props.data.options.length > 0 ? props.data : DEFAULT_INITIAL_DATA + ); + + const updateData = (newData) => { + setChoiceData(newData); + if (props.onDataChange) { + // Inform editorjs about data change + props.onDataChange(newData); + } + }; + + const onAddOption = () => { + const newData = { + ...choiceData, + }; + newData.options.push({ + id: uuidv4(), + label: "", + }); + updateData(newData); + }; + + const onDeleteOption = (optionIdx) => { + const newData = { + ...choiceData, + }; + newData.options.splice(optionIdx, 1); + updateData(newData); + }; + + const onInputChange = (fieldName) => { + return (e) => { + const newData = { + ...choiceData, + }; + newData[fieldName] = e.currentTarget.value; + updateData(newData); + }; + }; + + const onOptionChange = (index, fieldName) => { + return (e) => { + const newData = { + ...choiceData, + }; + newData.options[index][fieldName] = e.currentTarget.value; + updateData(newData); + }; + }; + + return ( +
+
+ + {choiceData.required && ( +
+ * +
+ )} +
+
+ {choiceData.options.map((option, optionIdx) => ( +
+ + + {optionIdx !== 0 && ( + + )} +
+ ))} +
+
+ + + { + const newData = { + ...choiceData, + }; + newData.multipleChoice = !newData.multipleChoice; + updateData(newData); + }} + className={classNames( + choiceData.multipleChoice ? "bg-red-600" : "bg-gray-200", + "relative inline-flex flex-shrink-0 h-4 w-7 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" + )} + > + + + + Multiple Selection{" "} + + {/* (Save 10%) */} + + +
+
+ ); +}; + +export default SingleChoiceQuestion; diff --git a/components/editorjs/tools/NumberQuestion.tsx b/components/editorjs/tools/NumberQuestion.tsx new file mode 100644 index 0000000000..7836e80cb1 --- /dev/null +++ b/components/editorjs/tools/NumberQuestion.tsx @@ -0,0 +1,128 @@ +import { API, BlockTool, BlockToolData, ToolConfig } from "@editorjs/editorjs"; +import ReactDOM from "react-dom"; + +//styles imports in angular.json +interface NumberQuestionData extends BlockToolData { + label: string; + placeholder: string; + required: boolean; +} + +export default class NumberQuestion implements BlockTool { + settings: { name: string; icon: string }[]; + api: API; + data: any; + wrapper: undefined | HTMLElement; + + static get toolbox(): { icon: string; title?: string } { + return { + icon: ` + + `, + title: "Number Question", + }; + } + + constructor({ + data, + }: { + api: API; + config?: ToolConfig; + data?: NumberQuestionData; + }) { + this.wrapper = undefined; + this.settings = [ + { + name: "required", + icon: ``, + }, + ]; + this.data = data; + this.data = { + label: data.label || "", + placeholder: data.placeholder || "", + required: data.required !== undefined ? data.required : true, + }; + } + + save(block: HTMLDivElement) { + return { + ...this.data, + label: ( + block.firstElementChild.firstElementChild + .firstElementChild as HTMLInputElement + ).value, + placeholder: ( + block.firstElementChild.lastElementChild as HTMLInputElement + ).value, + }; + } + + renderSettings(): HTMLElement { + const wrapper = document.createElement("div"); + + this.settings.forEach((tune) => { + let button = document.createElement("div"); + + button.classList.add("cdx-settings-button"); + button.classList.toggle( + "cdx-settings-button--active", + this.data[tune.name] + ); + button.innerHTML = tune.icon; + wrapper.appendChild(button); + + button.addEventListener("click", () => { + this._toggleTune(tune.name); + button.classList.toggle("cdx-settings-button--active"); + }); + }); + + return wrapper; + } + + /** + * @private + * Click on the Settings Button + * @param {string} tune — tune name from this.settings + */ + _toggleTune(tune) { + this.wrapper.classList.toggle(tune.name, !!this.data[tune.name]); + + if (tune === "required") { + this.data.required = !this.data.required; + this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this + .data.required + ? "*" + : ""; + } + } + + render(): HTMLElement { + this.wrapper = document.createElement("div"); + const toolView = ( +
+
+ +
+ * +
+
+ +
+ ); + ReactDOM.render(toolView, this.wrapper); + return this.wrapper; + } +} diff --git a/components/editorjs/tools/PhoneQuestion.tsx b/components/editorjs/tools/PhoneQuestion.tsx new file mode 100644 index 0000000000..dda34c5401 --- /dev/null +++ b/components/editorjs/tools/PhoneQuestion.tsx @@ -0,0 +1,135 @@ +import { API, BlockTool, BlockToolData, ToolConfig } from "@editorjs/editorjs"; +import { PhoneIcon } from "@heroicons/react/solid"; +import ReactDOM from "react-dom"; + +//styles imports in angular.json +interface PhoneQuestionData extends BlockToolData { + label: string; + placeholder: string; + required: boolean; +} + +export default class PhoneQuestion implements BlockTool { + settings: { name: string; icon: string }[]; + api: API; + data: any; + wrapper: undefined | HTMLElement; + + static get toolbox(): { icon: string; title?: string } { + return { + icon: ` + + `, + title: "Phone Question", + }; + } + + constructor({ + data, + }: { + api: API; + config?: ToolConfig; + data?: PhoneQuestionData; + }) { + this.wrapper = undefined; + this.settings = [ + { + name: "required", + icon: ``, + }, + ]; + this.data = data; + this.data = { + label: data.label || "", + placeholder: data.placeholder || "+1 (555) 987-6543", + required: data.required !== undefined ? data.required : true, + }; + } + + save(block: HTMLDivElement) { + return { + ...this.data, + label: ( + block.firstElementChild.firstElementChild + .firstElementChild as HTMLInputElement + ).value, + placeholder: ( + block.firstElementChild.lastElementChild + .lastElementChild as HTMLInputElement + ).value, + }; + } + + renderSettings(): HTMLElement { + const wrapper = document.createElement("div"); + + this.settings.forEach((tune) => { + let button = document.createElement("div"); + + button.classList.add("cdx-settings-button"); + button.classList.toggle( + "cdx-settings-button--active", + this.data[tune.name] + ); + button.innerHTML = tune.icon; + wrapper.appendChild(button); + + button.addEventListener("click", () => { + this._toggleTune(tune.name); + button.classList.toggle("cdx-settings-button--active"); + }); + }); + + return wrapper; + } + + /** + * @private + * Click on the Settings Button + * @param {string} tune — tune name from this.settings + */ + _toggleTune(tune) { + this.wrapper.classList.toggle(tune.name, !!this.data[tune.name]); + + if (tune === "required") { + this.data.required = !this.data.required; + this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this + .data.required + ? "*" + : ""; + } + } + + render(): HTMLElement { + this.wrapper = document.createElement("div"); + const toolView = ( +
+
+ +
+ * +
+
+
+
+
+ +
+
+ ); + ReactDOM.render(toolView, this.wrapper); + return this.wrapper; + } +} diff --git a/components/editorjs/tools/TextQuestion.tsx b/components/editorjs/tools/TextQuestion.tsx index 1c92c915a1..7f92b1d010 100644 --- a/components/editorjs/tools/TextQuestion.tsx +++ b/components/editorjs/tools/TextQuestion.tsx @@ -114,7 +114,7 @@ export default class TextQuestion implements BlockTool { diff --git a/components/editorjs/tools/WebsiteQuestion.tsx b/components/editorjs/tools/WebsiteQuestion.tsx new file mode 100644 index 0000000000..b54a380709 --- /dev/null +++ b/components/editorjs/tools/WebsiteQuestion.tsx @@ -0,0 +1,138 @@ +import { API, BlockTool, BlockToolData, ToolConfig } from "@editorjs/editorjs"; +import { GlobeAltIcon } from "@heroicons/react/solid"; +import ReactDOM from "react-dom"; + +//styles imports in angular.json +interface WebsiteQuestionData extends BlockToolData { + label: string; + placeholder: string; + required: boolean; +} + +export default class WebsiteQuestion implements BlockTool { + settings: { name: string; icon: string }[]; + api: API; + data: any; + wrapper: undefined | HTMLElement; + + static get toolbox(): { icon: string; title?: string } { + return { + icon: ` + + `, + title: "Website Question", + }; + } + + constructor({ + data, + }: { + api: API; + config?: ToolConfig; + data?: WebsiteQuestionData; + }) { + this.wrapper = undefined; + this.settings = [ + { + name: "required", + icon: ``, + }, + ]; + this.data = data; + this.data = { + label: data.label || "", + placeholder: data.placeholder || "https://", + required: data.required !== undefined ? data.required : true, + }; + } + + save(block: HTMLDivElement) { + return { + ...this.data, + label: ( + block.firstElementChild.firstElementChild + .firstElementChild as HTMLInputElement + ).value, + placeholder: ( + block.firstElementChild.lastElementChild + .lastElementChild as HTMLInputElement + ).value, + }; + } + + renderSettings(): HTMLElement { + const wrapper = document.createElement("div"); + + this.settings.forEach((tune) => { + let button = document.createElement("div"); + + button.classList.add("cdx-settings-button"); + button.classList.toggle( + "cdx-settings-button--active", + this.data[tune.name] + ); + button.innerHTML = tune.icon; + wrapper.appendChild(button); + + button.addEventListener("click", () => { + this._toggleTune(tune.name); + button.classList.toggle("cdx-settings-button--active"); + }); + }); + + return wrapper; + } + + /** + * @private + * Click on the Settings Button + * @param {string} tune — tune name from this.settings + */ + _toggleTune(tune) { + this.wrapper.classList.toggle(tune.name, !!this.data[tune.name]); + + if (tune === "required") { + this.data.required = !this.data.required; + this.wrapper.childNodes[0].childNodes[0].childNodes[1].textContent = this + .data.required + ? "*" + : ""; + } + } + + render(): HTMLElement { + this.wrapper = document.createElement("div"); + const toolView = ( +
+
+ +
+ * +
+
+
+
+
+ +
+
+ ); + ReactDOM.render(toolView, this.wrapper); + return this.wrapper; + } +} diff --git a/components/form/FormCode.tsx b/components/form/FormCode.tsx index 8ee6971b37..df7d0b5664 100644 --- a/components/form/FormCode.tsx +++ b/components/form/FormCode.tsx @@ -127,53 +127,53 @@ export default function FormCode({ formId }) { className="grid grid-cols-1 gap-5 mt-3 sm:gap-6 sm:grid-cols-2" > {libs.map((lib) => ( - -
  • + -
    - -
    -
    -
    -

    {lib.name}

    - {lib.comingSoon && ( -
    -

    coming soon

    -
    + lib.bgColor, + "flex-shrink-0 flex items-center justify-center w-20 text-white text-sm font-medium rounded-l-md" )} + > +
    -
    -
  • - +
    +
    +

    {lib.name}

    + {lib.comingSoon && ( +
    +

    coming soon

    +
    + )} +
    +
    + + + ))} diff --git a/components/frontend/App.tsx b/components/frontend/App.tsx index 84be0c3013..d87b4ba03a 100644 --- a/components/frontend/App.tsx +++ b/components/frontend/App.tsx @@ -1,3 +1,4 @@ +import { GlobeAltIcon, MailIcon, PhoneIcon } from "@heroicons/react/solid"; import { SnoopElement, SnoopForm, SnoopPage } from "@snoopforms/react"; import { useMemo } from "react"; import { trackPosthogEvent } from "../../lib/posthog"; @@ -43,11 +44,17 @@ export default function App({ id = "", formId, blocks, localOnly = false }) { localOnly={localOnly} className="w-full max-w-3xl mx-auto space-y-6" onSubmit={() => { - trackPosthogEvent("submitForm", { formId }); + if (!localOnly) { + trackPosthogEvent("submitForm", { formId }); + } }} > - {pages.map((page) => ( - + {pages.map((page, pageIdx) => ( + {page.blocks.map((block) => (
    {block.type === "paragraph" ? ( @@ -61,20 +68,81 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {

    {block.data.text}

    ) : null ) : block.type === "textQuestion" ? ( -
    - -
    + + ) : block.type === "emailQuestion" ? ( + } + classNames={{ + label: + "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate", + }} + required={block.data.required} + /> + ) : block.type === "multipleChoiceQuestion" && + block.data.multipleChoice ? ( + o.label)} + classNames={{ + label: + "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate", + }} + required={block.data.required} + /> + ) : block.type === "multipleChoiceQuestion" && + !block.data.multipleChoice ? ( + o.label)} + classNames={{ + label: + "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate", + }} + required={block.data.required} + /> + ) : block.type === "numberQuestion" ? ( + + ) : block.type === "phoneQuestion" ? ( + } + classNames={{ + label: + "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate", + }} + required={block.data.required} + /> ) : block.type === "submitButton" ? ( + ) : block.type === "websiteQuestion" ? ( + } + classNames={{ + label: + "mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate", + }} + required={block.data.required} + /> ) : null}
    ))} diff --git a/components/results/ResultsSummary.tsx b/components/results/ResultsSummary.tsx index a3efa88d71..ca29358ed4 100644 --- a/components/results/ResultsSummary.tsx +++ b/components/results/ResultsSummary.tsx @@ -10,6 +10,7 @@ import { timeSince } from "../../lib/utils"; import AnalyticsCard from "./AnalyticsCard"; import Loading from "../Loading"; import TextResults from "./summary/TextResults"; +import ChoiceResults from "./summary/ChoiceResults"; export default function ResultsSummary({ formId }) { const { submissionSessions, isLoadingSubmissionSessions } = @@ -83,8 +84,17 @@ export default function ResultsSummary({ formId }) { page.type === "form" && (
    {page.elements.map((element) => - element.type === "text" || element.type === "textarea" ? ( + [ + "email", + "number", + "phone", + "text", + "textarea", + "website", + ].includes(element.type) ? ( + ) : ["checkbox", "radio"].includes(element.type) ? ( + ) : null )}
    diff --git a/components/results/summary/BaseResults.tsx b/components/results/summary/BaseResults.tsx index f9fac4080a..969ed2b911 100644 --- a/components/results/summary/BaseResults.tsx +++ b/components/results/summary/BaseResults.tsx @@ -1,7 +1,27 @@ -import { CheckCircleIcon, MenuAlt1Icon } from "@heroicons/react/outline"; +import { + AtSymbolIcon, + CheckCircleIcon, + GlobeAltIcon, + HashtagIcon, + MenuAlt1Icon, + PhoneIcon, +} from "@heroicons/react/outline"; +import { IoMdRadioButtonOn } from "react-icons/io"; import { classNames } from "../../../lib/utils"; export const elementTypes = [ + { + type: "email", + icon: AtSymbolIcon, + }, + { + type: "number", + icon: HashtagIcon, + }, + { + type: "phone", + icon: PhoneIcon, + }, { type: "text", icon: MenuAlt1Icon, @@ -14,6 +34,14 @@ export const elementTypes = [ type: "checkbox", icon: CheckCircleIcon, }, + { + type: "radio", + icon: IoMdRadioButtonOn, + }, + { + type: "website", + icon: GlobeAltIcon, + }, ]; export const getElementTypeIcon = (type) => { diff --git a/components/results/summary/ChoiceResults.tsx b/components/results/summary/ChoiceResults.tsx new file mode 100644 index 0000000000..649e290ac3 --- /dev/null +++ b/components/results/summary/ChoiceResults.tsx @@ -0,0 +1,54 @@ +import { Chart } from "react-chartjs-2"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, +} from "chart.js"; +import BaseResults from "./BaseResults"; + +ChartJS.register(CategoryScale, LinearScale, BarElement); + +export default function ChoiceResults({ element }) { + const data = { + //labels: element.data.options, + labels: element.options.map((o) => o.label), + datasets: [ + { + //data: getDataset(element, elementAnswers), + data: element.options.map((o) => o.summary || 0), + backgroundColor: ["rgba(245, 59, 87, 0.7)"], + borderColor: ["rgba(245, 59, 87, 1)"], + borderWidth: 1, + }, + ], + }; + + const options: any = { + indexAxis: "y", + responsive: true, + plugins: { + legend: { + display: false, + }, + }, + scales: { + yAxis: [ + { + ticks: { + min: 1, + precision: 0, + }, + }, + ], + }, + }; + + return ( + +
    + +
    +
    + ); +} diff --git a/components/results/summary/TextResults.tsx b/components/results/summary/TextResults.tsx index 3fac3171df..06a3ecf87d 100644 --- a/components/results/summary/TextResults.tsx +++ b/components/results/summary/TextResults.tsx @@ -3,10 +3,10 @@ import BaseResults from "./BaseResults"; export default function TextResults({ element }) { return ( -
    +
      {element.summary.map((answer) => ( -
    • +
    • {/* Extend touch target to entire panel */} diff --git a/lib/submissionSessions.ts b/lib/submissionSessions.ts index 78d404ddc4..d9bf90c33e 100644 --- a/lib/submissionSessions.ts +++ b/lib/submissionSessions.ts @@ -111,17 +111,33 @@ export const getSubmissionSummary = ( ); if (typeof elementInSummary !== "undefined") { if ( - elementInSummary.type === "text" || - elementInSummary.type === "textarea" + [ + "email", + "number", + "phone", + "text", + "textarea", + "website", + ].includes(elementInSummary.type) ) { if (!("summary" in elementInSummary)) { elementInSummary.summary = []; } elementInSummary.summary.push(elementValue); - } else if ( - elementInSummary.type === "radio" || - elementInSummary.type === "checkbox" - ) { + } else if (elementInSummary.type === "checkbox") { + // checkbox values are a list of values + for (const value of elementValue) { + const optionInSummary = elementInSummary.options.find( + (o) => o.value === value + ); + if (typeof optionInSummary !== "undefined") { + if (!("summary" in optionInSummary)) { + optionInSummary.summary = 0; + } + optionInSummary.summary += 1; + } + } + } else if (elementInSummary.type === "radio") { const optionInSummary = elementInSummary.options.find( (o) => o.value === elementValue ); diff --git a/lib/types.ts b/lib/types.ts index f9ff43c6fd..823a2e1283 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -40,9 +40,20 @@ export type SchemaPage = { elements: SchemaElement[]; }; +export type SnoopType = + | "checkbox" + | "email" + | "number" + | "phone" + | "radio" + | "submit" + | "text" + | "textarea" + | "website"; + export type SchemaElement = { name: string; - type: "checkbox" | "radio" | "text" | "textarea" | "submit"; + type: SnoopType; label?: string; options?: SchemaOption[]; }; @@ -64,7 +75,7 @@ export type SubmissionSummaryPage = { export type SubmissionSummaryElement = { name: string; - type: "checkbox" | "radio" | "text" | "textarea" | "submit"; + type: SnoopType; label?: string; summary?: string[]; options?: SubmissionSummaryOption[]; diff --git a/package.json b/package.json index 670afa84d2..6ce9535ecc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "snoopforms", - "version": "0.1.0", + "version": "0.2.0", "private": true, "scripts": { "dev": "next dev", @@ -16,9 +16,10 @@ "@headlessui/react": "^1.6.1", "@heroicons/react": "^1.0.6", "@prisma/client": "^4.1.0", - "@snoopforms/react": "^0.0.4", + "@snoopforms/react": "^0.2.0", "babel-plugin-superjson-next": "^0.4.3", "bcryptjs": "^2.4.3", + "chart.js": "^3.8.2", "date-fns": "^2.28.0", "editorjs-drag-drop": "^1.1.2", "editorjs-undo": "^2.0.3", @@ -30,6 +31,7 @@ "nodemailer": "^6.7.7", "posthog-js": "^1.26.0", "react": "17.0.2", + "react-chartjs-2": "^4.3.1", "react-dom": "17.0.2", "react-icons": "^4.4.0", "react-loader-spinner": "^5.1.5", diff --git a/pages/forms/[id]/results/summary.tsx b/pages/forms/[id]/results/summary.tsx index 5e6111c394..b073ae6068 100644 --- a/pages/forms/[id]/results/summary.tsx +++ b/pages/forms/[id]/results/summary.tsx @@ -27,7 +27,6 @@ export default function ResultsSummaryPage() { breadcrumbs={[{ name: form.name, href: "#", current: true }]} steps={formMenuSteps} currentStep="results" - limitHeightScreen={true} >