mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-16 11:41:41 -05:00
added email templates
This commit is contained in:
@@ -1,6 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Dialog, DialogContent } from "@formbricks/ui";
|
||||
import toast from "react-hot-toast";
|
||||
import CodeBlock from "@/components/shared/CodeBlock";
|
||||
import {
|
||||
Section,
|
||||
Tailwind,
|
||||
render,
|
||||
Button as EmailButton,
|
||||
Text,
|
||||
Link,
|
||||
Container,
|
||||
Row,
|
||||
Column,
|
||||
} from "@react-email/components";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { Button, Dialog, DialogContent, Input } from "@formbricks/ui";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import {
|
||||
LinkIcon,
|
||||
@@ -9,16 +23,14 @@ import {
|
||||
DocumentDuplicateIcon,
|
||||
ArrowUpRightIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import CodeBlock from "@/components/shared/CodeBlock";
|
||||
import { SURVEY_BASE_URL } from "@formbricks/lib/constants";
|
||||
import toast from "react-hot-toast";
|
||||
import { QuestionType } from "@formbricks/types/questions";
|
||||
|
||||
interface EmbedSurveyModalProps {
|
||||
survey: TSurvey;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
surveyBaseUrl: string;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
@@ -27,15 +39,14 @@ const tabs = [
|
||||
{ id: "webpage", label: "Embed in a Web Page", icon: CodeBracketIcon },
|
||||
];
|
||||
|
||||
export default function EmbedSurveyModal({ survey, open, setOpen }: EmbedSurveyModalProps) {
|
||||
export default function EmbedSurveyModal({ survey, open, setOpen, surveyBaseUrl }: EmbedSurveyModalProps) {
|
||||
const [activeId, setActiveId] = useState(tabs[0].id);
|
||||
const [showEmbed, setShowEmbed] = useState(false);
|
||||
|
||||
const surveyUrl = useMemo(() => SURVEY_BASE_URL + survey.id, [survey]);
|
||||
const surveyUrl = useMemo(() => surveyBaseUrl + survey.id, [survey]);
|
||||
|
||||
const componentMap = {
|
||||
link: <LinkTab surveyUrl={surveyUrl} />,
|
||||
email: <EmailTab />,
|
||||
email: <EmailTab survey={survey} surveyUrl={surveyUrl} />,
|
||||
webpage: <WebpageTab surveyUrl={surveyUrl} />,
|
||||
};
|
||||
|
||||
@@ -54,7 +65,8 @@ export default function EmbedSurveyModal({ survey, open, setOpen }: EmbedSurveyM
|
||||
key={tab.id}
|
||||
onClick={() => setActiveId(tab.id)}
|
||||
className={cn(
|
||||
"rounded-[4px] px-4 py-[6px] text-slate-600 focus:ring-0 focus:ring-offset-0",
|
||||
"rounded-[4px] px-4 py-[6px] text-slate-600",
|
||||
// "focus:ring-0 focus:ring-offset-0", // enable these classes to remove the focus rings on buttons
|
||||
tab.id === activeId
|
||||
? " border border-gray-200 bg-slate-100 font-semibold text-slate-900"
|
||||
: "border-transparent text-slate-500 hover:text-slate-700"
|
||||
@@ -87,6 +99,7 @@ const LinkTab = ({ surveyUrl }) => {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col gap-5">
|
||||
<div className="flex justify-between gap-2">
|
||||
@@ -108,7 +121,7 @@ const LinkTab = ({ surveyUrl }) => {
|
||||
Copy URL
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative grow rounded-xl border border-gray-200 bg-white px-8 py-10">
|
||||
<div className="relative grow rounded-xl border border-gray-200 bg-white px-4 py-[18px]">
|
||||
<Button
|
||||
variant="minimal"
|
||||
className={cn(
|
||||
@@ -117,7 +130,9 @@ const LinkTab = ({ surveyUrl }) => {
|
||||
EndIcon={ArrowUpRightIcon}
|
||||
title="Open survey in new tab"
|
||||
aria-label="Open survey in new tab"
|
||||
endIconClassName="h-4 w-4 ">
|
||||
endIconClassName="h-4 w-4 "
|
||||
href={`${surveyUrl}?preview=true`}
|
||||
target="_blank">
|
||||
Open in new tab
|
||||
</Button>
|
||||
</div>
|
||||
@@ -125,8 +140,102 @@ const LinkTab = ({ surveyUrl }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const EmailTab = () => {
|
||||
return <>Email</>;
|
||||
const EmailTab = ({ survey, surveyUrl }: { survey: TSurvey; surveyUrl: string }) => {
|
||||
const [email, setEmail] = useState("");
|
||||
const [showEmbed, setShowEmbed] = useState(false);
|
||||
|
||||
console.log(survey);
|
||||
const Email = (
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"brand-dark": "#00C4B8",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}>
|
||||
{getEmailTemplate(survey, surveyUrl)}
|
||||
</Tailwind>
|
||||
);
|
||||
|
||||
const confirmEmail = render(Email, { pretty: true });
|
||||
|
||||
return (
|
||||
<div className="flex grow flex-col gap-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="user@mail.com"
|
||||
className="h-11 grow bg-white"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
{showEmbed ? (
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
title="Embed survey in your website"
|
||||
aria-label="Embed survey in your website"
|
||||
onClick={() => {
|
||||
toast.success("Embed code copied to clipboard!");
|
||||
}}
|
||||
className="shrink-0"
|
||||
EndIcon={DocumentDuplicateIcon}>
|
||||
Copy code
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
title="view embed code for email"
|
||||
aria-label="view embed code for email"
|
||||
onClick={() => {}}
|
||||
EndIcon={EnvelopeIcon}
|
||||
className="shrink-0">
|
||||
Send Preview
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
title="view embed code for email"
|
||||
aria-label="view embed code for email"
|
||||
onClick={() => setShowEmbed(!showEmbed)}
|
||||
EndIcon={CodeBracketIcon}
|
||||
className="shrink-0">
|
||||
{showEmbed ? "Hide Embed Code" : "View Embed Code"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grow overflow-y-scroll rounded-xl border border-gray-200 bg-white px-4 py-[18px]">
|
||||
{showEmbed ? (
|
||||
<>
|
||||
<CodeBlock
|
||||
customCodeClass="!whitespace-normal sm:!whitespace-pre-wrap !break-all sm:!break-normal"
|
||||
language="html"
|
||||
showCopyToClipboard={false}>
|
||||
{confirmEmail}
|
||||
</CodeBlock>
|
||||
</>
|
||||
) : (
|
||||
<div className="">
|
||||
<div className="mb-6 flex gap-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>
|
||||
<div className="">
|
||||
<div className="mb-2 border-b border-slate-200 pb-2 text-sm">
|
||||
To : {email || "user@mail.com"}
|
||||
</div>
|
||||
<div className="border-b border-slate-200 pb-2 text-sm">
|
||||
Subject : Formbricks Email Survey Preview
|
||||
</div>
|
||||
<div className="p-4">{Email}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WebpageTab = ({ surveyUrl }) => {
|
||||
@@ -153,15 +262,152 @@ const WebpageTab = ({ surveyUrl }) => {
|
||||
Copy code
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grow rounded-xl border border-gray-200 bg-white px-8 py-10">
|
||||
{/* {iframeCode} */}
|
||||
|
||||
{/* <CodeBlock
|
||||
customCodeClass="!whitespace-normal sm:!whitespace-pre-wrap !break-all sm:!break-normal bg-transparent"
|
||||
language="html">
|
||||
<div className="grow overflow-y-scroll rounded-xl border border-gray-200 bg-white px-4 py-[18px]">
|
||||
<CodeBlock
|
||||
customCodeClass="!whitespace-normal sm:!whitespace-pre-wrap !break-all sm:!break-normal"
|
||||
language="html"
|
||||
showCopyToClipboard={false}>
|
||||
{iframeCode}
|
||||
</CodeBlock> */}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getEmailTemplate = (survey: TSurvey, surveyUrl: string) => {
|
||||
const firstQuestion = survey.questions[0];
|
||||
console.log(firstQuestion);
|
||||
switch (firstQuestion.type) {
|
||||
case QuestionType.OpenText:
|
||||
return (
|
||||
<Link
|
||||
href={surveyUrl}
|
||||
target="_blank"
|
||||
className="mx-0 my-2 block rounded-lg border border-black px-4 py-2 text-inherit">
|
||||
<Text className="m-0 mb-1.5 mr-8 block p-0 text-base font-semibold leading-6 text-slate-900">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-600">
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Section className="mt-4 block h-20 w-full rounded-lg border border-gray-200 bg-slate-50" />
|
||||
<Container className="m-auto mt-4 text-center">
|
||||
<Text className="m-0 inline-block p-0 text-xs text-slate-400">powered by</Text>
|
||||
<Text className="m-0 ml-1 inline-block p-0 text-slate-700">Formbricks</Text>
|
||||
</Container>
|
||||
</Link>
|
||||
);
|
||||
case QuestionType.Consent:
|
||||
return (
|
||||
<Link href={surveyUrl} target="_blank">
|
||||
<Section className="block rounded-lg border border-black px-4 py-2 text-inherit">
|
||||
<Text className="m-0 mb-1.5 block text-base font-semibold leading-6 text-slate-900">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Container className="m-0 text-sm font-normal leading-6 text-slate-600">
|
||||
<Text className="m-0 p-0" dangerouslySetInnerHTML={{ __html: firstQuestion.html || "" }}></Text>
|
||||
</Container>
|
||||
|
||||
<Container className="mt-4 block w-full rounded-lg border border-gray-200 bg-slate-50 p-4 font-medium text-slate-800">
|
||||
<Text className="m-0 inline-block">{firstQuestion.label}</Text>
|
||||
</Container>
|
||||
<Container className="mt-4 flex justify-end">
|
||||
{!firstQuestion.required && (
|
||||
<EmailButton
|
||||
href={`${surveyUrl}?${firstQuestion.id}=dismissed`}
|
||||
className="inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-black">
|
||||
Reject
|
||||
</EmailButton>
|
||||
)}
|
||||
<EmailButton
|
||||
href={`${surveyUrl}?${firstQuestion.id}=accepted`}
|
||||
className="bg-brand-dark ml-2 inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-white">
|
||||
Accept
|
||||
</EmailButton>
|
||||
</Container>
|
||||
<Container className="m-auto mt-4 text-center">
|
||||
<Text className="m-0 inline-block p-0 text-xs text-slate-400">powered by</Text>
|
||||
<Text className="m-0 ml-1 inline-block p-0 text-slate-700">Formbricks</Text>
|
||||
</Container>
|
||||
</Section>
|
||||
</Link>
|
||||
);
|
||||
case QuestionType.NPS:
|
||||
return (
|
||||
<Link href={surveyUrl} target="_blank">
|
||||
<Section className="block rounded-lg border border-black px-4 py-2 text-inherit">
|
||||
<Text className="m-0 mb-1.5 block text-base font-semibold leading-6 text-slate-900">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-600">
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Container className="mx-0 mt-4 flex w-max flex-col ">
|
||||
<Section className="block overflow-hidden rounded-md border">
|
||||
{Array.from({ length: 11 }, (_, i) => (
|
||||
<EmailButton
|
||||
href={`${surveyUrl}?${firstQuestion.id}=${i}`}
|
||||
className="m-0 inline-flex h-10 w-10 items-center justify-center border p-0 text-slate-800">
|
||||
{i}
|
||||
</EmailButton>
|
||||
))}
|
||||
</Section>
|
||||
<Section className="m-0 px-1.5 text-xs leading-6 text-slate-500">
|
||||
<Row>
|
||||
<Column>
|
||||
<Text className="m-0 inline-block w-max p-0">{firstQuestion.lowerLabel}</Text>
|
||||
</Column>
|
||||
<Column className="text-right">
|
||||
<Text className="m-0 inline-block w-max p-0 text-right">{firstQuestion.upperLabel}</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
</Container>
|
||||
|
||||
<Container className="m-auto mt-4 text-center">
|
||||
<Text className="m-0 inline-block p-0 text-xs text-slate-400">powered by</Text>
|
||||
<Text className="m-0 ml-1 inline-block p-0 text-slate-700">Formbricks</Text>
|
||||
</Container>
|
||||
</Section>
|
||||
</Link>
|
||||
);
|
||||
case QuestionType.CTA:
|
||||
return (
|
||||
<Link href={surveyUrl} target="_blank">
|
||||
<Section className="block rounded-lg border border-black px-4 py-2 text-inherit">
|
||||
<Text className="m-0 mb-1.5 block text-base font-semibold leading-6 text-slate-900">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Container className="m-0 text-sm font-normal leading-6 text-slate-600">
|
||||
<Text className="m-0 p-0" dangerouslySetInnerHTML={{ __html: firstQuestion.html || "" }}></Text>
|
||||
</Container>
|
||||
|
||||
<Container className="mt-4 ">
|
||||
{!firstQuestion.required && (
|
||||
<EmailButton
|
||||
href={`${surveyUrl}?${firstQuestion.id}=dismissed`}
|
||||
className="inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-black">
|
||||
{firstQuestion.dismissButtonLabel}
|
||||
</EmailButton>
|
||||
)}
|
||||
<EmailButton
|
||||
onClick={() => {
|
||||
if (firstQuestion.buttonExternal && firstQuestion.buttonUrl) {
|
||||
window?.open(firstQuestion.buttonUrl, "_blank")?.focus();
|
||||
}
|
||||
}}
|
||||
href={`${surveyUrl}?${firstQuestion.id}=clicked`}
|
||||
className="ml-2 inline-flex cursor-pointer appearance-none rounded-md bg-slate-500 px-6 py-3 text-sm font-medium text-white">
|
||||
{firstQuestion.buttonLabel}
|
||||
</EmailButton>
|
||||
</Container>
|
||||
<Container className="m-auto mt-4 text-center">
|
||||
<Text className="m-0 inline-block p-0 text-xs text-slate-400">powered by</Text>
|
||||
<Text className="m-0 ml-1 inline-block p-0 text-slate-700">Formbricks</Text>
|
||||
</Container>
|
||||
</Section>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
|
||||
@@ -32,16 +32,22 @@ export default function LinkSurveyShareButton({
|
||||
onClick={() => setShowLinkModal(true)}>
|
||||
<ShareIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
{/* {showLinkModal && <LinkSurveyModal survey={survey} open={showLinkModal} setOpen={setShowLinkModal} />} */}
|
||||
{/* {showLinkModal && <EmbedSurveyModal survey={survey} open={showLinkModal} setOpen={setShowLinkModal} />} */}
|
||||
{showLinkModal && (
|
||||
<LinkSurveyModal
|
||||
<EmbedSurveyModal
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
/>
|
||||
)}
|
||||
{/* {showLinkModal && (
|
||||
<LinkSurveyModal
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
/>
|
||||
)} */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ export function getPrefillResponseData(
|
||||
|
||||
const answer = transformAnswer(question, firstQuestionPrefill || "");
|
||||
const answerObj = { [firstQuestionId]: answer };
|
||||
|
||||
if (question.type === QuestionType.CTA && question.buttonExternal && question.buttonUrl) {
|
||||
window?.open(question.buttonUrl, "blank");
|
||||
}
|
||||
|
||||
return answerObj;
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -11,23 +11,31 @@ interface CodeBlockProps {
|
||||
children: React.ReactNode;
|
||||
language: string;
|
||||
customCodeClass?: string;
|
||||
showCopyToClipboard?: boolean;
|
||||
}
|
||||
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({ children, language, customCodeClass = "" }) => {
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({
|
||||
children,
|
||||
language,
|
||||
customCodeClass = "",
|
||||
showCopyToClipboard = true,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
Prism.highlightAll();
|
||||
}, [children]);
|
||||
|
||||
return (
|
||||
<div className="group relative mt-4 rounded-md text-sm text-slate-200">
|
||||
<DocumentDuplicateIcon
|
||||
className="absolute right-4 top-4 z-20 h-5 w-5 cursor-pointer text-slate-600 opacity-0 transition-all duration-150 group-hover:opacity-60"
|
||||
onClick={() => {
|
||||
const childText = children?.toString() || "";
|
||||
navigator.clipboard.writeText(childText);
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
/>
|
||||
{showCopyToClipboard && (
|
||||
<DocumentDuplicateIcon
|
||||
className="absolute right-4 top-4 z-20 h-5 w-5 cursor-pointer text-slate-600 opacity-0 transition-all duration-150 group-hover:opacity-60"
|
||||
onClick={() => {
|
||||
const childText = children?.toString() || "";
|
||||
navigator.clipboard.writeText(childText);
|
||||
toast.success("Copied to clipboard");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<pre>
|
||||
<code className={cn(`language-${language} whitespace-pre-wrap`, customCodeClass)}>{children}</code>
|
||||
</pre>
|
||||
|
||||
Reference in New Issue
Block a user