mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-10 01:58:16 -05:00
Feature/add question types (#9)
* add new question types: email, website, phone, number & multiple choice
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
135
components/editorjs/tools/EmailQuestion.tsx
Normal file
135
components/editorjs/tools/EmailQuestion.tsx
Normal file
@@ -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: `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M14.243 5.757a6 6 0 10-.986 9.284 1 1 0 111.087 1.678A8 8 0 1118 10a3 3 0 01-4.8 2.401A4 4 0 1114 10a1 1 0 102 0c0-1.537-.586-3.07-1.757-4.243zM12 10a2 2 0 10-4 0 2 2 0 004 0z" clip-rule="evenodd" />
|
||||
</svg>`,
|
||||
title: "Email Question",
|
||||
};
|
||||
}
|
||||
|
||||
constructor({
|
||||
data,
|
||||
}: {
|
||||
api: API;
|
||||
config?: ToolConfig;
|
||||
data?: EmailQuestionData;
|
||||
}) {
|
||||
this.wrapper = undefined;
|
||||
this.settings = [
|
||||
{
|
||||
name: "required",
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 512" class="w-3 h-3"><path d="M471.99 334.43L336.06 256l135.93-78.43c7.66-4.42 10.28-14.2 5.86-21.86l-32.02-55.43c-4.42-7.65-14.21-10.28-21.87-5.86l-135.93 78.43V16c0-8.84-7.17-16-16.01-16h-64.04c-8.84 0-16.01 7.16-16.01 16v156.86L56.04 94.43c-7.66-4.42-17.45-1.79-21.87 5.86L2.15 155.71c-4.42 7.65-1.8 17.44 5.86 21.86L143.94 256 8.01 334.43c-7.66 4.42-10.28 14.21-5.86 21.86l32.02 55.43c4.42 7.65 14.21 10.27 21.87 5.86l135.93-78.43V496c0 8.84 7.17 16 16.01 16h64.04c8.84 0 16.01-7.16 16.01-16V339.14l135.93 78.43c7.66 4.42 17.45 1.8 21.87-5.86l32.02-55.43c4.42-7.65 1.8-17.43-5.86-21.85z"/></svg>`,
|
||||
},
|
||||
];
|
||||
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 = (
|
||||
<div className="pb-5">
|
||||
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
|
||||
<input
|
||||
type="text"
|
||||
id="label"
|
||||
defaultValue={this.data.label}
|
||||
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
|
||||
placeholder="Your Question"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
|
||||
*
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative max-w-sm mt-1 rounded-md shadow-sm">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<MailIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="email"
|
||||
className="block w-full pl-10 text-gray-300 border-gray-300 rounded-md sm:text-sm"
|
||||
defaultValue={this.data.placeholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
ReactDOM.render(toolView, this.wrapper);
|
||||
return this.wrapper;
|
||||
}
|
||||
}
|
||||
130
components/editorjs/tools/MultipleChoiceQuestion.tsx
Normal file
130
components/editorjs/tools/MultipleChoiceQuestion.tsx
Normal file
@@ -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: `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#000000" d="M8 0c-4.418 0-8 3.582-8 8s3.582 8 8 8 8-3.582 8-8-3.582-8-8-8zM8 14c-3.314 0-6-2.686-6-6s2.686-6 6-6c3.314 0 6 2.686 6 6s-2.686 6-6 6zM5 8c0-1.657 1.343-3 3-3s3 1.343 3 3c0 1.657-1.343 3-3 3s-3-1.343-3-3z"/>
|
||||
</svg>`,
|
||||
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: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 512" class="w-3 h-3"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M471.99 334.43L336.06 256l135.93-78.43c7.66-4.42 10.28-14.2 5.86-21.86l-32.02-55.43c-4.42-7.65-14.21-10.28-21.87-5.86l-135.93 78.43V16c0-8.84-7.17-16-16.01-16h-64.04c-8.84 0-16.01 7.16-16.01 16v156.86L56.04 94.43c-7.66-4.42-17.45-1.79-21.87 5.86L2.15 155.71c-4.42 7.65-1.8 17.44 5.86 21.86L143.94 256 8.01 334.43c-7.66 4.42-10.28 14.21-5.86 21.86l32.02 55.43c4.42 7.65 14.21 10.27 21.87 5.86l135.93-78.43V496c0 8.84 7.17 16 16.01 16h64.04c8.84 0 16.01-7.16 16.01-16V339.14l135.93 78.43c7.66 4.42 17.45 1.8 21.87-5.86l32.02-55.43c4.42-7.65 1.8-17.43-5.86-21.85z"/></svg>`,
|
||||
},
|
||||
];
|
||||
|
||||
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(
|
||||
<MultipleChoiceQuestionComponent
|
||||
onDataChange={onDataChange}
|
||||
readOnly={this.readOnly}
|
||||
data={this.data}
|
||||
/>,
|
||||
rootNode
|
||||
);
|
||||
|
||||
return this.nodes.holder;
|
||||
}
|
||||
|
||||
save() {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
168
components/editorjs/tools/MultipleChoiceQuestionComponent.tsx
Normal file
168
components/editorjs/tools/MultipleChoiceQuestionComponent.tsx
Normal file
@@ -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 (
|
||||
<div className="pb-5">
|
||||
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
|
||||
<input
|
||||
type="text"
|
||||
id="label"
|
||||
defaultValue={choiceData.label}
|
||||
onBlur={onInputChange("label")}
|
||||
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
|
||||
placeholder="Your Question"
|
||||
/>
|
||||
{choiceData.required && (
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
|
||||
*
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-sm mt-2 space-y-2">
|
||||
{choiceData.options.map((option, optionIdx) => (
|
||||
<div
|
||||
key={option.label}
|
||||
className={classNames("relative flex items-start")}
|
||||
>
|
||||
<span className="flex items-center text-sm">
|
||||
<span
|
||||
className={classNames(
|
||||
choiceData.multipleChoice ? "rounded-sm" : "rounded-full",
|
||||
"flex items-center justify-center w-4 h-4 bg-white border border-gray-300"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="rounded-full bg-white w-1.5 h-1.5" />
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={option.label}
|
||||
onBlur={onOptionChange(optionIdx, "label")}
|
||||
className="p-0 ml-3 font-medium text-gray-900 border-0 border-transparent outline-none focus:ring-0 focus:outline-none placeholder:text-gray-300"
|
||||
placeholder={`Option ${optionIdx + 1}`}
|
||||
/>
|
||||
</span>
|
||||
{optionIdx !== 0 && (
|
||||
<button
|
||||
onClick={() => onDeleteOption(optionIdx)}
|
||||
className="absolute p-1 right-3"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4 text-gray-300" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="relative z-0 flex mt-2 divide-x divide-gray-200">
|
||||
<button
|
||||
className="mr-3 justify-center mt-2 inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none"
|
||||
onClick={onAddOption}
|
||||
>
|
||||
Add option
|
||||
</button>
|
||||
<Switch.Group as="div" className="flex items-center pl-3">
|
||||
<Switch
|
||||
checked={choiceData.multipleChoice}
|
||||
onChange={() => {
|
||||
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"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
choiceData.multipleChoice ? "translate-x-3" : "translate-x-0",
|
||||
"pointer-events-none inline-block h-3 w-3 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200"
|
||||
)}
|
||||
/>
|
||||
</Switch>
|
||||
<Switch.Label as="span" className="ml-3">
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Multiple Selection{" "}
|
||||
</span>
|
||||
{/* <span className="text-sm text-gray-500">(Save 10%)</span> */}
|
||||
</Switch.Label>
|
||||
</Switch.Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleChoiceQuestion;
|
||||
128
components/editorjs/tools/NumberQuestion.tsx
Normal file
128
components/editorjs/tools/NumberQuestion.tsx
Normal file
@@ -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: `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9.243 3.03a1 1 0 01.727 1.213L9.53 6h2.94l.56-2.243a1 1 0 111.94.486L14.53 6H17a1 1 0 110 2h-2.97l-1 4H15a1 1 0 110 2h-2.47l-.56 2.242a1 1 0 11-1.94-.485L10.47 14H7.53l-.56 2.242a1 1 0 11-1.94-.485L5.47 14H3a1 1 0 110-2h2.97l1-4H5a1 1 0 110-2h2.47l.56-2.243a1 1 0 011.213-.727zM9.03 8l-1 4h2.938l1-4H9.031z" clip-rule="evenodd" />
|
||||
</svg>`,
|
||||
title: "Number Question",
|
||||
};
|
||||
}
|
||||
|
||||
constructor({
|
||||
data,
|
||||
}: {
|
||||
api: API;
|
||||
config?: ToolConfig;
|
||||
data?: NumberQuestionData;
|
||||
}) {
|
||||
this.wrapper = undefined;
|
||||
this.settings = [
|
||||
{
|
||||
name: "required",
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 512" class="w-3 h-3"><!-- Font Awesome Pro 5.15.4 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) --><path d="M471.99 334.43L336.06 256l135.93-78.43c7.66-4.42 10.28-14.2 5.86-21.86l-32.02-55.43c-4.42-7.65-14.21-10.28-21.87-5.86l-135.93 78.43V16c0-8.84-7.17-16-16.01-16h-64.04c-8.84 0-16.01 7.16-16.01 16v156.86L56.04 94.43c-7.66-4.42-17.45-1.79-21.87 5.86L2.15 155.71c-4.42 7.65-1.8 17.44 5.86 21.86L143.94 256 8.01 334.43c-7.66 4.42-10.28 14.21-5.86 21.86l32.02 55.43c4.42 7.65 14.21 10.27 21.87 5.86l135.93-78.43V496c0 8.84 7.17 16 16.01 16h64.04c8.84 0 16.01-7.16 16.01-16V339.14l135.93 78.43c7.66 4.42 17.45 1.8 21.87-5.86l32.02-55.43c4.42-7.65 1.8-17.43-5.86-21.85z"/></svg>`,
|
||||
},
|
||||
];
|
||||
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 = (
|
||||
<div className="pb-5">
|
||||
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
|
||||
<input
|
||||
type="text"
|
||||
id="label"
|
||||
defaultValue={this.data.label}
|
||||
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
|
||||
placeholder="Your Question"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
|
||||
*
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full max-w-sm mt-1 text-sm text-gray-400 border-gray-300 rounded-md shadow-sm placeholder:text-gray-300"
|
||||
placeholder="optional placeholder"
|
||||
defaultValue={this.data.placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
ReactDOM.render(toolView, this.wrapper);
|
||||
return this.wrapper;
|
||||
}
|
||||
}
|
||||
135
components/editorjs/tools/PhoneQuestion.tsx
Normal file
135
components/editorjs/tools/PhoneQuestion.tsx
Normal file
@@ -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: `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z" />
|
||||
</svg>`,
|
||||
title: "Phone Question",
|
||||
};
|
||||
}
|
||||
|
||||
constructor({
|
||||
data,
|
||||
}: {
|
||||
api: API;
|
||||
config?: ToolConfig;
|
||||
data?: PhoneQuestionData;
|
||||
}) {
|
||||
this.wrapper = undefined;
|
||||
this.settings = [
|
||||
{
|
||||
name: "required",
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 512" class="w-3 h-3"><path d="M471.99 334.43L336.06 256l135.93-78.43c7.66-4.42 10.28-14.2 5.86-21.86l-32.02-55.43c-4.42-7.65-14.21-10.28-21.87-5.86l-135.93 78.43V16c0-8.84-7.17-16-16.01-16h-64.04c-8.84 0-16.01 7.16-16.01 16v156.86L56.04 94.43c-7.66-4.42-17.45-1.79-21.87 5.86L2.15 155.71c-4.42 7.65-1.8 17.44 5.86 21.86L143.94 256 8.01 334.43c-7.66 4.42-10.28 14.21-5.86 21.86l32.02 55.43c4.42 7.65 14.21 10.27 21.87 5.86l135.93-78.43V496c0 8.84 7.17 16 16.01 16h64.04c8.84 0 16.01-7.16 16.01-16V339.14l135.93 78.43c7.66 4.42 17.45 1.8 21.87-5.86l32.02-55.43c4.42-7.65 1.8-17.43-5.86-21.85z"/></svg>`,
|
||||
},
|
||||
];
|
||||
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 = (
|
||||
<div className="pb-5">
|
||||
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
|
||||
<input
|
||||
type="text"
|
||||
id="label"
|
||||
defaultValue={this.data.label}
|
||||
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
|
||||
placeholder="Your Question"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
|
||||
*
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative max-w-sm mt-1 rounded-md shadow-sm">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<PhoneIcon className="w-5 h-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
className="block w-full pl-10 text-gray-300 border-gray-300 rounded-md sm:text-sm"
|
||||
defaultValue={this.data.placeholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
ReactDOM.render(toolView, this.wrapper);
|
||||
return this.wrapper;
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export default class TextQuestion implements BlockTool {
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full mt-1 text-sm text-gray-400 border-gray-300 rounded-md shadow-sm placeholder:text-gray-300"
|
||||
className="block w-full max-w-sm mt-1 text-sm text-gray-400 border-gray-300 rounded-md shadow-sm placeholder:text-gray-300"
|
||||
placeholder="optional placeholder"
|
||||
defaultValue={this.data.placeholder}
|
||||
/>
|
||||
|
||||
138
components/editorjs/tools/WebsiteQuestion.tsx
Normal file
138
components/editorjs/tools/WebsiteQuestion.tsx
Normal file
@@ -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: `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4.083 9h1.946c.089-1.546.383-2.97.837-4.118A6.004 6.004 0 004.083 9zM10 2a8 8 0 100 16 8 8 0 000-16zm0 2c-.076 0-.232.032-.465.262-.238.234-.497.623-.737 1.182-.389.907-.673 2.142-.766 3.556h3.936c-.093-1.414-.377-2.649-.766-3.556-.24-.56-.5-.948-.737-1.182C10.232 4.032 10.076 4 10 4zm3.971 5c-.089-1.546-.383-2.97-.837-4.118A6.004 6.004 0 0115.917 9h-1.946zm-2.003 2H8.032c.093 1.414.377 2.649.766 3.556.24.56.5.948.737 1.182.233.23.389.262.465.262.076 0 .232-.032.465-.262.238-.234.498-.623.737-1.182.389-.907.673-2.142.766-3.556zm1.166 4.118c.454-1.147.748-2.572.837-4.118h1.946a6.004 6.004 0 01-2.783 4.118zm-6.268 0C6.412 13.97 6.118 12.546 6.03 11H4.083a6.004 6.004 0 002.783 4.118z" clip-rule="evenodd" />
|
||||
</svg>`,
|
||||
title: "Website Question",
|
||||
};
|
||||
}
|
||||
|
||||
constructor({
|
||||
data,
|
||||
}: {
|
||||
api: API;
|
||||
config?: ToolConfig;
|
||||
data?: WebsiteQuestionData;
|
||||
}) {
|
||||
this.wrapper = undefined;
|
||||
this.settings = [
|
||||
{
|
||||
name: "required",
|
||||
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 512" class="w-3 h-3"><path d="M471.99 334.43L336.06 256l135.93-78.43c7.66-4.42 10.28-14.2 5.86-21.86l-32.02-55.43c-4.42-7.65-14.21-10.28-21.87-5.86l-135.93 78.43V16c0-8.84-7.17-16-16.01-16h-64.04c-8.84 0-16.01 7.16-16.01 16v156.86L56.04 94.43c-7.66-4.42-17.45-1.79-21.87 5.86L2.15 155.71c-4.42 7.65-1.8 17.44 5.86 21.86L143.94 256 8.01 334.43c-7.66 4.42-10.28 14.21-5.86 21.86l32.02 55.43c4.42 7.65 14.21 10.27 21.87 5.86l135.93-78.43V496c0 8.84 7.17 16 16.01 16h64.04c8.84 0 16.01-7.16 16.01-16V339.14l135.93 78.43c7.66 4.42 17.45 1.8 21.87-5.86l32.02-55.43c4.42-7.65 1.8-17.43-5.86-21.85z"/></svg>`,
|
||||
},
|
||||
];
|
||||
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 = (
|
||||
<div className="pb-5">
|
||||
<div className="relative font-bold leading-7 text-gray-800 text-md sm:truncate">
|
||||
<input
|
||||
type="text"
|
||||
id="label"
|
||||
defaultValue={this.data.label}
|
||||
className="w-full p-0 border-0 border-transparent ring-0 focus:ring-0 placeholder:text-gray-300"
|
||||
placeholder="Your Question"
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-red-500 pointer-events-none">
|
||||
*
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative max-w-sm mt-1 rounded-md shadow-sm">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
<GlobeAltIcon
|
||||
className="w-5 h-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="website"
|
||||
className="block w-full pl-10 text-gray-300 border-gray-300 rounded-md sm:text-sm"
|
||||
defaultValue={this.data.placeholder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
ReactDOM.render(toolView, this.wrapper);
|
||||
return this.wrapper;
|
||||
}
|
||||
}
|
||||
@@ -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) => (
|
||||
<a
|
||||
className="flex col-span-1 rounded-md shadow-sm"
|
||||
key={lib.id}
|
||||
href={lib.href}
|
||||
target={lib.target || ""}
|
||||
rel="noreferrer"
|
||||
>
|
||||
<li
|
||||
className={classNames(
|
||||
lib.comingSoon
|
||||
? "text-ui-gray-medium"
|
||||
: "shadow-sm text-ui-gray-dark hover:text-black",
|
||||
"flex col-span-1 rounded-md w-full"
|
||||
)}
|
||||
<Link key={lib.id} href={lib.href}>
|
||||
<a
|
||||
className="flex col-span-1 rounded-md shadow-sm"
|
||||
target={lib.target || ""}
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div
|
||||
<li
|
||||
className={classNames(
|
||||
lib.bgColor,
|
||||
"flex-shrink-0 flex items-center justify-center w-20 text-white text-sm font-medium rounded-l-md"
|
||||
lib.comingSoon
|
||||
? "text-ui-gray-medium"
|
||||
: "shadow-sm text-ui-gray-dark hover:text-black",
|
||||
"flex col-span-1 rounded-md w-full"
|
||||
)}
|
||||
>
|
||||
<lib.icon
|
||||
<div
|
||||
className={classNames(
|
||||
lib.comingSoon
|
||||
? "text-ui-gray-medium"
|
||||
: "text-white stroke-1",
|
||||
"w-10 h-10"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
lib.comingSoon ? "border-dashed" : "",
|
||||
"flex items-center justify-between flex-1 truncate bg-white rounded-r-md"
|
||||
)}
|
||||
>
|
||||
<div className="inline-flex px-4 py-6 text-lg truncate">
|
||||
<p className="font-light">{lib.name}</p>
|
||||
{lib.comingSoon && (
|
||||
<div className="p-1 px-3 ml-3 bg-green-100 rounded">
|
||||
<p className="text-xs text-black">coming soon</p>
|
||||
</div>
|
||||
lib.bgColor,
|
||||
"flex-shrink-0 flex items-center justify-center w-20 text-white text-sm font-medium rounded-l-md"
|
||||
)}
|
||||
>
|
||||
<lib.icon
|
||||
className={classNames(
|
||||
lib.comingSoon
|
||||
? "text-ui-gray-medium"
|
||||
: "text-white stroke-1",
|
||||
"w-10 h-10"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</a>
|
||||
<div
|
||||
className={classNames(
|
||||
lib.comingSoon ? "border-dashed" : "",
|
||||
"flex items-center justify-between flex-1 truncate bg-white rounded-r-md"
|
||||
)}
|
||||
>
|
||||
<div className="inline-flex px-4 py-6 text-lg truncate">
|
||||
<p className="font-light">{lib.name}</p>
|
||||
{lib.comingSoon && (
|
||||
<div className="p-1 px-3 ml-3 bg-green-100 rounded">
|
||||
<p className="text-xs text-black">coming soon</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -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) => (
|
||||
<SnoopPage key={page.id} name={page.id}>
|
||||
{pages.map((page, pageIdx) => (
|
||||
<SnoopPage
|
||||
key={page.id}
|
||||
name={page.id}
|
||||
thankyou={pageIdx === pages.length - 1}
|
||||
>
|
||||
{page.blocks.map((block) => (
|
||||
<div key={block.id}>
|
||||
{block.type === "paragraph" ? (
|
||||
@@ -61,20 +68,81 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {
|
||||
<h3 className="ce-header">{block.data.text}</h3>
|
||||
) : null
|
||||
) : block.type === "textQuestion" ? (
|
||||
<div className="pb-5">
|
||||
<SnoopElement
|
||||
type="text"
|
||||
name={block.id}
|
||||
label={block.data.label}
|
||||
classNames={{
|
||||
label:
|
||||
"mt-4 mb-2 block text-md font-bold leading-7 text-gray-800 sm:truncate",
|
||||
element:
|
||||
"flex-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full min-w-0 rounded-md sm:text-base border-gray-300",
|
||||
}}
|
||||
required={block.data.required}
|
||||
/>
|
||||
</div>
|
||||
<SnoopElement
|
||||
type="text"
|
||||
name={block.id}
|
||||
label={block.data.label}
|
||||
placeholder={block.data.placeholder}
|
||||
classNames={{
|
||||
label:
|
||||
"mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
|
||||
}}
|
||||
required={block.data.required}
|
||||
/>
|
||||
) : block.type === "emailQuestion" ? (
|
||||
<SnoopElement
|
||||
type="email"
|
||||
name={block.id}
|
||||
label={block.data.label}
|
||||
placeholder={block.data.placeholder}
|
||||
icon={<MailIcon className="w-5 h-5" />}
|
||||
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 ? (
|
||||
<SnoopElement
|
||||
type="checkbox"
|
||||
name={block.id}
|
||||
label={block.data.label}
|
||||
options={block.data.options.map((o) => 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 ? (
|
||||
<SnoopElement
|
||||
type="radio"
|
||||
name={block.id}
|
||||
label={block.data.label}
|
||||
options={block.data.options.map((o) => 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" ? (
|
||||
<SnoopElement
|
||||
type="number"
|
||||
name={block.id}
|
||||
label={block.data.label}
|
||||
placeholder={block.data.placeholder}
|
||||
classNames={{
|
||||
label:
|
||||
"mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
|
||||
}}
|
||||
required={block.data.required}
|
||||
/>
|
||||
) : block.type === "phoneQuestion" ? (
|
||||
<SnoopElement
|
||||
type="phone"
|
||||
name={block.id}
|
||||
label={block.data.label}
|
||||
placeholder={block.data.placeholder}
|
||||
icon={<PhoneIcon className="w-5 h-5" />}
|
||||
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" ? (
|
||||
<SnoopElement
|
||||
name="submit"
|
||||
@@ -85,6 +153,19 @@ export default function App({ id = "", formId, blocks, localOnly = false }) {
|
||||
"inline-flex items-center px-4 py-3 text-sm font-medium text-white bg-gray-700 border border-transparent rounded-md shadow-sm hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500",
|
||||
}}
|
||||
/>
|
||||
) : block.type === "websiteQuestion" ? (
|
||||
<SnoopElement
|
||||
type="website"
|
||||
name={block.id}
|
||||
label={block.data.label}
|
||||
placeholder={block.data.placeholder}
|
||||
icon={<GlobeAltIcon className="w-5 h-5" />}
|
||||
classNames={{
|
||||
label:
|
||||
"mt-4 mb-2 block text-lg font-bold leading-7 text-gray-800 sm:truncate",
|
||||
}}
|
||||
required={block.data.required}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -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" && (
|
||||
<div key={page.name}>
|
||||
{page.elements.map((element) =>
|
||||
element.type === "text" || element.type === "textarea" ? (
|
||||
[
|
||||
"email",
|
||||
"number",
|
||||
"phone",
|
||||
"text",
|
||||
"textarea",
|
||||
"website",
|
||||
].includes(element.type) ? (
|
||||
<TextResults element={element} />
|
||||
) : ["checkbox", "radio"].includes(element.type) ? (
|
||||
<ChoiceResults element={element} />
|
||||
) : null
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
54
components/results/summary/ChoiceResults.tsx
Normal file
54
components/results/summary/ChoiceResults.tsx
Normal file
@@ -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 (
|
||||
<BaseResults element={element}>
|
||||
<div className="flow-root px-8 my-4 mt-6 text-center">
|
||||
<Chart type="bar" data={data} options={options} height={75} />
|
||||
</div>
|
||||
</BaseResults>
|
||||
);
|
||||
}
|
||||
@@ -3,10 +3,10 @@ import BaseResults from "./BaseResults";
|
||||
export default function TextResults({ element }) {
|
||||
return (
|
||||
<BaseResults element={element}>
|
||||
<div className="flow-root px-8 my-4 mt-6 overflow-y-scroll text-center max-h-64">
|
||||
<div className="flow-root px-8 my-4 mt-6 overflow-y-scroll text-center h-44 max-h-64">
|
||||
<ul className="-my-5 divide-y divide-ui-gray-light">
|
||||
{element.summary.map((answer) => (
|
||||
<li key={answer} className="py-8">
|
||||
<li key={answer} className="py-4">
|
||||
<div className="relative focus-within:ring-2 focus-within:ring-indigo-500">
|
||||
<h3 className="text-sm text-gray-700">
|
||||
{/* Extend touch target to entire panel */}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
15
lib/types.ts
15
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[];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -27,7 +27,6 @@ export default function ResultsSummaryPage() {
|
||||
breadcrumbs={[{ name: form.name, href: "#", current: true }]}
|
||||
steps={formMenuSteps}
|
||||
currentStep="results"
|
||||
limitHeightScreen={true}
|
||||
>
|
||||
<SecondNavBar
|
||||
navItems={formResultsSecondNavigation}
|
||||
|
||||
Reference in New Issue
Block a user