mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-26 10:42:16 -06:00
Revamp survey settings (#287)
* improve survey settings flow --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
@@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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="">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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`}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Product" ALTER COLUMN "brandColor" SET DEFAULT '#64748b';
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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" : "",
|
||||
|
||||
Reference in New Issue
Block a user