Revamp survey settings (#287)

* improve survey settings flow

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Johannes
2023-05-19 08:33:52 +02:00
committed by GitHub
parent b76289c9d8
commit 41443267c9
29 changed files with 523 additions and 327 deletions

View File

@@ -32,8 +32,8 @@ const thankYouCardDefault = {
};
export const customSurvey: Template = {
name: "Custom Survey",
description: "Create your survey from scratch.",
name: "Start from scratch",
description: "Create a survey without template.",
icon: null,
preset: {
name: "New Survey",
@@ -41,8 +41,8 @@ export const customSurvey: Template = {
{
id: createId(),
type: "openText",
headline: "What's poppin?",
subheader: "This can help us improve your experience.",
headline: "Custom Survey",
subheader: "This is an example survey.",
placeholder: "Type your answer here...",
required: true,
},
@@ -226,7 +226,7 @@ export const templates: Template[] = [
},
{
id: createId(),
label: "in a Podcast",
label: "In a Podcast",
},
],
},
@@ -336,7 +336,7 @@ export const templates: Template[] = [
},
{
id: createId(),
label: "in a Podcast",
label: "In a Podcast",
},
],
},

View File

@@ -78,7 +78,7 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
to be identical to the frontend we're building in the next step.
</Callout>
6. Click on “Continue to Audience” or select the audience tab manually. Scroll down to “When to ask” and create a new Action:
6. Click on “Continue to Settings or select the audience tab manually. Scroll down to “When to ask” and create a new Action:
<Image
src={WhenToAsk}

View File

@@ -5,6 +5,7 @@ import { createProduct } from "@/lib/products/products";
import { Button, Input, Label } from "@formbricks/ui";
import { PlusCircleIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
interface AddProductModalProps {
@@ -15,12 +16,15 @@ interface AddProductModalProps {
export default function AddProductModal({ environmentId, open, setOpen }: AddProductModalProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const { register, handleSubmit } = useForm();
const submitProduct = async (data) => {
setLoading(true);
const newEnv = await createProduct(environmentId, data);
router.push(`/environments/${newEnv.id}/`);
setOpen(false);
setLoading(false);
};
return (
@@ -58,7 +62,7 @@ export default function AddProductModal({ environmentId, open, setOpen }: AddPro
}}>
Cancel
</Button>
<Button variant="primary" type="submit">
<Button variant="primary" type="submit" loading={loading}>
Add product
</Button>
</div>

View File

@@ -303,7 +303,21 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<div>
<p className="ph-no-capture break-all">{truncate(environment?.product?.name, 20)}</p>
<div className="flex items-center space-x-1">
<p className="">{truncate(environment?.product?.name, 20)}</p>
{!widgetSetupCompleted && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="mt-0.5 h-2 w-2 rounded-full bg-amber-500 hover:bg-amber-600"></div>
</TooltipTrigger>
<TooltipContent>
<p>Your app is not connected to Formbricks.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
<p className=" block text-xs text-slate-500">Product</p>
</div>
</DropdownMenuSubTrigger>

View File

@@ -2,44 +2,55 @@ import Modal from "@/components/preview/Modal";
import Progress from "@/components/preview/Progress";
import QuestionConditional from "@/components/preview/QuestionConditional";
import ThankYouCard from "@/components/preview/ThankYouCard";
import ContentWrapper from "@/components/shared/ContentWrapper";
import { useEnvironment } from "@/lib/environments/environments";
import type { Question } from "@formbricks/types/questions";
import { Survey } from "@formbricks/types/surveys";
import { ArrowPathIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react";
interface PreviewSurveyProps {
localSurvey?: Survey;
setActiveQuestionId: (id: string | null) => void;
activeQuestionId?: string | null;
questions: Question[];
brandColor: string;
environmentId: string;
surveyType: Survey["type"];
thankYouCard: Survey["thankYouCard"];
previewType?: "modal" | "fullwidth" | "email";
}
export default function PreviewSurvey({
localSurvey,
setActiveQuestionId,
activeQuestionId,
questions,
brandColor,
environmentId,
surveyType,
thankYouCard,
previewType,
}: PreviewSurveyProps) {
const [isModalOpen, setIsModalOpen] = useState(true);
const [progress, setProgress] = useState(0); // [0, 1]
const [widgetSetupCompleted, setWidgetSetupCompleted] = useState(false);
const { environment } = useEnvironment(environmentId);
const [lastActiveQuestionId, setLastActiveQuestionId] = useState("");
useEffect(() => {
if (activeQuestionId && localSurvey) {
setProgress(calculateProgress(localSurvey));
if (activeQuestionId) {
setLastActiveQuestionId(activeQuestionId);
setProgress(calculateProgress(questions, activeQuestionId));
} else if (lastActiveQuestionId) {
setProgress(calculateProgress(questions, lastActiveQuestionId));
}
function calculateProgress(survey) {
const elementIdx = survey.questions.findIndex((e) => e.id === activeQuestionId);
return elementIdx / survey.questions.length;
function calculateProgress(questions, id) {
const elementIdx = questions.findIndex((e) => e.id === id);
return elementIdx / questions.length;
}
}, [activeQuestionId, localSurvey]);
}, [activeQuestionId, lastActiveQuestionId, questions]);
useEffect(() => {
// close modal if there are no questions left
if (localSurvey?.type === "web" && !localSurvey?.thankYouCard.enabled) {
if (surveyType === "web" && !thankYouCard.enabled) {
if (activeQuestionId === "thank-you-card") {
setIsModalOpen(false);
setTimeout(() => {
@@ -48,14 +59,16 @@ export default function PreviewSurvey({
}, 500);
}
}
}, [activeQuestionId, localSurvey, questions, setActiveQuestionId]);
}, [activeQuestionId, surveyType, questions, setActiveQuestionId, thankYouCard]);
const gotoNextQuestion = () => {
const currentIndex = questions.findIndex((q) => q.id === activeQuestionId);
const currentQuestionId = activeQuestionId || lastActiveQuestionId;
const currentIndex = questions.findIndex((q) => q.id === currentQuestionId);
if (currentIndex < questions.length - 1) {
setActiveQuestionId(questions[currentIndex + 1].id);
} else {
if (localSurvey?.thankYouCard?.enabled) {
if (thankYouCard?.enabled) {
setActiveQuestionId("thank-you-card");
} else {
setIsModalOpen(false);
@@ -63,7 +76,7 @@ export default function PreviewSurvey({
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
if (localSurvey?.thankYouCard?.enabled) {
if (thankYouCard?.enabled) {
setActiveQuestionId("thank-you-card");
setProgress(1);
} else {
@@ -77,82 +90,94 @@ export default function PreviewSurvey({
}
};
const resetPreview = () => {
/* const resetPreview = () => {
setIsModalOpen(false);
setTimeout(() => {
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
};
*/
if (!activeQuestionId) {
return null;
useEffect(() => {
if (environment && environment.widgetSetupCompleted) {
setWidgetSetupCompleted(true);
} else {
setWidgetSetupCompleted(false);
}
}, [environment]);
if (!previewType) {
previewType = widgetSetupCompleted ? "modal" : "fullwidth";
}
return (
<>
{localSurvey?.type === "link" ? (
<div className="relative flex h-full flex-1 flex-shrink-0 flex-col overflow-hidden border border-amber-400">
<div
className="absolute right-3 mr-6 flex items-center rounded-b bg-amber-500 px-3 text-sm font-semibold text-white opacity-100 transition-all duration-500 ease-in-out hover:cursor-pointer"
onClick={resetPreview}>
<ArrowPathIcon className="mr-1.5 mt-0.5 h-4 w-4 " />
Preview
</div>
<div className="flex h-full flex-1 items-center overflow-y-auto bg-white">
<ContentWrapper className="w-full md:max-w-lg">
{activeQuestionId == "thank-you-card" ? (
<div className="my-4 flex h-full w-5/6 flex-col rounded-lg border border-slate-300 bg-slate-200 ">
<div className="flex h-8 items-center rounded-t-lg bg-slate-100">
<div className="ml-6 flex space-x-2">
<div className="h-3 w-3 rounded-full bg-red-500"></div>
<div className="h-3 w-3 rounded-full bg-amber-500"></div>
<div className="h-3 w-3 rounded-full bg-emerald-500"></div>
</div>
<p>
{previewType === "modal" && <p className="ml-4 font-mono text-sm text-slate-400">Your web app</p>}
</p>
</div>
{previewType === "modal" ? (
<Modal isOpen={isModalOpen}>
{(activeQuestionId || lastActiveQuestionId) === "thank-you-card" ? (
<ThankYouCard
brandColor={brandColor}
headline={thankYouCard?.headline || "Thank you!"}
subheader={thankYouCard?.subheader || "We appreciate your feedback."}
/>
) : (
questions.map((question, idx) =>
(activeQuestionId || lastActiveQuestionId) === question.id ? (
<QuestionConditional
key={question.id}
question={question}
brandColor={brandColor}
lastQuestion={idx === questions.length - 1}
onSubmit={gotoNextQuestion}
/>
) : null
)
)}
</Modal>
) : (
<div className="flex flex-grow flex-col">
<div className="flex w-full flex-grow flex-col items-center justify-center bg-white">
<div className="w-full max-w-md">
{(activeQuestionId || lastActiveQuestionId) === "thank-you-card" ? (
<ThankYouCard
brandColor={brandColor}
headline={localSurvey?.thankYouCard?.headline || ""}
subheader={localSurvey?.thankYouCard?.subheader || ""}
headline={thankYouCard?.headline || "Thank you!"}
subheader={thankYouCard?.subheader || "We appreciate your feedback."}
/>
) : (
questions.map(
(question, idx) =>
activeQuestionId === question.id && (
<QuestionConditional
key={question.id}
question={question}
brandColor={brandColor}
lastQuestion={idx === questions.length - 1}
onSubmit={gotoNextQuestion}
/>
)
questions.map((question, idx) =>
(activeQuestionId || lastActiveQuestionId) === question.id ? (
<QuestionConditional
key={question.id}
question={question}
brandColor={brandColor}
lastQuestion={idx === questions.length - 1}
onSubmit={gotoNextQuestion}
/>
) : null
)
)}
</ContentWrapper>
</div>
</div>
<div className="top-0 z-10 w-full border-b bg-white">
<div className="mx-auto max-w-md p-6">
<div className="z-10 w-full rounded-b-lg bg-white">
<div className="mx-auto max-w-md p-6 pt-4">
<Progress progress={progress} brandColor={brandColor} />
</div>
</div>
</div>
) : (
<Modal isOpen={isModalOpen} reset={resetPreview}>
{activeQuestionId == "thank-you-card" ? (
<ThankYouCard
brandColor={brandColor}
headline={localSurvey?.thankYouCard?.headline || ""}
subheader={localSurvey?.thankYouCard?.subheader || ""}
/>
) : (
questions.map(
(question, idx) =>
activeQuestionId === question.id && (
<QuestionConditional
key={question.id}
question={question}
brandColor={brandColor}
lastQuestion={idx === questions.length - 1}
onSubmit={gotoNextQuestion}
/>
)
)
)}
</Modal>
)}
</>
</div>
);
}

View File

@@ -11,6 +11,7 @@ import {
} from "@/components/shared/DropdownMenu";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator";
import { useEnvironment } from "@/lib/environments/environments";
import { useProfile } from "@/lib/profile";
import { createSurvey, deleteSurvey, duplicateSurvey, useSurveys } from "@/lib/surveys/surveys";
import { Badge, ErrorComponent } from "@formbricks/ui";
@@ -33,6 +34,7 @@ export default function SurveysList({ environmentId }) {
const router = useRouter();
const { surveys, mutateSurveys, isLoadingSurveys, isErrorSurveys } = useSurveys(environmentId);
const { isLoadingProfile, isErrorProfile } = useProfile();
const { environment } = useEnvironment(environmentId);
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isCreateSurveyLoading, setIsCreateSurveyLoading] = useState(false);
@@ -46,8 +48,12 @@ export default function SurveysList({ environmentId }) {
const newSurveyFromTemplate = async (template: Template) => {
setIsCreateSurveyLoading(true);
const augmentedTemplate = {
...template.preset,
type: environment?.widgetSetupCompleted ? "web" : "link",
};
try {
const survey = await createSurvey(environmentId, template.preset);
const survey = await createSurvey(environmentId, augmentedTemplate);
router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);
} catch (e) {
toast.error("An error occured creating a new survey");
@@ -89,7 +95,7 @@ export default function SurveysList({ environmentId }) {
if (surveys.length === 0) {
return (
<div className="mx-auto flex w-full max-w-5xl flex-col py-24">
<div className="mx-auto flex w-full max-w-5xl flex-col py-12">
{isCreateSurveyLoading ? (
<LoadingSpinner />
) : (
@@ -116,7 +122,7 @@ export default function SurveysList({ environmentId }) {
<ul className="grid grid-cols-2 place-content-stretch gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-5 ">
<button onClick={() => newSurvey()}>
<li className="col-span-1 h-56">
<div className="from-brand-light to-brand-dark delay-50 flex h-full items-center justify-center overflow-hidden rounded-md bg-gradient-to-b font-light text-white shadow transition ease-in-out hover:scale-105">
<div className="delay-50 flex h-full items-center justify-center overflow-hidden rounded-md bg-gradient-to-br from-slate-900 to-slate-800 font-light text-white shadow transition ease-in-out hover:scale-105 hover:from-slate-800 hover:to-slate-700">
<div id="main-cta" className="px-4 py-8 sm:p-14 xl:p-10">
<PlusIcon className="stroke-thin mx-auto h-14 w-14" />
Create Survey

View File

@@ -13,8 +13,12 @@ interface AudienceViewProps {
export default function AudienceView({ environmentId, localSurvey, setLocalSurvey }: AudienceViewProps) {
return (
<div className="space-y-3 p-5">
<HowToSendCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
<div className="mt-12 space-y-3 p-5">
<HowToSendCard
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
environmentId={environmentId}
/>
<WhoToSendCard
localSurvey={localSurvey}

View File

@@ -1,5 +1,6 @@
"use client";
import { useEnvironment } from "@/lib/environments/environments";
import { cn } from "@formbricks/lib/cn";
import type { Survey } from "@formbricks/types/surveys";
import { Badge, Label, RadioGroup, RadioGroupItem } from "@formbricks/ui";
@@ -8,49 +9,31 @@ import {
ComputerDesktopIcon,
DevicePhoneMobileIcon,
EnvelopeIcon,
ExclamationCircleIcon,
LinkIcon,
} from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
const options = [
{
id: "web",
name: "In-app (Popup)",
icon: ComputerDesktopIcon,
description: "Survey users inside of your web application.",
comingSoon: false,
},
{
id: "link",
name: "Link survey",
icon: LinkIcon,
description: "Creates a standalone survey to share via link.",
comingSoon: false,
},
{
id: "mobile",
name: "Mobile app",
icon: DevicePhoneMobileIcon,
description: "Survey users inside a mobile app (iOS & Android).",
comingSoon: true,
},
{
id: "email",
name: "Email",
icon: EnvelopeIcon,
description: "Send email surveys to your user base with your current email provider.",
comingSoon: true,
},
];
import Link from "next/link";
import { useEffect, useState } from "react";
interface HowToSendCardProps {
localSurvey: Survey;
setLocalSurvey: (survey: Survey) => void;
environmentId: string;
}
export default function HowToSendCard({ localSurvey, setLocalSurvey }: HowToSendCardProps) {
const [open, setOpen] = useState(false);
export default function HowToSendCard({ localSurvey, setLocalSurvey, environmentId }: HowToSendCardProps) {
const [open, setOpen] = useState(localSurvey.type === "web" ? false : true);
const [widgetSetupCompleted, setWidgetSetupCompleted] = useState(false);
const { environment } = useEnvironment(environmentId);
useEffect(() => {
if (environment && environment.widgetSetupCompleted) {
setWidgetSetupCompleted(true);
} else {
setWidgetSetupCompleted(false);
}
}, [environment]);
const setSurveyType = (type: string) => {
const updatedSurvey = JSON.parse(JSON.stringify(localSurvey));
@@ -61,6 +44,41 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey }: HowToSend
setLocalSurvey(updatedSurvey);
};
const options = [
{
id: "web",
name: "Web App",
icon: ComputerDesktopIcon,
description: "Embed a survey in your web app to collect responses.",
comingSoon: false,
alert: !widgetSetupCompleted,
},
{
id: "link",
name: "Link survey",
icon: LinkIcon,
description: "Share a link to a survey page.",
comingSoon: false,
alert: false,
},
{
id: "mobile",
name: "Mobile app",
icon: DevicePhoneMobileIcon,
description: "Survey users inside a mobile app (iOS & Android).",
comingSoon: true,
alert: false,
},
{
id: "email",
name: "Email",
icon: EnvelopeIcon,
description: "Send email surveys to your user base with your current email provider.",
comingSoon: true,
alert: false,
},
];
return (
<Collapsible.Root
open={open}
@@ -70,7 +88,7 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey }: HowToSend
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
)}>
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
<div className="inline-flex px-4 py-6">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
<CheckCircleIcon className="h-8 w-8 text-green-400" />
</div>
@@ -120,10 +138,30 @@ export default function HowToSendCard({ localSurvey, setLocalSurvey }: HowToSend
{option.name}
</p>
{option.comingSoon && (
<Badge text="coming soon" size="normal" type="warning" className="ml-2" />
<Badge text="coming soon" size="normal" type="success" className="ml-2" />
)}
</div>
<p className="mt-2 text-xs font-normal text-slate-600">{option.description}</p>
{option.alert && (
<div className="mt-2 flex items-center space-x-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-2">
<ExclamationCircleIcon className="h-5 w-5 text-amber-500" />
<div className=" text-amber-800">
<p className="text-xs font-semibold">
Your app is not yet connected to Formbricks.
</p>
<p className="text-xs font-normal">
Follow the{" "}
<Link
href={`/environments/${environmentId}/settings/setup`}
className="underline hover:text-amber-900"
target="_blank">
set up guide
</Link>{" "}
to connect Formbricks and launch surveys in your app.
</p>
</div>
</div>
)}
</div>
</div>
</Label>

View File

@@ -66,7 +66,7 @@ export default function QuestionCard({
<div
className={cn(
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"
"top-0 w-10 rounded-l-lg p-2 text-center text-sm text-white hover:bg-slate-600"
)}>
{questionIdx + 1}
</div>
@@ -74,14 +74,15 @@ export default function QuestionCard({
open={open}
onOpenChange={() => {
if (activeQuestionId !== question.id) {
// only be able to open other question
setActiveQuestionId(question.id);
} else {
setActiveQuestionId(null);
}
}}
className="flex-1 rounded-r-lg border border-slate-200">
<Collapsible.CollapsibleTrigger
asChild
className={cn(open ? "" : "cursor-pointer hover:bg-slate-50 ", "flex justify-between p-4")}>
className={cn(open ? "" : " ", "flex cursor-pointer justify-between p-4 hover:bg-slate-50")}>
<div>
<div className="inline-flex">
<div className="-ml-0.5 mr-3 h-6 w-6 text-slate-400">

View File

@@ -1,8 +1,8 @@
import { cn } from "@formbricks/lib/cn";
import { QuestionMarkCircleIcon, UserGroupIcon } from "@heroicons/react/24/solid";
import { QueueListIcon, Cog8ToothIcon } from "@heroicons/react/24/solid";
interface Tab {
id: "questions" | "audience";
id: "questions" | "settings";
label: string;
icon: JSX.Element;
}
@@ -11,23 +11,23 @@ const tabs: Tab[] = [
{
id: "questions",
label: "Questions",
icon: <QuestionMarkCircleIcon />,
icon: <QueueListIcon />,
},
{
id: "audience",
label: "Audience",
icon: <UserGroupIcon />,
id: "settings",
label: "Settings",
icon: <Cog8ToothIcon />,
},
];
interface QuestionsAudienceTabsProps {
activeId: "questions" | "audience";
setActiveId: (id: "questions" | "audience") => void;
activeId: "questions" | "settings";
setActiveId: (id: "questions" | "settings") => void;
}
export default function QuestionsAudienceTabs({ activeId, setActiveId }: QuestionsAudienceTabsProps) {
return (
<div className="flex h-14 w-full items-center justify-center border bg-white">
<div className="fixed z-10 flex h-14 w-1/2 items-center justify-center border bg-white">
<nav className="flex h-full items-center space-x-4" aria-label="Tabs">
{tabs.map((tab) => (
<button

View File

@@ -86,7 +86,7 @@ export default function QuestionsView({
};
return (
<div className="px-5 py-4">
<div className="mt-12 px-5 py-4">
<DragDropContext onDragEnd={onDragEnd}>
<div className="mb-5 grid grid-cols-1 gap-5 ">
<StrictModeDroppable droppableId="questionsList">

View File

@@ -2,11 +2,11 @@
import { cn } from "@formbricks/lib/cn";
import type { Survey } from "@formbricks/types/surveys";
import { Checkbox, Input, Label, RadioGroup, RadioGroupItem } from "@formbricks/ui";
import { Badge, Input, Label, RadioGroup, RadioGroupItem, Switch } from "@formbricks/ui";
import { CheckCircleIcon } from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import Link from "next/link";
import { useState } from "react";
import { useEffect, useState } from "react";
interface DisplayOption {
id: "displayOnce" | "displayMultiple" | "respondMultiple";
@@ -17,8 +17,8 @@ interface DisplayOption {
const displayOptions: DisplayOption[] = [
{
id: "displayOnce",
name: "Only once, even if they do not respond",
description: "The survey won't be shown again, if person doesn't respond.",
name: "Show only once",
description: "The survey will be shown once, even if person doesn't respond.",
},
{
id: "displayMultiple",
@@ -27,7 +27,7 @@ const displayOptions: DisplayOption[] = [
},
{
id: "respondMultiple",
name: "Always, when the conditions match",
name: "Keep showing while conditions match",
description: "Even after they submitted a response (e.g. Feedback Box)",
},
];
@@ -67,22 +67,36 @@ export default function RecontactOptionsCard({
setLocalSurvey(updatedSurvey);
};
if (localSurvey.type === "link") {
useEffect(() => {
if (localSurvey.type === "link") {
setOpen(false);
}
}, [localSurvey.type]);
/* if (localSurvey.type === "link") {
return null;
}
} */
return (
<Collapsible.Root
open={open}
onOpenChange={setOpen}
className={cn(
open ? "" : "hover:bg-slate-50",
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
)}>
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
<div className="inline-flex px-4 py-6">
onOpenChange={(openState) => {
if (localSurvey.type !== "link") {
setOpen(openState);
}
}}
className="w-full rounded-lg border border-slate-300 bg-white">
<Collapsible.CollapsibleTrigger
asChild
className={cn(
localSurvey.type !== "link" ? "cursor-pointer hover:bg-slate-50" : "cursor-not-allowed bg-slate-50",
"h-full w-full rounded-lg "
)}>
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
<CheckCircleIcon className="h-8 w-8 text-green-400" />
<CheckCircleIcon
className={cn(localSurvey.type !== "link" ? "text-green-400" : "text-slate-300", "h-8 w-8 ")}
/>
</div>
<div>
<p className="font-semibold text-slate-800">Recontact Options</p>
@@ -90,6 +104,11 @@ export default function RecontactOptionsCard({
Decide how often people can answer this survey.
</p>
</div>
{localSurvey.type === "link" && (
<div className="flex w-full items-center justify-end pr-2">
<Badge size="normal" text="In-app survey settings" type="warning" />
</div>
)}
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="pb-3">
@@ -127,7 +146,8 @@ export default function RecontactOptionsCard({
<div className="p-3">
<div className="ml-2 flex items-center space-x-1">
<Checkbox id="recontactDays" checked={ignoreWaiting} onCheckedChange={handleCheckMark} />
<Switch id="recontactDays" checked={ignoreWaiting} onCheckedChange={handleCheckMark} />
{/* <Checkbox id="recontactDays" checked={ignoreWaiting} onCheckedChange={handleCheckMark} /> */}
<Label htmlFor="recontactDays" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Ignore waiting time between surveys</h3>

View File

@@ -30,12 +30,9 @@ export default function ResponseOptionsCard({}: ResponseOptionsCardProps) {
<Collapsible.Root
open={open}
onOpenChange={setOpen}
className={cn(
open ? "" : "hover:bg-slate-50",
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
)}>
className="w-full rounded-lg border border-slate-300 bg-white">
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
<div className="inline-flex px-4 py-6">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
<CheckCircleIcon className="h-8 w-8 text-green-400" />
</div>

View File

@@ -5,7 +5,6 @@ import { useProduct } from "@/lib/products/products";
import { useSurvey } from "@/lib/surveys/surveys";
import type { Survey } from "@formbricks/types/surveys";
import { ErrorComponent } from "@formbricks/ui";
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
import { useEffect, useState } from "react";
import PreviewSurvey from "../../PreviewSurvey";
import AudienceView from "./AudienceView";
@@ -19,7 +18,7 @@ interface SurveyEditorProps {
}
export default function SurveyEditor({ environmentId, surveyId }: SurveyEditorProps) {
const [activeView, setActiveView] = useState<"questions" | "audience">("questions");
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
const [localSurvey, setLocalSurvey] = useState<Survey | null>();
@@ -57,13 +56,6 @@ export default function SurveyEditor({ environmentId, surveyId }: SurveyEditorPr
/>
<div className="relative z-0 flex flex-1 overflow-hidden">
<main className="relative z-0 flex-1 overflow-y-auto focus:outline-none">
{survey.status !== "draft" && (
<div className="flex items-center border border-red-200 bg-red-100 p-2 text-sm text-slate-700 shadow-sm">
<ExclamationTriangleIcon className="mr-3 h-6 w-6 text-red-400" />
You&apos;re editing a published survey. Be cautious when making changes, they might mess up the
data.
</div>
)}
<QuestionsAudienceTabs activeId={activeView} setActiveId={setActiveView} />
{activeView === "questions" ? (
<QuestionsView
@@ -81,13 +73,16 @@ export default function SurveyEditor({ environmentId, surveyId }: SurveyEditorPr
/>
)}
</main>
<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">
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-100 bg-slate-50 md:flex md:flex-col">
<PreviewSurvey
activeQuestionId={activeQuestionId}
setActiveQuestionId={setActiveQuestionId}
questions={localSurvey.questions}
brandColor={product.brandColor}
localSurvey={localSurvey}
environmentId={environmentId}
surveyType={localSurvey.type}
thankYouCard={localSurvey.thankYouCard}
previewType={localSurvey.type === "web" ? "modal" : "fullwidth"}
/>
</aside>
</div>

View File

@@ -1,10 +1,13 @@
"use client";
import DeleteDialog from "@/components/shared/DeleteDialog";
import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown";
import { useProduct } from "@/lib/products/products";
import { useSurveyMutation } from "@/lib/surveys/mutateSurveys";
import { deleteSurvey } from "@/lib/surveys/surveys";
import type { Survey } from "@formbricks/types/surveys";
import { Button, Input } from "@formbricks/ui";
import { ArrowLeftIcon, UserGroupIcon } from "@heroicons/react/24/solid";
import { ArrowLeftIcon, Cog8ToothIcon, ExclamationTriangleIcon } from "@heroicons/react/24/solid";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import toast from "react-hot-toast";
@@ -13,8 +16,8 @@ interface SurveyMenuBarProps {
localSurvey: Survey;
setLocalSurvey: (survey: Survey) => void;
environmentId: string;
activeId: "questions" | "audience";
setActiveId: (id: "questions" | "audience") => void;
activeId: "questions" | "settings";
setActiveId: (id: "questions" | "settings") => void;
}
export default function SurveyMenuBar({
@@ -27,9 +30,11 @@ export default function SurveyMenuBar({
const router = useRouter();
const { triggerSurveyMutate, isMutatingSurvey } = useSurveyMutation(environmentId, localSurvey.id);
const [audiencePrompt, setAudiencePrompt] = useState(true);
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const { product } = useProduct(environmentId);
useEffect(() => {
if (audiencePrompt && activeId === "audience") {
if (audiencePrompt && activeId === "settings") {
setAudiencePrompt(false);
}
}, [activeId, audiencePrompt]);
@@ -40,36 +45,62 @@ export default function SurveyMenuBar({
setLocalSurvey(updatedSurvey);
};
const deleteSurveyAction = async (survey) => {
try {
await deleteSurvey(environmentId, survey.id);
setDeleteDialogOpen(false);
router.back();
} catch (error) {
console.log("An error occured deleting the survey");
}
};
const handleBack = () => {
if (localSurvey.createdAt === localSurvey.updatedAt && localSurvey.status === "draft") {
setDeleteDialogOpen(true);
console.log(localSurvey);
} else {
router.back();
}
};
return (
<div className="border-b border-slate-200 bg-white px-5 py-3 sm:flex sm:items-center sm:justify-between">
<div className="flex space-x-2 whitespace-nowrap">
<div className="flex items-center space-x-2 whitespace-nowrap">
<Button
variant="minimal"
className="px-0"
variant="secondary"
StartIcon={ArrowLeftIcon}
onClick={() => {
router.back();
handleBack();
}}>
<ArrowLeftIcon className="h-5 w-5 text-slate-700" />
Back
</Button>
<p className="pl-4 font-semibold">{product.name} / </p>
<Input
defaultValue={localSurvey.name}
onChange={(e) => {
const updatedSurvey = { ...localSurvey, name: e.target.value };
setLocalSurvey(updatedSurvey);
}}
className="w-72"
className="w-72 border-white hover:border-slate-200 "
/>
<div className="flex items-center">
</div>
{localSurvey?.responseRate && (
<div className="mx-auto flex items-center rounded-full border border-amber-200 bg-amber-100 p-2 text-sm text-amber-700 shadow-sm">
<ExclamationTriangleIcon className="mr-2 h-5 w-5 text-amber-400" />
This survey received responses. To keep the data consistent, make changes with caution.
</div>
)}
<div className="mt-3 flex sm:ml-4 sm:mt-0">
<div className="mr-4 flex items-center">
<SurveyStatusDropdown
surveyId={localSurvey.id}
environmentId={environmentId}
updateLocalSurveyStatus={updateLocalSurveyStatus}
/>
</div>
</div>
<div className="mt-3 flex sm:ml-4 sm:mt-0">
<Button
variant="secondary"
variant={localSurvey.status === "draft" ? "secondary" : "darkCTA"}
className="mr-3"
loading={isMutatingSurvey}
onClick={() => {
@@ -78,25 +109,30 @@ export default function SurveyMenuBar({
if (!response?.ok) {
throw new Error(await response?.text());
}
const updatedSurvey = await response.json();
setLocalSurvey(updatedSurvey); // update local survey state
toast.success("Changes saved.");
if (localSurvey.status !== "draft") {
router.push(`/environments/${environmentId}/surveys/${localSurvey.id}/summary`);
} else {
router.push(`/environments/${environmentId}/surveys`);
}
})
.catch(() => {
toast.error(`Error saving changes`);
});
}}>
Save Changes
Save
</Button>
{localSurvey.status === "draft" && audiencePrompt && (
<Button
variant="highlight"
variant="darkCTA"
onClick={() => {
setAudiencePrompt(false);
setActiveId("audience");
}}>
<UserGroupIcon className="mr-1 h-4 w-4" /> Continue to Audience
setActiveId("settings");
}}
EndIcon={Cog8ToothIcon}>
Continue to Settings
</Button>
)}
{localSurvey.status === "draft" && !audiencePrompt && (
@@ -105,16 +141,23 @@ export default function SurveyMenuBar({
localSurvey.type === "web" &&
(localSurvey.triggers[0] === "" || localSurvey.triggers.length === 0)
}
variant="highlight"
variant="darkCTA"
loading={isMutatingSurvey}
onClick={async () => {
await triggerSurveyMutate({ ...localSurvey, status: "inProgress" });
router.push(`/environments/${environmentId}/surveys/${localSurvey.id}/summary?success=true`);
}}>
Publish Survey
Publish
</Button>
)}
</div>
<DeleteDialog
deleteWhat="Draft"
open={isDeleteDialogOpen}
setOpen={setDeleteDialogOpen}
onDelete={() => deleteSurveyAction(localSurvey)}
text="Do you want to delete this draft?"
/>
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { useEventClasses } from "@/lib/eventClasses/eventClasses";
import { cn } from "@formbricks/lib/cn";
import type { Survey } from "@formbricks/types/surveys";
import {
Badge,
Button,
Select,
SelectContent,
@@ -25,7 +26,7 @@ interface WhenToSendCardProps {
}
export default function WhenToSendCard({ environmentId, localSurvey, setLocalSurvey }: WhenToSendCardProps) {
const [open, setOpen] = useState(true);
const [open, setOpen] = useState(localSurvey.type === "web" ? true : false);
const { eventClasses, isLoadingEventClasses, isErrorEventClasses, mutateEventClasses } =
useEventClasses(environmentId);
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
@@ -48,6 +49,12 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
setLocalSurvey(updatedSurvey);
};
useEffect(() => {
if (localSurvey.type === "link") {
setOpen(false);
}
}, [localSurvey.type]);
//create new empty trigger on page load, remove one click for user
useEffect(() => {
if (localSurvey.triggers.length === 0) {
@@ -63,35 +70,60 @@ export default function WhenToSendCard({ environmentId, localSurvey, setLocalSur
return <div>Error</div>;
}
if (localSurvey.type === "link") {
/* if (localSurvey.type === "link") {
return null;
}
} */
return (
<>
<Collapsible.Root
open={open}
onOpenChange={setOpen}
className={cn(
open ? "" : "hover:bg-slate-50",
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
)}>
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
<div className="inline-flex px-4 py-6">
onOpenChange={(openState) => {
if (localSurvey.type !== "link") {
setOpen(openState);
}
}}
className="w-full rounded-lg border border-slate-300 bg-white">
<Collapsible.CollapsibleTrigger
asChild
className={cn(
localSurvey.type !== "link"
? "cursor-pointer hover:bg-slate-50"
: "cursor-not-allowed bg-slate-50",
"h-full w-full rounded-lg "
)}>
<div className="inline-flex px-4 py-4">
<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" />
<div
className={cn(
localSurvey.type !== "link"
? "border-amber-500 bg-amber-50"
: "border-slate-300 bg-slate-100",
"h-7 w-7 rounded-full border "
)}
/>
) : (
<CheckCircleIcon className="h-8 w-8 text-green-400" />
<CheckCircleIcon
className={cn(
localSurvey.type !== "link" ? "text-green-400" : "text-slate-300",
"h-8 w-8 "
)}
/>
)}
</div>
<div>
<p className="font-semibold text-slate-800">When to ask</p>
<p className="font-semibold text-slate-800">Survey Trigger</p>
<p className="mt-1 truncate text-sm text-slate-500">
Choose the actions which trigger the survey.
</p>
</div>
{localSurvey.type === "link" && (
<div className="flex w-full items-center justify-end pr-2">
<Badge size="normal" text="In-app survey settings" type="warning" />
</div>
)}
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="">

View File

@@ -34,6 +34,12 @@ export default function WhoToSendCard({ environmentId, localSurvey, setLocalSurv
}
}, [isLoadingAttributeClasses]);
useEffect(() => {
if (localSurvey.type === "link") {
setOpen(false);
}
}, [localSurvey.type]);
const addAttributeFilter = () => {
const updatedSurvey = { ...localSurvey };
updatedSurvey.attributeFilters = [
@@ -66,26 +72,26 @@ export default function WhoToSendCard({ environmentId, localSurvey, setLocalSurv
return <div>Error</div>;
}
if (localSurvey.type === "link") {
return null;
}
return (
<>
<Collapsible.Root
open={open}
onOpenChange={setOpen}
className={cn(
open ? "" : "hover:bg-slate-50",
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
)}>
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
className="w-full rounded-lg border border-slate-300 bg-white">
<Collapsible.CollapsibleTrigger
asChild
className={cn(
localSurvey.type !== "link"
? "cursor-pointer hover:bg-slate-50"
: "cursor-not-allowed bg-slate-50",
"h-full w-full rounded-lg "
)}>
<div className="inline-flex px-4 py-6">
<div className="flex items-center pl-2 pr-5">
<CheckCircleIcon className="h-8 w-8 text-green-400" />
</div>
<div>
<p className="font-semibold text-slate-800">Who to ask</p>
<p className="font-semibold text-slate-800">Target Audience</p>
<p className="mt-1 truncate text-sm text-slate-500">
Pre-segment your users with attributes filters.
</p>
@@ -98,21 +104,21 @@ export default function WhoToSendCard({ environmentId, localSurvey, setLocalSurv
<div className="mx-6 flex items-center rounded-lg border border-slate-200 p-4 text-slate-800">
<div>
{localSurvey.attributeFilters.length === 0 ? (
<UserGroupIcon className="mr-4 h-6 w-6 text-slate-700" />
<UserGroupIcon className="mr-4 h-6 w-6 text-slate-600" />
) : (
<FunnelIcon className="mr-4 h-6 w-6 text-slate-700" />
<FunnelIcon className="mr-4 h-6 w-6 text-slate-600" />
)}
</div>
<div>
<p className="">
Audience:{" "}
Current:{" "}
<span className="font-semibold text-slate-900">
{localSurvey.attributeFilters.length === 0 ? "All users" : "Filtered"}
</span>
</p>
<p className="mt-1 text-sm text-slate-500">
{localSurvey.attributeFilters.length === 0
? "Currently, all users might see the survey."
? "All users can see the survey."
: "Only users who match the attribute filter will see the survey."}
</p>
</div>

View File

@@ -15,7 +15,7 @@ import {
TooltipTrigger,
} from "@formbricks/ui";
import { ShareIcon } from "@heroicons/react/24/outline";
import { PencilSquareIcon } from "@heroicons/react/24/solid";
import { PencilSquareIcon, QuestionMarkCircleIcon } from "@heroicons/react/24/solid";
import { useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
@@ -84,7 +84,10 @@ export default function SummaryMetadata({ surveyId, environmentId }) {
<Tooltip>
<TooltipTrigger>
<div className="flex h-full cursor-default flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 text-left shadow-sm">
<p className="text-sm text-slate-600">Response Rate</p>
<p className="text-sm text-slate-600">
Response %
<QuestionMarkCircleIcon className="mb-1 ml-2 inline h-4 w-4 text-slate-500" />
</p>
<p className="text-2xl font-bold text-slate-800">
{survey.responseRate === null || survey.responseRate === 0 ? (
<span>-</span>
@@ -102,8 +105,11 @@ export default function SummaryMetadata({ surveyId, environmentId }) {
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<div className="flex flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 text-left shadow-sm">
<p className="text-sm text-slate-600">Completion Rate</p>
<div className="flex cursor-default flex-col justify-between space-y-2 rounded-lg border border-slate-200 bg-white p-4 text-left shadow-sm">
<p className="text-sm text-slate-600">
Completion %
<QuestionMarkCircleIcon className="mb-1 ml-2 inline h-4 w-4 text-slate-500" />
</p>
<p className="text-2xl font-bold text-slate-800">
{responses.length === 0 ? (
<span>-</span>
@@ -133,9 +139,9 @@ export default function SummaryMetadata({ surveyId, environmentId }) {
</Button>
)}
{environment.widgetSetupCompleted && (
{environment.widgetSetupCompleted || survey.type === "link" ? (
<SurveyStatusDropdown surveyId={surveyId} environmentId={environmentId} />
)}
) : null}
<Button
className="h-full w-full px-3 lg:px-6"
href={`/environments/${environmentId}/surveys/${surveyId}/edit`}>

View File

@@ -1,14 +1,17 @@
"use client";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { useEnvironment } from "@/lib/environments/environments";
import { useProduct } from "@/lib/products/products";
import { useProfile } from "@/lib/profile";
import { createSurvey } from "@/lib/surveys/surveys";
import { replacePresetPlaceholders } from "@/lib/templates";
import { cn } from "@formbricks/lib/cn";
import type { Template } from "@formbricks/types/templates";
import { ErrorComponent } from "@formbricks/ui";
import { Button, ErrorComponent } from "@formbricks/ui";
import { PlusCircleIcon } from "@heroicons/react/24/outline";
import { SparklesIcon } from "@heroicons/react/24/solid";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { customSurvey, templates } from "./templates";
@@ -21,19 +24,22 @@ const ALL_CATEGORY_NAME = "All";
const RECOMMENDED_CATEGORY_NAME = "For you";
export default function TemplateList({ environmentId, onTemplateClick }: TemplateList) {
const [activeTemplate, setActiveTemplate] = useState<Template | null>(null);
const router = useRouter();
const [activeTemplate, setActiveTemplate] = useState<Template | null>(null);
const [loading, setLoading] = useState(false);
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
const { profile, isLoadingProfile, isErrorProfile } = useProfile();
const { environment } = useEnvironment(environmentId);
const [selectedFilter, setSelectedFilter] = useState(RECOMMENDED_CATEGORY_NAME);
const [categories, setCategories] = useState<Array<string>>([]);
useEffect(() => {
/* useEffect(() => {
if (product && templates?.length) {
setActiveTemplate(null);
setActiveTemplate(customSurvey);
}
}, [product]);
}, [product]); */
useEffect(() => {
const defaultCategories = [
@@ -53,11 +59,21 @@ export default function TemplateList({ environmentId, onTemplateClick }: Templat
setSelectedFilter(activeFilter);
}, [profile]);
const addSurvey = async (activeTemplate) => {
setLoading(true);
const augmentedTemplate = {
...activeTemplate.preset,
type: environment?.widgetSetupCompleted ? "web" : "link",
};
const survey = await createSurvey(environmentId, augmentedTemplate);
router.push(`/environments/${environmentId}/surveys/${survey.id}/edit`);
};
if (isLoadingProduct || isLoadingProfile) return <LoadingSpinner />;
if (isErrorProduct || isErrorProfile) return <ErrorComponent />;
return (
<main className="relative z-0 flex-1 overflow-y-auto px-8 py-6 focus:outline-none">
<main className="relative z-0 flex-1 overflow-y-auto px-6 pb-6 pt-3 focus:outline-none">
<div className="mb-6 flex flex-wrap space-x-2">
{categories.map((category) => (
<button
@@ -66,9 +82,9 @@ export default function TemplateList({ environmentId, onTemplateClick }: Templat
onClick={() => setSelectedFilter(category)}
className={cn(
selectedFilter === category
? "text-brand-dark border-brand-dark font-semibold"
: "border-slate-300 text-slate-700 hover:bg-slate-100",
"mt-2 rounded border bg-slate-50 px-3 py-1 text-sm transition-all duration-150 "
? " bg-slate-800 font-semibold text-white"
: " bg-white text-slate-700 hover:bg-slate-100",
"mt-2 rounded border border-slate-800 px-2 py-1 text-sm transition-all duration-150 "
)}>
{category}
{category === RECOMMENDED_CATEGORY_NAME && <SparklesIcon className="ml-1 inline h-5 w-5" />}
@@ -92,6 +108,18 @@ export default function TemplateList({ environmentId, onTemplateClick }: Templat
<PlusCircleIcon className="text-brand-dark h-8 w-8 transition-all duration-150 group-hover:scale-110" />
<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>
{activeTemplate?.name === customSurvey.name && (
<div className="text-left">
<Button
variant="darkCTA"
className="mt-6 px-6 py-3"
disabled={activeTemplate === null}
loading={loading}
onClick={() => addSurvey(activeTemplate)}>
Create survey
</Button>
</div>
)}
</button>
{templates
.filter(
@@ -102,8 +130,7 @@ export default function TemplateList({ environmentId, onTemplateClick }: Templat
template.objectives?.includes(profile.objective))
)
.map((template: Template) => (
<button
type="button"
<div
onClick={() => {
const newTemplate = replacePresetPlaceholders(template, product);
onTemplateClick(newTemplate);
@@ -112,7 +139,7 @@ export default function TemplateList({ environmentId, onTemplateClick }: Templat
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"
"duration-120 group relative cursor-pointer rounded-lg bg-white p-6 shadow transition-all duration-150 hover:scale-105"
)}>
<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}
@@ -120,7 +147,17 @@ export default function TemplateList({ environmentId, onTemplateClick }: Templat
<template.icon className="h-8 w-8" />
<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>
{activeTemplate?.name === template.name && (
<Button
variant="darkCTA"
className="mt-6 px-6 py-3"
disabled={activeTemplate === null}
loading={loading}
onClick={() => addSurvey(activeTemplate)}>
Use this template
</Button>
)}
</div>
))}
</div>
</main>

View File

@@ -1,21 +1,23 @@
"use client";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { useEnvironment } from "@/lib/environments/environments";
import { useProduct } from "@/lib/products/products";
import { replacePresetPlaceholders } from "@/lib/templates";
import type { Template } from "@formbricks/types/templates";
import { ErrorComponent } from "@formbricks/ui";
import { PaintBrushIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
/* import { PaintBrushIcon } from "@heroicons/react/24/solid";
import Link from "next/link"; */
import { useEffect, useState } from "react";
import PreviewSurvey from "../PreviewSurvey";
import TemplateList from "./TemplateList";
import TemplateMenuBar from "./TemplateMenuBar";
/* import TemplateMenuBar from "./TemplateMenuBar"; */
import { templates } from "./templates";
export default function SurveyTemplatesPage({ params }) {
const environmentId = params.environmentId;
const [activeTemplate, setActiveTemplate] = useState<Template | null>(null);
const { environment, isLoadingEnvironment, isErrorEnvironment } = useEnvironment(environmentId);
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
@@ -29,33 +31,37 @@ export default function SurveyTemplatesPage({ params }) {
}
}, [product]);
if (isLoadingProduct) return <LoadingSpinner />;
if (isErrorProduct) return <ErrorComponent />;
if (isLoadingProduct || isLoadingEnvironment) return <LoadingSpinner />;
if (isErrorProduct || isErrorEnvironment) return <ErrorComponent />;
return (
<div className="flex h-full flex-col">
<TemplateMenuBar activeTemplate={activeTemplate} environmentId={environmentId} />
<div className="flex h-full flex-col ">
{/* <TemplateMenuBar activeTemplate={activeTemplate} environmentId={environmentId} /> */}
<div className="relative z-0 flex flex-1 overflow-hidden">
<TemplateList
environmentId={environmentId}
onTemplateClick={(template) => {
setActiveQuestionId(template.preset.questions[0].id);
setActiveTemplate(template);
}}
/>
<aside className="group 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">
<Link
href={`/environments/${environmentId}/settings/lookandfeel`}
className="absolute left-6 top-6 z-50 flex items-center rounded bg-slate-50 px-2 py-0.5 text-xs text-slate-500 opacity-0 transition-all duration-500 hover:scale-105 hover:bg-slate-100 group-hover:opacity-100">
Update brand color <PaintBrushIcon className="ml-1.5 h-3 w-3" />
</Link>
<div className="flex-1 flex-col overflow-auto bg-slate-50">
<h1 className="ml-6 mt-6 text-2xl font-bold text-slate-800">Create a new survey</h1>
<TemplateList
environmentId={environmentId}
onTemplateClick={(template) => {
setActiveQuestionId(template.preset.questions[0].id);
setActiveTemplate(template);
}}
/>
</div>
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-100 bg-slate-50 md:flex md:flex-col">
{activeTemplate && (
<PreviewSurvey
activeQuestionId={activeQuestionId}
questions={activeTemplate.preset.questions}
brandColor={product.brandColor}
setActiveQuestionId={setActiveQuestionId}
/>
<div className="my-6 flex h-full w-full flex-col items-center justify-center">
<p className="pb-2 text-center text-sm font-normal text-slate-400">Preview</p>
<PreviewSurvey
activeQuestionId={activeQuestionId}
questions={activeTemplate.preset.questions}
brandColor={product.brandColor}
setActiveQuestionId={setActiveQuestionId}
environmentId={environmentId}
surveyType={environment?.widgetSetupCompleted ? "web" : "link"}
thankYouCard={{ enabled: true }}
/>
</div>
)}
</aside>
</div>

View File

@@ -246,7 +246,7 @@ export const templates: Template[] = [
},
{
id: createId(),
label: "in a Podcast",
label: "In a Podcast",
},
],
},
@@ -358,7 +358,7 @@ export const templates: Template[] = [
},
{
id: createId(),
label: "in a Podcast",
label: "In a Podcast",
},
],
},
@@ -1055,8 +1055,8 @@ export const templates: Template[] = [
];
export const customSurvey: Template = {
name: "Custom Survey",
description: "Create your survey from scratch.",
name: "Start from scratch",
description: "Create a survey without template.",
icon: null,
preset: {
name: "New Survey",
@@ -1064,8 +1064,8 @@ export const customSurvey: Template = {
{
id: createId(),
type: "openText",
headline: "What's poppin?",
subheader: "This can help us improve your experience.",
headline: "Custom Survey",
subheader: "This is an example survey.",
placeholder: "Type your answer here...",
required: true,
},

View File

@@ -21,7 +21,7 @@ const Product: React.FC<Product> = ({ done, isLoading, environmentId }) => {
const { triggerProductMutate } = useProductMutation(environmentId);
const [name, setName] = useState("");
const [color, setColor] = useState("#334155");
const [color, setColor] = useState("##4748b");
const handleNameChange = (event) => {
setName(event.target.value);

View File

@@ -1,16 +1,7 @@
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,
reset,
}: {
children: ReactNode;
isOpen: boolean;
reset: () => void;
}) {
export default function Modal({ children, isOpen }: { children: ReactNode; isOpen: boolean }) {
const [show, setShow] = useState(false);
useEffect(() => {
@@ -18,24 +9,13 @@ export default function Modal({
}, [isOpen]);
return (
<div aria-live="assertive" className="absolute inset-0 flex cursor-pointer items-end">
<div className="flex w-full flex-col items-center p-4 sm:items-end">
<div
className={cn(
show ? "opacity-100" : "opacity-0",
"mr-6 flex items-center rounded-t bg-amber-500 px-3 text-sm font-semibold text-white transition-all duration-500 ease-in-out 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",
"pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg border-2 border-amber-400 bg-white px-4 py-6 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out sm:p-6"
)}>
{children}
</div>
<div aria-live="assertive" className="flex w-full grow items-end justify-end p-4">
<div
className={cn(
show ? "translate-x-0 opacity-100" : "translate-x-32 opacity-0",
"pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white px-4 py-6 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out sm:p-6"
)}>
{children}
</div>
</div>
);

View File

@@ -12,15 +12,8 @@ import {
SelectTrigger,
SelectValue,
} from "@formbricks/ui";
import {
CheckCircleIcon,
PauseCircleIcon,
PlayCircleIcon,
PencilSquareIcon,
ArchiveBoxIcon,
} from "@heroicons/react/24/solid";
import { CheckCircleIcon, PauseCircleIcon, PlayCircleIcon } from "@heroicons/react/24/solid";
import toast from "react-hot-toast";
import { Badge } from "@formbricks/ui";
export default function SurveyStatusDropdown({
surveyId,
@@ -47,24 +40,8 @@ export default function SurveyStatusDropdown({
{survey.status === "draft" || survey.status === "archived" ? (
<div className="flex items-center">
<SurveyStatusIndicator status={survey.status} environmentId={environmentId} />
{survey.status === "draft" && (
<Badge
text="Draft"
type="gray"
size="normal"
StartIcon={PencilSquareIcon}
startIconClassName="mr-2"
/>
)}
{survey.status === "archived" && (
<Badge
text="Archived"
type="gray"
size="normal"
StartIcon={ArchiveBoxIcon}
startIconClassName="mr-2"
/>
)}
{survey.status === "draft" && <p className="text-sm italic text-slate-600">Draft</p>}
{survey.status === "archived" && <p className="text-sm italic text-slate-600">Archived</p>}
</div>
) : (
<Select

View File

@@ -16,9 +16,12 @@ export const useResponses = (environmentId: string, surveyId: string) => {
};
export const deleteSubmission = async (environmentId: string, surveyId: string, responseId: string) => {
const response = await fetch(`/api/v1/environments/${environmentId}/surveys/${surveyId}/responses/${responseId}`, {
method: "DELETE",
});
const response = await fetch(
`/api/v1/environments/${environmentId}/surveys/${surveyId}/responses/${responseId}`,
{
method: "DELETE",
}
);
return response.json();
};

View File

@@ -115,7 +115,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
const firstEnvironment = newProduct.environments[0];
res.json(firstEnvironment);
}
// DELETE
else if (req.method === "DELETE") {
const membership = await prisma.membership.findUnique({
@@ -140,11 +140,11 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
return res.status(404).json({ message: "This environment doesn't exist" });
}
// Delete the product with
// Delete the product with
const prismaRes = await prisma.product.delete({
where: { id: environment.productId },
});
return res.json(prismaRes);
}

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Product" ALTER COLUMN "brandColor" SET DEFAULT '#64748b';

View File

@@ -245,7 +245,7 @@ model Product {
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId String
environments Environment[]
brandColor String @default("#334155")
brandColor String @default("#64748b")
recontactDays Int @default(7)
}

View File

@@ -98,7 +98,7 @@ export const Button: React.ForwardRefExoticComponent<
variant === "darkCTA" &&
(disabled
? "text-slate-400 dark:text-slate-500 bg-slate-200 dark:bg-slate-800"
: "text-slate-100 hover:text-slate-50 bg-slate-800 hover:bg-slate-700 dark:bg-slate-200 dark:text-slate-700 dark:hover:bg-slate-300 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-slate-700 focus:ring-neutral-500"),
: "text-slate-100 hover:text-slate-50 bg-gradient-to-br from-slate-900 to-slate-800 hover:from-slate-800 hover:to-slate-700 dark:bg-slate-200 dark:text-slate-700 dark:hover:bg-slate-300 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-slate-700 focus:ring-neutral-500"),
// set not-allowed cursor if disabled
loading ? "cursor-wait" : disabled ? "cursor-not-allowed" : "",