Feature Thank You Card (#222)

add thankyou card to surveys.

---------

Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Moritz Rengert
2023-04-12 12:35:04 +02:00
committed by GitHub
parent bc771bc188
commit a0baa25864
29 changed files with 524 additions and 117 deletions

View File

@@ -20,6 +20,7 @@ import clsx from "clsx";
import { useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { testURLmatch } from "./testURLmatch";
import toast from "react-hot-toast";
interface EventDetailModalProps {
environmentId: string;
@@ -70,6 +71,8 @@ export default function AddNoCodeEventModal({
watch("noCodeConfig.[pageUrl].rule")
);
setIsMatch(match);
if (match === "yes") toast.success("Your survey would be shown on this URL.");
if (match === "no") toast.error("Your survey would not be shown.");
};
return (
@@ -82,9 +85,9 @@ export default function AddNoCodeEventModal({
<CursorArrowRaysIcon />
</div>
<div>
<div className="text-xl font-medium text-slate-700">Add No-Code Event</div>
<div className="text-xl font-medium text-slate-700">Add Trigger Event</div>
<div className="text-sm text-slate-500">
Create a new no-code event to filter your user base with.
Create a new trigger event to show the survey at a specific point in the user journey.
</div>
</div>
</div>

View File

@@ -10,7 +10,7 @@ export function EditTeamName() {
<Label htmlFor="teamname">Team Name</Label>
<Input type="text" id="teamname" />
<Button type="submit" className="mt-4" onClick={(e) => console.log(e)}>
<Button type="submit" className="mt-4" onClick={() => new Error("Not implemented yet")}>
Update
</Button>
</div>

View File

@@ -1,25 +1,52 @@
import { Survey } from "@formbricks/types/surveys";
import Modal from "@/components/preview/Modal";
import MultipleChoiceSingleQuestion from "@/components/preview/MultipleChoiceSingleQuestion";
import OpenTextQuestion from "@/components/preview/OpenTextQuestion";
import ThankYouCard from "@/components/preview/ThankYouCard";
import type { Question } from "@formbricks/types/questions";
import { useEffect, useState } from "react";
import QuestionConditional from "@/components/preview/QuestionConditional";
interface PreviewSurveyProps {
localSurvey?: Survey;
setActiveQuestionId: (id: string | null) => void;
activeQuestionId?: string | null;
questions: Question[];
brandColor: string;
}
export default function PreviewSurvey({ activeQuestionId, questions, brandColor }: PreviewSurveyProps) {
export default function PreviewSurvey({
localSurvey,
setActiveQuestionId,
activeQuestionId,
questions,
brandColor,
}: PreviewSurveyProps) {
const [isModalOpen, setIsModalOpen] = useState(true);
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null);
useEffect(() => {
if (!localSurvey?.thankYouCard.enabled) {
if (activeQuestionId === "thank-you-card") {
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions[0]);
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
}
}
}, [localSurvey]);
useEffect(() => {
const currentIndex = questions.findIndex((q) => q.id === currentQuestion?.id);
if (currentIndex < questions.length && currentIndex >= 0 && !localSurvey) return;
if (activeQuestionId) {
if (currentQuestion && currentQuestion.id === activeQuestionId) {
setCurrentQuestion(questions.find((q) => q.id === activeQuestionId) || null);
return;
}
if (activeQuestionId === "thank-you-card") return;
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions.find((q) => q.id === activeQuestionId) || null);
@@ -30,24 +57,38 @@ export default function PreviewSurvey({ activeQuestionId, questions, brandColor
setCurrentQuestion(questions[0]);
}
}
}, [activeQuestionId, questions]);
}, [activeQuestionId, currentQuestion, localSurvey, questions]);
const gotoNextQuestion = () => {
if (currentQuestion) {
const currentIndex = questions.findIndex((q) => q.id === currentQuestion.id);
if (currentIndex < questions.length - 1) {
setCurrentQuestion(questions[currentIndex + 1]);
setActiveQuestionId(questions[currentIndex + 1].id);
} else {
// start over
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions[0]);
setIsModalOpen(true);
}, 500);
if (localSurvey?.thankYouCard?.enabled) {
setActiveQuestionId("thank-you-card");
} else {
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions[0]);
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
}
}
}
};
const resetPreview = () => {
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions[0]);
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
};
if (!currentQuestion) {
return null;
}
@@ -55,22 +96,21 @@ export default function PreviewSurvey({ activeQuestionId, questions, brandColor
const lastQuestion = currentQuestion.id === questions[questions.length - 1].id;
return (
<Modal isOpen={isModalOpen}>
{currentQuestion.type === "openText" ? (
<OpenTextQuestion
question={currentQuestion}
onSubmit={() => gotoNextQuestion()}
lastQuestion={lastQuestion}
<Modal isOpen={isModalOpen} reset={resetPreview}>
{activeQuestionId == "thank-you-card" ? (
<ThankYouCard
brandColor={brandColor}
headline={localSurvey?.thankYouCard?.headline || ""}
subheader={localSurvey?.thankYouCard?.subheader || ""}
/>
) : currentQuestion.type === "multipleChoiceSingle" ? (
<MultipleChoiceSingleQuestion
question={currentQuestion}
onSubmit={() => gotoNextQuestion()}
lastQuestion={lastQuestion}
) : (
<QuestionConditional
currentQuestion={currentQuestion}
brandColor={brandColor}
lastQuestion={lastQuestion}
onSubmit={gotoNextQuestion}
/>
) : null}
)}
</Modal>
);
}

View File

@@ -5,6 +5,7 @@ import { PlusIcon } from "@heroicons/react/24/solid";
import { createId } from "@paralleldrive/cuid2";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
interface AddQuestionButtonProps {
addQuestion: (question: any) => void;
@@ -16,7 +17,10 @@ export default function AddQuestionButton({ addQuestion }: AddQuestionButtonProp
<Collapsible.Root
open={open}
onOpenChange={setOpen}
className=" w-full space-y-2 rounded-lg border border-dashed border-slate-300 bg-white hover:cursor-pointer">
className={cn(
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
"w-full space-y-2 rounded-lg border border-dashed border-slate-300 bg-white transition-transform duration-300 ease-in-out hover:cursor-pointer"
)}>
<Collapsible.CollapsibleTrigger asChild className="group h-full w-full">
<div className="inline-flex">
<div className="bg-brand-dark flex w-10 items-center justify-center rounded-l-lg group-aria-expanded:rounded-bl-none">
@@ -33,7 +37,7 @@ export default function AddQuestionButton({ addQuestion }: AddQuestionButtonProp
{questionTypes.map((questionType) => (
<button
key={questionType.id}
className="inline-flex items-center py-2 px-4 text-sm font-medium text-slate-700 last:mb-2 hover:bg-slate-100"
className="inline-flex items-center px-4 py-2 text-sm font-medium text-slate-700 last:mb-2 hover:bg-slate-100"
onClick={() => {
addQuestion({
id: createId(),

View File

@@ -0,0 +1,120 @@
"use client";
import { cn } from "@formbricks/lib/cn";
import type { Survey } from "@formbricks/types/surveys";
import { Input, Label, Switch } from "@formbricks/ui";
import * as Collapsible from "@radix-ui/react-collapsible";
interface EditThankYouCardProps {
localSurvey: Survey;
setLocalSurvey: (survey: Survey) => void;
setActiveQuestionId: (id: string | null) => void;
activeQuestionId: string | null;
}
export default function EditThankYouCard({
localSurvey,
setLocalSurvey,
setActiveQuestionId,
activeQuestionId,
}: EditThankYouCardProps) {
// const [open, setOpen] = useState(false);
let open = activeQuestionId == "thank-you-card";
const setOpen = (e) => {
if (e) {
setActiveQuestionId("thank-you-card");
} else {
setActiveQuestionId(null);
}
};
const updateSurvey = (data) => {
setLocalSurvey({
...localSurvey,
thankYouCard: {
...localSurvey.thankYouCard,
...data,
},
});
};
return (
<div
className={cn(
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
"flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
)}>
<div
className={cn(
open ? "bg-slate-700" : "bg-slate-400",
"flex w-10 items-center justify-center rounded-l-lg hover:bg-slate-600 group-aria-expanded:rounded-bl-none"
)}>
<p>🙏</p>
</div>
<Collapsible.Root
open={open}
onOpenChange={setOpen}
className="flex-1 rounded-r-lg border border-slate-200 transition-all duration-300 ease-in-out">
<Collapsible.CollapsibleTrigger
asChild
className="flex cursor-pointer justify-between p-4 hover:bg-slate-50">
<div>
<div className="inline-flex">
<div>
<p className="text-sm font-semibold">Thank You Card</p>
{!open && (
<p className="mt-1 truncate text-xs text-slate-500">
{localSurvey?.thankYouCard?.enabled ? "Shown" : "Hidden"}
</p>
)}
</div>
</div>
<div className="flex items-center space-x-2">
<Label htmlFor="thank-you-toggle">Show</Label>
<Switch
id="thank-you-toggle"
checked={localSurvey?.thankYouCard?.enabled}
onClick={(e) => {
e.stopPropagation();
updateSurvey({ enabled: !localSurvey.thankYouCard?.enabled });
}}
/>
</div>
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="px-4 pb-4">
<form>
<div className="mt-3">
<Label htmlFor="headline">Headline</Label>
<div className="mt-2">
<Input
id="headline"
name="headline"
defaultValue={localSurvey?.thankYouCard?.headline}
onChange={(e) => {
updateSurvey({ headline: e.target.value });
}}
/>
</div>
</div>
<div className="mt-3">
<Label htmlFor="subheader">Description</Label>
<div className="mt-2">
<Input
id="subheader"
name="subheader"
defaultValue={localSurvey?.thankYouCard?.subheader}
onChange={(e) => {
updateSurvey({ subheader: e.target.value });
}}
/>
</div>
</div>
</form>
</Collapsible.CollapsibleContent>
</Collapsible.Root>
</div>
);
}

View File

@@ -44,7 +44,7 @@ export default function MultipleChoiceSingleForm({
return (
<form>
<div className="mt-3">
<Label htmlFor="headline">Headline</Label>
<Label htmlFor="headline">Question</Label>
<div className="mt-2">
<Input
id="headline"
@@ -56,7 +56,7 @@ export default function MultipleChoiceSingleForm({
</div>
<div className="mt-3">
<Label htmlFor="subheader">Subheader</Label>
<Label htmlFor="subheader">Description</Label>
<div className="mt-2">
<Input
id="subheader"

View File

@@ -18,7 +18,7 @@ export default function OpenQuestionForm({
return (
<form>
<div className="mt-3">
<Label htmlFor="headline">Headline</Label>
<Label htmlFor="headline">Question</Label>
<div className="mt-2">
<Input
id="headline"
@@ -30,7 +30,7 @@ export default function OpenQuestionForm({
</div>
<div className="mt-3">
<Label htmlFor="subheader">Subheader</Label>
<Label htmlFor="subheader">Description</Label>
<div className="mt-2">
<Input
id="subheader"

View File

@@ -5,7 +5,7 @@ import { Switch } from "@formbricks/ui";
import { getQuestionTypeName } from "@/lib/questions";
import { cn } from "@formbricks/lib/cn";
import type { Question } from "@formbricks/types/questions";
import { Bars4Icon } from "@heroicons/react/24/solid";
import { Bars3BottomLeftIcon } from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { Draggable } from "react-beautiful-dnd";
import MultipleChoiceSingleForm from "./MultipleChoiceSingleForm";
@@ -36,14 +36,17 @@ export default function QuestionCard({
<Draggable draggableId={question.id} index={questionIdx}>
{(provided) => (
<div
className="flex flex-row rounded-lg bg-white shadow-lg"
className={cn(
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
"flex flex-row rounded-lg bg-white transition-all duration-300 ease-in-out"
)}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}>
<div
className={cn(
open ? "bg-slate-600" : "bg-slate-500",
"top-0 w-10 cursor-grabbing rounded-l-lg p-2 text-center text-sm text-white hover:bg-slate-700"
open ? "bg-slate-700" : "bg-slate-400",
"top-0 w-10 cursor-move rounded-l-lg p-2 text-center text-sm text-white hover:bg-slate-600"
)}>
{questionIdx + 1}
</div>
@@ -58,7 +61,7 @@ export default function QuestionCard({
className="flex cursor-pointer justify-between p-4 hover:bg-slate-50">
<div>
<div className="inline-flex">
<Bars4Icon className="-ml-0.5 mr-2 h-5 w-5 text-slate-400" />
<Bars3BottomLeftIcon className="-ml-0.5 mr-2 h-5 w-5 text-slate-400" />
<div>
<p className="text-sm font-semibold">
{question.headline || getQuestionTypeName(question.type)}
@@ -70,20 +73,23 @@ export default function QuestionCard({
)}
</div>
</div>
{open && (
<div className="flex items-center space-x-2">
<Label htmlFor="airplane-mode">Required</Label>
<Switch
id="airplane-mode"
checked={question.required}
onClick={(e) => {
e.stopPropagation();
updateQuestion(questionIdx, { required: !question.required });
}}
/>
<QuestionDropdown deleteQuestion={deleteQuestion} questionIdx={questionIdx} />
</div>
)}
<div className="flex items-center space-x-2">
{open && (
<div className="flex items-center space-x-2">
<Label htmlFor="required-toggle">Required</Label>
<Switch
id="required-toggle"
checked={question.required}
onClick={(e) => {
e.stopPropagation();
updateQuestion(questionIdx, { required: !question.required });
}}
/>
</div>
)}
<QuestionDropdown deleteQuestion={deleteQuestion} questionIdx={questionIdx} />
</div>
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="px-4 pb-4">

View File

@@ -5,6 +5,7 @@ import { DragDropContext } from "react-beautiful-dnd";
import AddQuestionButton from "./AddQuestionButton";
import QuestionCard from "./QuestionCard";
import { StrictModeDroppable } from "./StrictModeDroppable";
import EditThankYouCard from "./EditThankYouCard";
interface QuestionsViewProps {
localSurvey: Survey;
@@ -80,6 +81,14 @@ export default function QuestionsView({
</div>
</DragDropContext>
<AddQuestionButton addQuestion={addQuestion} />
<div className="mt-5">
<EditThankYouCard
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
setActiveQuestionId={setActiveQuestionId}
activeQuestionId={activeQuestionId}
/>
</div>
</div>
);
}

View File

@@ -83,8 +83,10 @@ export default function SurveyEditor({ environmentId, surveyId }: SurveyEditorPr
<aside className="relative hidden h-full flex-1 flex-shrink-0 overflow-hidden border-l border-slate-200 bg-slate-200 shadow-inner md:flex md:flex-col">
<PreviewSurvey
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
questions={localSurvey.questions}
brandColor={product.brandColor}
localSurvey={localSurvey}
/>
</aside>
</div>

View File

@@ -7,7 +7,7 @@ import type { Survey } from "@formbricks/types/surveys";
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui";
import { CheckCircleIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
import { useEffect, useState } from "react";
import AddNoCodeEventModal from "../../../events/AddNoCodeEventModal";
interface WhenToSendCardProps {
@@ -22,14 +22,6 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
useEventClasses(environmentId);
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
if (isLoadingEventClasses) {
return <LoadingSpinner />;
}
if (isErrorEventClasses) {
return <div>Error</div>;
}
const addTriggerEvent = () => {
const updatedSurvey = { ...localSurvey };
updatedSurvey.triggers = [...localSurvey.triggers, ""];
@@ -48,14 +40,23 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
setLocalSurvey(updatedSurvey);
};
/* // If there are no trigger events, set default to first event class in the eventClasses object
if (localSurvey.triggers.length === 0 && eventClasses.length > 0) {
setTriggerEvent(0, eventClasses[0].id);
} */
//create new empty trigger on page load, remove one click for user
useEffect(() => {
if (localSurvey.triggers.length === 0) {
addTriggerEvent();
}
}, []);
if (isLoadingEventClasses) {
return <LoadingSpinner />;
}
if (isErrorEventClasses) {
return <div>Error</div>;
}
return (
<>
{" "}
<Collapsible.Root
open={open}
onOpenChange={setOpen}
@@ -65,7 +66,7 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
)}>
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
<div className="inline-flex px-4 py-6">
<div className="flex items-center pr-5 pl-2">
<div className="flex items-center pl-2 pr-5">
{localSurvey.triggers.length === 0 || !localSurvey.triggers[0] ? (
<div className="h-7 w-7 rounded-full border border-slate-400" />
) : (
@@ -97,6 +98,15 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
{eventClasses.map((eventClass) => (
<SelectItem value={eventClass.id}>{eventClass.name}</SelectItem>
))}
<button
className="flex w-full items-center space-x-2 rounded-md p-1 text-sm font-semibold text-slate-800 hover:bg-slate-100 hover:text-slate-500 "
value="none"
onClick={() => {
setAddEventModalOpen(true);
}}>
<PlusIcon className="mr-1 h-5 w-5" />
Create Event
</button>
</SelectContent>
</Select>
<p className="mx-2 text-sm">event is triggered</p>
@@ -106,7 +116,7 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
</div>
</div>
))}
<div className="p-3">
<div className="ml-14 p-3">
<Button
variant="secondary"
onClick={() => {
@@ -115,13 +125,6 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
<PlusIcon className="mr-2 h-4 w-4" />
Add condition
</Button>
<Button
variant="minimal"
onClick={() => {
setAddEventModalOpen(true);
}}>
Create event
</Button>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.Root>

View File

@@ -5,18 +5,20 @@ import { useProduct } from "@/lib/products/products";
import { cn } from "@formbricks/lib/cn";
import type { Template } from "@formbricks/types/templates";
import { PlusCircleIcon } from "@heroicons/react/24/outline";
import { createId } from "@paralleldrive/cuid2";
import Link from "next/link";
import { useEffect, useState } from "react";
import PreviewSurvey from "../PreviewSurvey";
import TemplateMenuBar from "./TemplateMenuBar";
import { templates } from "./templates";
import { templates, customSurvey } from "./templates";
import { PaintBrushIcon } from "@heroicons/react/24/solid";
import { ErrorComponent } from "@formbricks/ui";
import { replacePresetPlaceholders } from "@/lib/templates";
export default function TemplateList({ environmentId }: { environmentId: string }) {
const [activeTemplate, setActiveTemplate] = useState<Template | null>(null);
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
const [selectedFilter, setSelectedFilter] = useState("All");
const categories = [
@@ -33,25 +35,6 @@ export default function TemplateList({ environmentId }: { environmentId: string
if (isLoadingProduct) return <LoadingSpinner />;
if (isErrorProduct) return <ErrorComponent />;
const customSurvey: Template = {
name: "Custom Survey",
description: "Create your survey from scratch.",
icon: null,
preset: {
name: "New Survey",
questions: [
{
id: createId(),
type: "openText",
headline: "What's poppin?",
subheader: "This can help us improve your experience.",
placeholder: "Type your answer here...",
required: true,
},
],
},
};
return (
<div className="flex h-full flex-col">
<TemplateMenuBar activeTemplate={activeTemplate} environmentId={environmentId} />
@@ -79,17 +62,20 @@ export default function TemplateList({ environmentId }: { environmentId: string
.map((template: Template) => (
<button
type="button"
onClick={() => setActiveTemplate(replacePresetPlaceholders(template, product))}
onClick={() => {
setActiveQuestionId(null);
setActiveTemplate(replacePresetPlaceholders(template, product));
}}
key={template.name}
className={cn(
activeTemplate?.name === template.name && "ring-brand ring-2",
"duration-120 group relative rounded-lg bg-white p-6 shadow transition-all duration-150 hover:scale-105"
)}>
<div className="absolute top-6 right-6 rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500">
<div className="absolute right-6 top-6 rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500">
{template.category}
</div>
<template.icon className="h-8 w-8" />
<h3 className="text-md mt-3 mb-1 text-left font-bold text-slate-700">{template.name}</h3>
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700">{template.name}</h3>
<p className="text-left text-xs text-slate-600">{template.description}</p>
</button>
))}
@@ -101,7 +87,7 @@ export default function TemplateList({ environmentId }: { environmentId: string
"duration-120 hover:border-brand-dark group relative rounded-lg border-2 border-dashed border-slate-300 bg-transparent p-8 transition-colors duration-150"
)}>
<PlusCircleIcon className="text-brand-dark h-8 w-8 transition-all duration-150 group-hover:scale-110" />
<h3 className="text-md mt-3 mb-1 text-left font-bold text-slate-700 ">{customSurvey.name}</h3>
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700 ">{customSurvey.name}</h3>
<p className="text-left text-xs text-slate-600 ">{customSurvey.description}</p>
</button>
</div>
@@ -114,9 +100,10 @@ export default function TemplateList({ environmentId }: { environmentId: string
</Link>
{activeTemplate && (
<PreviewSurvey
activeQuestionId={null}
activeQuestionId={activeQuestionId}
questions={activeTemplate.preset.questions}
brandColor={product.brandColor}
setActiveQuestionId={setActiveQuestionId}
/>
)}
</aside>

View File

@@ -14,6 +14,12 @@ import { ArrowRightCircleIcon } from "@formbricks/ui";
import type { Template } from "@formbricks/types/templates";
import { createId } from "@paralleldrive/cuid2";
const thankYouCardDefault = {
enabled: false,
headline: "Thank you!",
subheader: "We appreciate your time and insight.",
};
export const templates: Template[] = [
{
name: "Product Market Fit Survey",
@@ -81,6 +87,7 @@ export const templates: Template[] = [
required: true,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -179,6 +186,7 @@ export const templates: Template[] = [
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -247,6 +255,7 @@ export const templates: Template[] = [
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -287,6 +296,7 @@ export const templates: Template[] = [
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -334,6 +344,7 @@ export const templates: Template[] = [
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -380,6 +391,7 @@ export const templates: Template[] = [
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -439,6 +451,7 @@ export const templates: Template[] = [
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -476,6 +489,7 @@ export const templates: Template[] = [
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -512,6 +526,7 @@ export const templates: Template[] = [
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -558,6 +573,7 @@ export const templates: Template[] = [
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -589,6 +605,7 @@ export const templates: Template[] = [
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -623,6 +640,7 @@ export const templates: Template[] = [
required: true,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -669,6 +687,7 @@ export const templates: Template[] = [
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
/* {
@@ -690,3 +709,23 @@ export const templates: Template[] = [
},s
}, */
];
export const customSurvey: Template = {
name: "Custom Survey",
description: "Create your survey from scratch.",
icon: null,
preset: {
name: "New Survey",
questions: [
{
id: createId(),
type: "openText",
headline: "What's poppin?",
subheader: "This can help us improve your experience.",
placeholder: "Type your answer here...",
required: true,
},
],
thankYouCard: thankYouCardDefault,
},
};

View File

@@ -1,18 +1,31 @@
import { cn } from "@formbricks/lib/cn";
import { ReactNode, useEffect, useState } from "react";
import { ArrowPathIcon } from "@heroicons/react/24/solid";
export default function Modal({ children, isOpen }: { children: ReactNode; isOpen: boolean }) {
export default function Modal({
children,
isOpen,
reset,
}: {
children: ReactNode;
isOpen: boolean;
reset: () => void;
}) {
const [show, setShow] = useState(false);
useEffect(() => {
setShow(isOpen);
}, [isOpen]);
return (
<div
aria-live="assertive"
className="pointer-events-none absolute inset-0 flex items-end px-4 py-6 sm:p-6">
<div aria-live="assertive" className="absolute inset-0 flex cursor-pointer items-end px-4 py-6 sm:p-6">
<div className="flex w-full flex-col items-center sm:items-end">
<div className="mr-6 rounded-t bg-amber-400 px-3 text-sm font-semibold text-white">Preview</div>
<div
className="mr-6 flex items-center rounded-t bg-amber-500 px-3 text-sm font-semibold text-white hover:cursor-pointer"
onClick={reset}>
<ArrowPathIcon className="mr-1.5 mt-0.5 h-4 w-4 " />
Preview
</div>
<div
className={cn(
show ? "translate-x-0 opacity-100" : "translate-x-28 opacity-0",

View File

@@ -0,0 +1,33 @@
import type { Question } from "@formbricks/types/questions";
import OpenTextQuestion from "./OpenTextQuestion";
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
interface QuestionConditionalProps {
currentQuestion: Question;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function QuestionConditional({
currentQuestion,
onSubmit,
lastQuestion,
brandColor,
}: QuestionConditionalProps) {
return currentQuestion.type === "openText" ? (
<OpenTextQuestion
question={currentQuestion}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : currentQuestion.type === "multipleChoiceSingle" ? (
<MultipleChoiceSingleQuestion
question={currentQuestion}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : null;
}

View File

@@ -0,0 +1,50 @@
import Headline from "./Headline";
import Subheader from "./Subheader";
interface ThankYouCardProps {
headline: string;
subheader: string;
brandColor: string;
}
export default function ThankYouCard({ headline, subheader, brandColor }: ThankYouCardProps) {
return (
<div className="text-center">
<div className="flex items-center justify-center" style={{ color: brandColor }}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-24 w-24">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<span className="mb-[10px] inline-block h-1 w-16 rounded-[100%] bg-slate-300"></span>
<div>
<Headline headline={headline} questionId="thankYouCard" />
<Subheader subheader={subheader} questionId="thankYouCard" />
</div>
{/* <span
className="mb-[10px] mt-[35px] inline-block h-[2px] w-4/5 rounded-full opacity-25"
style={{ backgroundColor: brandColor }}></span>
<div>
<p className="text-xs text-slate-500">
Powered by{" "}
<b>
<a href="https://formbricks.com" target="_blank" className="hover:text-slate-700">
Formbricks
</a>
</b>
</p>
</div> */}
</div>
);
}

View File

@@ -20,7 +20,7 @@ export const getSettings = async (environmentId: string, personId: string): Prom
throw new Error("Product not found");
}
// get all surveys that meed the displayOption criteria
// get all surveys that meet the displayOption criteria
const potentialSurveys = await prisma.survey.findMany({
where: {
OR: [
@@ -60,8 +60,8 @@ export const getSettings = async (environmentId: string, personId: string): Prom
},
},
},
// last display
},
// last display
displays: {
where: {
personId,
@@ -74,6 +74,7 @@ export const getSettings = async (environmentId: string, personId: string): Prom
createdAt: true,
},
},
thankYouCard: true,
},
});
@@ -125,6 +126,7 @@ export const getSettings = async (environmentId: string, personId: string): Prom
id: survey.id,
questions: JSON.parse(JSON.stringify(survey.questions)),
triggers: survey.triggers,
thankYouCard: JSON.parse(JSON.stringify(survey.thankYouCard)),
};
});

View File

@@ -1,4 +1,4 @@
import { Bars4Icon, ListBulletIcon } from "@heroicons/react/24/solid";
import { Bars3BottomLeftIcon, ListBulletIcon } from "@heroicons/react/24/solid";
import { createId } from "@paralleldrive/cuid2";
export type QuestionType = {
@@ -14,7 +14,7 @@ export const questionTypes: QuestionType[] = [
id: "openText",
label: "Open text",
description: "A single line of text",
icon: Bars4Icon,
icon: Bars3BottomLeftIcon,
defaults: {
placeholder: "Type your answer here...",
},

View File

@@ -38,8 +38,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
else if (req.method === "POST") {
const eventClass = req.body;
console.log(eventClass);
if (eventClass.type === "automatic") {
res.status(400).json({ message: "You are not allowed to create new automatic events" });
}

View File

@@ -24,8 +24,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
// lastSyncedAt is the last time the environment was synced (iso string)
const { users }: { users: FormbricksUser[] } = req.body;
console.log(users);
for (const user of users) {
// check if user with this userId as attribute already exists
const existingUser = await prisma.person.findFirst({

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Survey" ADD COLUMN "thankYouCard" JSONB NOT NULL DEFAULT '{"enabled": false}';

View File

@@ -131,6 +131,7 @@ model Survey {
environmentId String
status SurveyStatus @default(draft)
questions Json @default("[]")
thankYouCard Json @default("{\"enabled\": false}")
responses Response[]
displayOption displayOptions @default(displayOnce)
recontactDays Int?

View File

@@ -1,10 +1,19 @@
import { h } from "preact";
export default function Headline({ headline, questionId }: { headline: string; questionId: string }) {
export default function Headline({
headline,
questionId,
style,
}: {
headline: string;
questionId: string;
style?: any;
}) {
return (
<label
htmlFor={questionId}
className="fb-block fb-text-base fb-font-semibold fb-leading-6 fb-mr-8 text-slate-900">
className="fb-block fb-text-base fb-font-semibold fb-leading-6 fb-mr-8 text-slate-900"
style={style}>
{headline}
</label>
);

View File

@@ -7,6 +7,7 @@ import { JsConfig, Survey } from "@formbricks/types/js";
import OpenTextQuestion from "./OpenTextQuestion";
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
import Progress from "./Progress";
import ThankYouCard from "./ThankYouCard";
interface SurveyViewProps {
config: JsConfig;
@@ -63,7 +64,14 @@ export default function SurveyView({ config, survey, close, brandColor }: Survey
setCurrentQuestion(survey.questions[questionIdx + 1]);
} else {
setProgress(100);
close();
if (survey.thankYouCard.enabled) {
setTimeout(() => {
close();
}, 2000);
} else {
close();
}
}
};
@@ -74,7 +82,13 @@ export default function SurveyView({ config, survey, close, brandColor }: Survey
loadingElement ? "fb-animate-pulse fb-opacity-60" : "",
"fb-p-4 fb-text-slate-800 fb-font-sans"
)}>
{currentQuestion.type === "multipleChoiceSingle" ? (
{progress === 100 && survey.thankYouCard.enabled ? (
<ThankYouCard
headline={survey.thankYouCard.headline}
subheader={survey.thankYouCard.subheader}
brandColor={config.settings?.brandColor}
/>
) : currentQuestion.type === "multipleChoiceSingle" ? (
<MultipleChoiceSingleQuestion
question={currentQuestion}
onSubmit={submitResponse}

View File

@@ -0,0 +1,53 @@
import { h } from "preact";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface ThankYouCardProps {
headline: string;
subheader: string;
brandColor: string;
}
export default function ThankYouCard({ headline, subheader, brandColor }: ThankYouCardProps) {
return (
<div className="fb-text-center">
<div className="fb-flex fb-items-center fb-justify-center" style={{ color: brandColor }}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="fb-h-24 fb-w-24">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<span className="fb-inline-block fb-rounded-[100%] fb-w-16 fb-h-1 fb-mb-[10px] fb-bg-slate-300"></span>
<div>
<Headline headline={headline} questionId="thankYouCard" style={{ "margin-right": 0 }} />
<Subheader subheader={subheader} questionId="thankYouCard" />
</div>
{/* <span
className="fb-inline-block fb-w-4/5 fb-h-[2px] fb-mt-[35px] fb-mb-[10px]"
style={{ backgroundColor: brandColor }}></span>
<div>
<p className="fb-text-xs fb-text-slate-500">
Powered by{" "}
<b>
<a href="https://formbricks.com" target="_blank" className="fb-hover:text-slate-700">
Formbricks
</a>
</b>
</p>
</div> */}
</div>
);
}

View File

@@ -31,6 +31,9 @@ module.exports = {
screens: {
xs: "430px",
},
scale: {
97: "0.97",
},
},
},
plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],

View File

@@ -70,6 +70,13 @@ export interface Survey {
id: string;
questions: Question[];
triggers: Trigger[];
thankYouCard: ThankYouCard;
}
export interface ThankYouCard {
enabled: boolean;
headline?: string;
subheader?: string;
}
export type Question = OpenTextQuestion | MultipleChoiceSingleQuestion;

View File

@@ -1,8 +1,14 @@
import type { Survey as PrismaSurvey } from "@prisma/client";
import { Question } from "./questions";
export interface Survey extends Omit<PrismaSurvey, "questions" | "triggers"> {
export interface ThankYouCard {
enabled: boolean;
headline?: string;
subheader?: string;
}
export interface Survey extends Omit<PrismaSurvey, "questions" | "triggers" | "thankYouCard"> {
questions: Question[];
thankYouCard: ThankYouCard;
triggers: string[];
numDisplays: number;
responseRate: number;

View File

@@ -8,5 +8,10 @@ export interface Template {
preset: {
name: string;
questions: Question[];
thankYouCard: {
enabled: boolean;
headline: string;
subheader: string;
};
};
}