mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-01 19:32:57 -05:00
feat: Better Embedding of Link Surveys
feat: Better Embedding of Link Surveys
This commit is contained in:
+4
@@ -13,6 +13,7 @@ import { useEffect, useMemo } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
interface ResponsePageProps {
|
||||
environment: TEnvironment;
|
||||
@@ -21,6 +22,7 @@ interface ResponsePageProps {
|
||||
responses: TResponse[];
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
environmentTags: TTag[];
|
||||
}
|
||||
|
||||
@@ -31,6 +33,7 @@ const ResponsePage = ({
|
||||
responses,
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
environmentTags,
|
||||
}: ResponsePageProps) => {
|
||||
const { selectedFilter, dateRange, resetState } = useResponseFilter();
|
||||
@@ -55,6 +58,7 @@ const ResponsePage = ({
|
||||
surveyId={surveyId}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
profile={profile}
|
||||
/>
|
||||
<CustomFilter
|
||||
environmentTags={environmentTags}
|
||||
|
||||
+7
@@ -9,6 +9,7 @@ import ResponsesLimitReachedBanner from "@/app/(app)/environments/[environmentId
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getProfile } from "@formbricks/lib/services/profile";
|
||||
|
||||
export default async function Page({ params }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
@@ -26,6 +27,11 @@ export default async function Page({ params }) {
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const profile = await getProfile(session.user.id);
|
||||
if (!profile) {
|
||||
throw new Error("Profile not found");
|
||||
}
|
||||
const tags = await getTagsByEnvironmentId(params.environmentId);
|
||||
|
||||
return (
|
||||
@@ -39,6 +45,7 @@ export default async function Page({ params }) {
|
||||
surveyBaseUrl={SURVEY_BASE_URL}
|
||||
product={product}
|
||||
environmentTags={tags}
|
||||
profile={profile}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
|
||||
import { sendEmbedSurveyPreviewEmail } from "@formbricks/lib/emails/emails";
|
||||
import { AuthenticationError } from "@formbricks/types/v1/errors";
|
||||
|
||||
type TSendEmailActionArgs = {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
};
|
||||
|
||||
export const sendEmailAction = async ({ html, subject, to }: TSendEmailActionArgs) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
if (!session) {
|
||||
throw new AuthenticationError("Not authenticated");
|
||||
}
|
||||
return await sendEmbedSurveyPreviewEmail(to, subject, html);
|
||||
};
|
||||
+10
-2
@@ -1,17 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import LinkSurveyModal from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkSurveyModal";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { ShareIcon } from "@heroicons/react/24/outline";
|
||||
import { useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import ShareEmbedSurvey from "./ShareEmbedSurvey";
|
||||
import LinkSingleUseSurveyModal from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkSingleUseSurveyModal";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
interface LinkSurveyShareButtonProps {
|
||||
survey: TSurvey;
|
||||
className?: string;
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
singleUseIds?: string[];
|
||||
}
|
||||
|
||||
@@ -19,6 +23,8 @@ export default function LinkSurveyShareButton({
|
||||
survey,
|
||||
className,
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
singleUseIds,
|
||||
}: LinkSurveyShareButtonProps) {
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
@@ -43,11 +49,13 @@ export default function LinkSurveyShareButton({
|
||||
singleUseIds={singleUseIds}
|
||||
/>
|
||||
) : (
|
||||
<LinkSurveyModal
|
||||
<ShareEmbedSurvey
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
product={product}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
profile={profile}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import CodeBlock from "@/components/shared/CodeBlock";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { Button, Dialog, DialogContent } from "@formbricks/ui";
|
||||
import { CheckIcon } from "@heroicons/react/24/outline";
|
||||
import { CodeBracketIcon, DocumentDuplicateIcon, EyeIcon } from "@heroicons/react/24/solid";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface LinkSurveyModalProps {
|
||||
survey: TSurvey;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
surveyBaseUrl: string;
|
||||
}
|
||||
|
||||
export default function LinkSurveyModal({ survey, open, setOpen, surveyBaseUrl }: LinkSurveyModalProps) {
|
||||
const linkTextRef = useRef(null);
|
||||
const [showEmbed, setShowEmbed] = useState(false);
|
||||
|
||||
const surveyUrl = useMemo(() => surveyBaseUrl + survey.id, [survey.id, surveyBaseUrl]);
|
||||
|
||||
const iframeCode = `<div style="position: relative; height:100vh; max-height:100vh;
|
||||
overflow:auto;">
|
||||
<iframe
|
||||
src="${surveyUrl}"
|
||||
frameborder="0" style="position: absolute; left:0;
|
||||
top:0; width:100%; height:100%; border:0;">
|
||||
</iframe></div>`;
|
||||
const handleTextSelection = () => {
|
||||
if (linkTextRef.current) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(linkTextRef.current);
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="bottom-0 max-w-sm bg-white p-4 sm:bottom-auto sm:max-w-xl sm:p-6">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-teal-100">
|
||||
<CheckIcon className="h-6 w-6 text-teal-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-semibold leading-6 text-gray-900">Your survey is ready!</h3>
|
||||
{showEmbed ? (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-500">Embed survey on your website:</p>
|
||||
<CodeBlock
|
||||
customCodeClass="!whitespace-normal sm:!whitespace-pre-wrap !break-all sm:!break-normal"
|
||||
language="html">
|
||||
{iframeCode}
|
||||
</CodeBlock>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-500">Share this link to let people answer your survey:</p>
|
||||
<div
|
||||
ref={linkTextRef}
|
||||
className="relative mt-3 max-w-full overflow-auto rounded-lg border border-slate-300 bg-slate-50 p-3 text-center text-slate-800"
|
||||
onClick={() => handleTextSelection()}>
|
||||
<span
|
||||
style={{
|
||||
wordBreak: "break-all",
|
||||
}}>
|
||||
{surveyUrl}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 flex flex-col justify-center gap-2 sm:flex-row sm:justify-end">
|
||||
<Button
|
||||
variant="secondary"
|
||||
title="Embed survey in your website"
|
||||
aria-label="Embed survey in your website"
|
||||
className="flex justify-center"
|
||||
onClick={() => {
|
||||
setShowEmbed(true);
|
||||
navigator.clipboard.writeText(iframeCode);
|
||||
toast.success("iframe code copied to clipboard!");
|
||||
}}
|
||||
EndIcon={CodeBracketIcon}>
|
||||
Embed
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowEmbed(false);
|
||||
navigator.clipboard.writeText(surveyUrl);
|
||||
toast.success("URL copied to clipboard!");
|
||||
}}
|
||||
title="Copy survey link to clipboard"
|
||||
aria-label="Copy survey link to clipboard"
|
||||
className="flex justify-center"
|
||||
EndIcon={DocumentDuplicateIcon}>
|
||||
Copy URL
|
||||
</Button>
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
title="Preview survey"
|
||||
aria-label="Preview survey"
|
||||
className="flex justify-center"
|
||||
href={`${surveyUrl}?preview=true`}
|
||||
target="_blank"
|
||||
EndIcon={EyeIcon}>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
"use client";
|
||||
|
||||
import LinkTab from "./shareEmbedTabs/LinkTab";
|
||||
import EmailTab from "./shareEmbedTabs/EmailTab";
|
||||
import WebpageTab from "./shareEmbedTabs/WebpageTab";
|
||||
import { useMemo, useState } from "react";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { DialogContent, Button, Dialog } from "@formbricks/ui";
|
||||
import { LinkIcon, EnvelopeIcon, CodeBracketIcon } from "@heroicons/react/24/outline";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
interface ShareEmbedSurveyProps {
|
||||
survey: TSurvey;
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
}
|
||||
|
||||
export default function ShareEmbedSurvey({
|
||||
survey,
|
||||
open,
|
||||
setOpen,
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
}: ShareEmbedSurveyProps) {
|
||||
const surveyUrl = useMemo(() => surveyBaseUrl + survey.id, [survey]);
|
||||
|
||||
const { email } = profile;
|
||||
const { brandColor } = product;
|
||||
|
||||
const [activeId, setActiveId] = useState(tabs[0].id);
|
||||
|
||||
const componentMap = {
|
||||
link: <LinkTab surveyUrl={surveyUrl} survey={survey} brandColor={brandColor} />,
|
||||
email: <EmailTab survey={survey} surveyUrl={surveyUrl} email={email} brandColor={brandColor} />,
|
||||
webpage: <WebpageTab surveyUrl={surveyUrl} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setActiveId(tabs[0].id);
|
||||
setOpen(open);
|
||||
}}>
|
||||
<DialogContent className="bottom-0 flex h-[95%] w-full flex-col gap-0 overflow-hidden rounded-2xl bg-white p-0 sm:max-w-none lg:bottom-auto lg:h-auto lg:w-[960px]">
|
||||
<div className="border-b border-gray-200 px-4 py-3 lg:px-6 lg:py-4 ">Share or embed your survey</div>
|
||||
<div className="flex grow overflow-x-hidden overflow-y-scroll">
|
||||
<div className="hidden basis-[326px] border-r border-gray-200 px-6 py-8 lg:block lg:shrink-0">
|
||||
<div className="flex w-max flex-col gap-3">
|
||||
{tabs.map((tab) => (
|
||||
<Button
|
||||
StartIcon={tab.icon}
|
||||
startIconClassName={cn("h-4 w-4")}
|
||||
variant="minimal"
|
||||
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", // 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"
|
||||
)}
|
||||
aria-current={tab.id === activeId ? "page" : undefined}>
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full grow flex-col gap-6 bg-gray-50 px-4 py-6 lg:p-6">
|
||||
<div className="flex h-full overflow-y-scroll lg:h-[590px] lg:overflow-y-visible">
|
||||
{componentMap[activeId]}
|
||||
</div>
|
||||
<div className="mx-auto flex max-w-max rounded-md bg-slate-100 p-1 lg:hidden">
|
||||
{tabs.slice(0, 2).map((tab) => (
|
||||
<Button
|
||||
variant="minimal"
|
||||
key={tab.id}
|
||||
onClick={() => setActiveId(tab.id)}
|
||||
className={cn(
|
||||
"rounded-sm px-3 py-[6px]",
|
||||
tab.id === activeId
|
||||
? "bg-white text-slate-900"
|
||||
: "border-transparent text-slate-700 hover:text-slate-900"
|
||||
)}>
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: "link", label: "Share the Link", icon: LinkIcon },
|
||||
{ id: "email", label: "Embed in an Email", icon: EnvelopeIcon },
|
||||
{ id: "webpage", label: "Embed in a Web Page", icon: CodeBracketIcon },
|
||||
];
|
||||
+11
-2
@@ -5,14 +5,18 @@ import { Confetti } from "@formbricks/ui";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import LinkSurveyModal from "./LinkSurveyModal";
|
||||
import ShareEmbedSurvey from "./ShareEmbedSurvey";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import LinkSingleUseSurveyModal from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkSingleUseSurveyModal";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
interface SummaryMetadataProps {
|
||||
environment: TEnvironment;
|
||||
survey: TSurvey;
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
singleUseIds?: string[];
|
||||
}
|
||||
|
||||
@@ -20,9 +24,12 @@ export default function SuccessMessage({
|
||||
environment,
|
||||
survey,
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
singleUseIds,
|
||||
}: SummaryMetadataProps) {
|
||||
const isSingleUse = survey.singleUse?.enabled ?? false;
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
const [confetti, setConfetti] = useState(false);
|
||||
@@ -61,11 +68,13 @@ export default function SuccessMessage({
|
||||
singleUseIds={singleUseIds}
|
||||
/>
|
||||
) : (
|
||||
<LinkSurveyModal
|
||||
<ShareEmbedSurvey
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
profile={profile}
|
||||
/>
|
||||
)}
|
||||
{confetti && <Confetti />}
|
||||
|
||||
+4
@@ -15,6 +15,7 @@ import { useEffect, useMemo } from "react";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { TTag } from "@formbricks/types/v1/tags";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
interface SummaryPageProps {
|
||||
environment: TEnvironment;
|
||||
@@ -24,6 +25,7 @@ interface SummaryPageProps {
|
||||
surveyBaseUrl: string;
|
||||
singleUseIds?: string[];
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
environmentTags: TTag[];
|
||||
}
|
||||
|
||||
@@ -35,6 +37,7 @@ const SummaryPage = ({
|
||||
surveyBaseUrl,
|
||||
singleUseIds,
|
||||
product,
|
||||
profile,
|
||||
environmentTags,
|
||||
}: SummaryPageProps) => {
|
||||
const { selectedFilter, dateRange, resetState } = useResponseFilter();
|
||||
@@ -60,6 +63,7 @@ const SummaryPage = ({
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
singleUseIds={singleUseIds}
|
||||
product={product}
|
||||
profile={profile}
|
||||
/>
|
||||
<CustomFilter
|
||||
environmentTags={environmentTags}
|
||||
|
||||
+387
@@ -0,0 +1,387 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { Button, Input } from "@formbricks/ui";
|
||||
import { QuestionType } from "@formbricks/types/questions";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { AuthenticationError } from "@formbricks/types/v1/errors";
|
||||
import { sendEmailAction } from "../../actions";
|
||||
import CodeBlock from "@/components/shared/CodeBlock";
|
||||
import { CodeBracketIcon, DocumentDuplicateIcon, EnvelopeIcon } from "@heroicons/react/24/solid";
|
||||
import {
|
||||
Column,
|
||||
Container,
|
||||
Button as EmailButton,
|
||||
Link,
|
||||
Row,
|
||||
Section,
|
||||
Tailwind,
|
||||
Text,
|
||||
render,
|
||||
} from "@react-email/components";
|
||||
import { useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface EmailTabProps {
|
||||
survey: TSurvey;
|
||||
surveyUrl: string;
|
||||
email: string;
|
||||
brandColor: string;
|
||||
}
|
||||
|
||||
export default function EmailTab({ survey, surveyUrl, email, brandColor }: EmailTabProps) {
|
||||
const [showEmbed, setShowEmbed] = useState(false);
|
||||
const subject = "Formbricks Email Survey Preview";
|
||||
|
||||
const emailValues = useMemo(() => {
|
||||
return getEmailValues({ brandColor, survey, surveyUrl, preview: false });
|
||||
}, []);
|
||||
|
||||
const previewEmailValues = useMemo(() => {
|
||||
return getEmailValues({ brandColor, survey, surveyUrl, preview: true });
|
||||
}, []);
|
||||
|
||||
const sendPreviewEmail = async () => {
|
||||
try {
|
||||
await sendEmailAction({ html: previewEmailValues.html, subject, to: email });
|
||||
toast.success("Email sent!");
|
||||
} catch (err) {
|
||||
if (err instanceof AuthenticationError) {
|
||||
toast.error("You are not authenticated to perform this action.");
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error("Something went wrong. Please try again later.");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full 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} />
|
||||
{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!");
|
||||
navigator.clipboard.writeText(emailValues.html);
|
||||
}}
|
||||
className="shrink-0"
|
||||
EndIcon={DocumentDuplicateIcon}>
|
||||
Copy code
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
title="send preview email"
|
||||
aria-label="send preview email"
|
||||
onClick={sendPreviewEmail}
|
||||
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}>
|
||||
{emailValues.html}
|
||||
</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 : {subject}</div>
|
||||
<div className="p-4">{previewEmailValues.Component}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getEmailValues = ({ survey, surveyUrl, brandColor, preview }) => {
|
||||
const doctype =
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
|
||||
|
||||
const Template = getEmailTemplate(survey, surveyUrl, brandColor, preview);
|
||||
const html = render(Template, { pretty: true });
|
||||
const htmlWithoutDoctype = html.replace(doctype, "");
|
||||
|
||||
return { Component: Template, html: htmlWithoutDoctype };
|
||||
};
|
||||
|
||||
const getEmailTemplate = (survey: TSurvey, surveyUrl: string, brandColor: string, preview: boolean) => {
|
||||
const url = preview ? `${surveyUrl}?preview=true` : surveyUrl;
|
||||
const urlWithPrefilling = preview ? `${surveyUrl}?preview=true&` : `${surveyUrl}?`;
|
||||
|
||||
const firstQuestion = survey.questions[0];
|
||||
switch (firstQuestion.type) {
|
||||
case QuestionType.OpenText:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Section className="mt-4 block h-20 w-full rounded-lg border border-solid border-gray-200 bg-slate-50" />
|
||||
<EmailFooter />
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case QuestionType.Consent:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Container className="m-0 text-sm font-normal leading-6 text-slate-500">
|
||||
<Text className="m-0 p-0" dangerouslySetInnerHTML={{ __html: firstQuestion.html || "" }}></Text>
|
||||
</Container>
|
||||
|
||||
<Container className="m-0 mt-4 block w-full max-w-none rounded-lg border border-solid 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="mx-0 mt-4 flex max-w-none justify-end">
|
||||
{!firstQuestion.required && (
|
||||
<EmailButton
|
||||
href={`${urlWithPrefilling}${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={`${urlWithPrefilling}${firstQuestion.id}=accepted`}
|
||||
className="bg-brand-color ml-2 inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-white">
|
||||
Accept
|
||||
</EmailButton>
|
||||
</Container>
|
||||
<EmailFooter />
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case QuestionType.NPS:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Section>
|
||||
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Container className="mx-0 mt-4 flex w-max flex-col">
|
||||
<Section className="block overflow-hidden rounded-md border border-gray-200">
|
||||
{Array.from({ length: 11 }, (_, i) => (
|
||||
<EmailButton
|
||||
key={i}
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=${i}`}
|
||||
className="m-0 inline-flex h-10 w-10 items-center justify-center border-gray-200 p-0 text-slate-800">
|
||||
{i}
|
||||
</EmailButton>
|
||||
))}
|
||||
</Section>
|
||||
<Section className="mt-2 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>
|
||||
{/* {!firstQuestion.required && (
|
||||
<EmailButton
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=dismissed`}
|
||||
className="mt-4 cursor-pointer appearance-none rounded-md bg-brand-color px-6 py-3 text-sm font-medium text-white">
|
||||
{firstQuestion.buttonLabel || "Skip"}
|
||||
</EmailButton>
|
||||
)} */}
|
||||
|
||||
<EmailFooter />
|
||||
</Section>
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case QuestionType.CTA:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Container className="mt-2 text-sm font-normal leading-6 text-slate-500">
|
||||
<Text className="m-0 p-0" dangerouslySetInnerHTML={{ __html: firstQuestion.html || "" }}></Text>
|
||||
</Container>
|
||||
|
||||
<Container className="mx-0 mt-4 max-w-none">
|
||||
{!firstQuestion.required && (
|
||||
<EmailButton
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=dismissed`}
|
||||
className="inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-black">
|
||||
{firstQuestion.dismissButtonLabel || "Skip"}
|
||||
</EmailButton>
|
||||
)}
|
||||
<EmailButton
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=clicked`}
|
||||
className="bg-brand-color inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-white">
|
||||
{firstQuestion.buttonLabel}
|
||||
</EmailButton>
|
||||
</Container>
|
||||
<EmailFooter />
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case QuestionType.Rating:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Section>
|
||||
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Container className="mx-0 mt-4 flex">
|
||||
<Section
|
||||
className={cn("inline-block w-max overflow-hidden rounded-md", {
|
||||
["border border-solid border-gray-200"]: firstQuestion.scale === "number",
|
||||
})}>
|
||||
{Array.from({ length: firstQuestion.range }, (_, i) => (
|
||||
<EmailButton
|
||||
key={i}
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=${i + 1}`}
|
||||
className={cn(
|
||||
"m-0 inline-flex h-16 w-16 items-center justify-center p-0 text-slate-800",
|
||||
{
|
||||
["border border-solid border-gray-200"]: firstQuestion.scale === "number",
|
||||
}
|
||||
)}>
|
||||
{firstQuestion.scale === "smiley" && <Text className="text-3xl">😃</Text>}
|
||||
{firstQuestion.scale === "number" && i + 1}
|
||||
{firstQuestion.scale === "star" && <Text className="text-3xl">⭐</Text>}
|
||||
</EmailButton>
|
||||
))}
|
||||
</Section>
|
||||
<Section className="m-0 px-1.5 text-xs leading-6 text-slate-500">
|
||||
<Row>
|
||||
<Column>
|
||||
<Text className="m-0 inline-block p-0">{firstQuestion.lowerLabel}</Text>
|
||||
</Column>
|
||||
<Column className="text-right">
|
||||
<Text className="m-0 inline-block p-0 text-right">{firstQuestion.upperLabel}</Text>
|
||||
</Column>
|
||||
</Row>
|
||||
</Section>
|
||||
</Container>
|
||||
{/* {!firstQuestion.required && (
|
||||
<EmailButton
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=dismissed`}
|
||||
className="mt-4 cursor-pointer appearance-none rounded-md bg-brand-color px-6 py-3 text-sm font-medium text-white">
|
||||
{firstQuestion.buttonLabel || "Skip"}
|
||||
</EmailButton>
|
||||
)} */}
|
||||
<EmailFooter />
|
||||
</Section>
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case QuestionType.MultipleChoiceMulti:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Container className="mx-0 max-w-none">
|
||||
{firstQuestion.choices.map((choice) => (
|
||||
<Section
|
||||
className="mt-2 block w-full rounded-lg border border-solid border-gray-200 bg-slate-50 p-4 text-slate-800"
|
||||
key={choice.id}>
|
||||
{choice.label}
|
||||
</Section>
|
||||
))}
|
||||
</Container>
|
||||
<EmailFooter />
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
case QuestionType.MultipleChoiceSingle:
|
||||
return (
|
||||
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
|
||||
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
|
||||
{firstQuestion.headline}
|
||||
</Text>
|
||||
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
|
||||
{firstQuestion.subheader}
|
||||
</Text>
|
||||
<Container className="mx-0 max-w-none">
|
||||
{firstQuestion.choices
|
||||
.filter((choice) => choice.id !== "other")
|
||||
.map((choice) => (
|
||||
<Link
|
||||
key={choice.id}
|
||||
className="mt-2 block rounded-lg border border-solid border-gray-200 bg-slate-50 p-4 text-slate-800 hover:bg-slate-100"
|
||||
href={`${urlWithPrefilling}${firstQuestion.id}=${choice.label}`}>
|
||||
{choice.label}
|
||||
</Link>
|
||||
))}
|
||||
</Container>
|
||||
<EmailFooter />
|
||||
</EmailTemplateWrapper>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const EmailTemplateWrapper = ({ children, surveyUrl, brandColor }) => {
|
||||
return (
|
||||
<Tailwind
|
||||
config={{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"brand-color": brandColor,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}>
|
||||
<Link
|
||||
href={surveyUrl}
|
||||
target="_blank"
|
||||
className="mx-0 my-2 block rounded-lg border border-solid border-slate-300 bg-white p-8 font-sans text-inherit">
|
||||
{children}
|
||||
</Link>
|
||||
</Tailwind>
|
||||
);
|
||||
};
|
||||
|
||||
const EmailFooter = () => {
|
||||
return (
|
||||
<Container className="m-auto mt-8 text-center ">
|
||||
<Link href="https://formbricks.com/" target="_blank" className="text-xs text-slate-400">
|
||||
Powered by Formbricks
|
||||
</Link>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import toast from "react-hot-toast";
|
||||
import { SurveyInline } from "@/components/shared/Survey";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { DocumentDuplicateIcon } from "@heroicons/react/24/solid";
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import { useRef } from "react";
|
||||
|
||||
interface EmailTabProps {
|
||||
surveyUrl: string;
|
||||
survey: TSurvey;
|
||||
brandColor: string;
|
||||
}
|
||||
|
||||
export default function LinkTab({ surveyUrl, survey, brandColor }: EmailTabProps) {
|
||||
const linkTextRef = useRef(null);
|
||||
|
||||
const handleTextSelection = () => {
|
||||
if (linkTextRef.current) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(linkTextRef.current);
|
||||
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full grow flex-col gap-5">
|
||||
<div className="flex flex-wrap justify-between gap-2">
|
||||
<div
|
||||
ref={linkTextRef}
|
||||
className="relative grow overflow-auto rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-800"
|
||||
onClick={() => handleTextSelection()}>
|
||||
<span style={{ wordBreak: "break-all" }}>{surveyUrl}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
title="Copy survey link to clipboard"
|
||||
aria-label="Copy survey link to clipboard"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(surveyUrl);
|
||||
toast.success("URL copied to clipboard!");
|
||||
}}
|
||||
EndIcon={DocumentDuplicateIcon}>
|
||||
Copy URL
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative grow overflow-y-scroll rounded-xl border border-gray-200 bg-white px-4 py-[18px]">
|
||||
<SurveyInline
|
||||
brandColor={brandColor}
|
||||
survey={survey}
|
||||
formbricksSignature={false}
|
||||
autoFocus={false}
|
||||
isRedirectDisabled={false}
|
||||
key={survey.id}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="minimal"
|
||||
className={cn(
|
||||
"absolute bottom-8 left-1/2 -translate-x-1/2 transform rounded-lg border border-slate-200 bg-white"
|
||||
)}
|
||||
EndIcon={ArrowUpRightIcon}
|
||||
title="Open survey in new tab"
|
||||
aria-label="Open survey in new tab"
|
||||
endIconClassName="h-4 w-4 "
|
||||
href={`${surveyUrl}?preview=true`}
|
||||
target="_blank">
|
||||
Open in new tab
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import toast from "react-hot-toast";
|
||||
import CodeBlock from "@/components/shared/CodeBlock";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { DocumentDuplicateIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export default function WebpageTab({ surveyUrl }) {
|
||||
const iframeCode = `<div style="position: relative; height:100vh; max-height:100vh; overflow:auto;">
|
||||
<iframe
|
||||
src="${surveyUrl}"
|
||||
frameborder="0" style="position: absolute; left:0; top:0; width:100%; height:100%; border:0;">
|
||||
</iframe>
|
||||
</div>`;
|
||||
|
||||
return (
|
||||
<div className="flex h-full grow flex-col gap-5">
|
||||
<div className="flex justify-between">
|
||||
<div className=""></div>
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
title="Embed survey in your website"
|
||||
aria-label="Embed survey in your website"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(iframeCode);
|
||||
toast.success("Embed code copied to clipboard!");
|
||||
}}
|
||||
EndIcon={DocumentDuplicateIcon}>
|
||||
Copy code
|
||||
</Button>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+8
@@ -10,6 +10,7 @@ import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { generateSurveySingleUseId } from "@/lib/singleUseSurveys";
|
||||
import { getProfile } from "@formbricks/lib/services/profile";
|
||||
|
||||
const generateSingleUseIds = (isEncrypted: boolean) => {
|
||||
return Array(5)
|
||||
@@ -44,6 +45,12 @@ export default async function Page({ params }) {
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const profile = await getProfile(session.user.id);
|
||||
if (!profile) {
|
||||
throw new Error("Profile not found");
|
||||
}
|
||||
|
||||
const tags = await getTagsByEnvironmentId(params.environmentId);
|
||||
|
||||
return (
|
||||
@@ -57,6 +64,7 @@ export default async function Page({ params }) {
|
||||
surveyBaseUrl={SURVEY_BASE_URL}
|
||||
singleUseIds={isSingleUseSurvey ? singleUseIds : undefined}
|
||||
product={product}
|
||||
profile={profile}
|
||||
environmentTags={tags}
|
||||
/>
|
||||
</>
|
||||
|
||||
+14
-1
@@ -24,6 +24,7 @@ import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown";
|
||||
import { TEnvironment } from "@formbricks/types/v1/environment";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { updateSurveyAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||
import { TProfile } from "@formbricks/types/v1/profile";
|
||||
|
||||
interface SummaryHeaderProps {
|
||||
surveyId: string;
|
||||
@@ -31,6 +32,7 @@ interface SummaryHeaderProps {
|
||||
survey: TSurvey;
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
singleUseIds?: string[];
|
||||
}
|
||||
const SummaryHeader = ({
|
||||
@@ -39,6 +41,7 @@ const SummaryHeader = ({
|
||||
survey,
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
singleUseIds,
|
||||
}: SummaryHeaderProps) => {
|
||||
const router = useRouter();
|
||||
@@ -55,7 +58,13 @@ const SummaryHeader = ({
|
||||
</div>
|
||||
<div className="hidden justify-end gap-x-1.5 sm:flex">
|
||||
{survey.type === "link" && (
|
||||
<LinkSurveyShareButton survey={survey} surveyBaseUrl={surveyBaseUrl} singleUseIds={singleUseIds} />
|
||||
<LinkSurveyShareButton
|
||||
survey={survey}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
singleUseIds={singleUseIds}
|
||||
product={product}
|
||||
profile={profile}
|
||||
/>
|
||||
)}
|
||||
{(environment?.widgetSetupCompleted || survey.type === "link") && survey?.status !== "draft" ? (
|
||||
<SurveyStatusDropdown environment={environment} survey={survey} />
|
||||
@@ -82,6 +91,8 @@ const SummaryHeader = ({
|
||||
className="flex w-full justify-center p-1"
|
||||
survey={survey}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
profile={profile}
|
||||
singleUseIds={singleUseIds}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -164,6 +175,8 @@ const SummaryHeader = ({
|
||||
environment={environment}
|
||||
survey={survey}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
profile={profile}
|
||||
singleUseIds={singleUseIds}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,16 @@ export function getPrefillResponseData(
|
||||
|
||||
const answer = transformAnswer(question, firstQuestionPrefill || "");
|
||||
const answerObj = { [firstQuestionId]: answer };
|
||||
|
||||
if (
|
||||
question.type === QuestionType.CTA &&
|
||||
question.buttonExternal &&
|
||||
question.buttonUrl &&
|
||||
answer === "clicked"
|
||||
) {
|
||||
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>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@json2csv/node": "^7.0.3",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@radix-ui/react-collapsible": "^1.0.3",
|
||||
"@react-email/components": "^0.0.7",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@sentry/nextjs": "^7.72.0",
|
||||
"@t3-oss/env-nextjs": "^0.6.1",
|
||||
@@ -42,6 +43,7 @@
|
||||
"react": "18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-email": "^1.9.4",
|
||||
"react-hook-form": "^7.46.2",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-icons": "^4.11.0",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getQuestionResponseMapping } from "../responses";
|
||||
import { WEBAPP_URL } from "../constants";
|
||||
import { withEmailTemplate } from "./email-template";
|
||||
import { createInviteToken, createToken } from "../jwt";
|
||||
import { Question } from "@formbricks/types/questions";
|
||||
import { TResponse } from "@formbricks/types/v1/responses";
|
||||
import { WEBAPP_URL } from "../constants";
|
||||
import { createInviteToken, createToken } from "../jwt";
|
||||
import { getQuestionResponseMapping } from "../responses";
|
||||
import { withEmailTemplate } from "./email-template";
|
||||
|
||||
const nodemailer = require("nodemailer");
|
||||
|
||||
@@ -170,3 +170,14 @@ export const sendResponseFinishedEmail = async (
|
||||
`),
|
||||
});
|
||||
};
|
||||
|
||||
export const sendEmbedSurveyPreviewEmail = async (to: string, subject: string, html: string) => {
|
||||
await sendEmail({
|
||||
to: to,
|
||||
subject: subject,
|
||||
html: withEmailTemplate(`
|
||||
<h1>Preview Email Embed</h1>
|
||||
<p>This is how the code snippet looks embedded into an email:</p>
|
||||
${html}`),
|
||||
});
|
||||
};
|
||||
|
||||
Generated
+1750
-333
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user