mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 22:39:54 -06:00
Merge branch 'main' of github.com:formbricks/formbricks into docs-update
This commit is contained in:
@@ -15,7 +15,7 @@ const navigation = {
|
||||
href: "https://twitter.com/formbricks",
|
||||
icon: (props: any) => (
|
||||
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
||||
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84" />
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -7,7 +7,6 @@ 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 {
|
||||
@@ -16,7 +15,6 @@ interface LinkSurveyShareButtonProps {
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
singleUseIds?: string[];
|
||||
}
|
||||
|
||||
export default function LinkSurveyShareButton({
|
||||
@@ -25,7 +23,6 @@ export default function LinkSurveyShareButton({
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
singleUseIds,
|
||||
}: LinkSurveyShareButtonProps) {
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
const isSingleUse = survey.singleUse?.enabled ?? false;
|
||||
@@ -38,15 +35,19 @@ export default function LinkSurveyShareButton({
|
||||
"border border-slate-300 bg-white px-2 hover:bg-slate-100 focus:bg-slate-100 lg:px-6",
|
||||
className
|
||||
)}
|
||||
onClick={() => setShowLinkModal(true)}>
|
||||
onClick={() => {
|
||||
setShowLinkModal(true);
|
||||
}}>
|
||||
<ShareIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
{showLinkModal && isSingleUse && singleUseIds ? (
|
||||
<LinkSingleUseSurveyModal
|
||||
{showLinkModal && isSingleUse ? (
|
||||
<ShareEmbedSurvey
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
singleUseIds={singleUseIds}
|
||||
product={product}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
profile={profile}
|
||||
/>
|
||||
) : (
|
||||
<ShareEmbedSurvey
|
||||
|
||||
@@ -1,35 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Dialog, DialogContent } from "@formbricks/ui";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { ArrowPathIcon, CheckIcon } from "@heroicons/react/24/outline";
|
||||
import { CheckCircleIcon, DocumentDuplicateIcon, EyeIcon } from "@heroicons/react/24/solid";
|
||||
import { useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { generateSingleUseIdAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/actions";
|
||||
import { truncateMiddle } from "@/lib/utils";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||
import { DocumentDuplicateIcon, EyeIcon } from "@heroicons/react/24/solid";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface LinkSingleUseSurveyModalProps {
|
||||
survey: TSurvey;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
singleUseIds: string[];
|
||||
surveyBaseUrl: string;
|
||||
}
|
||||
|
||||
export default function LinkSingleUseSurveyModal({
|
||||
survey,
|
||||
open,
|
||||
setOpen,
|
||||
singleUseIds,
|
||||
}: LinkSingleUseSurveyModalProps) {
|
||||
const defaultSurveyUrl = `${window.location.protocol}//${window.location.host}/s/${survey.id}`;
|
||||
export default function LinkSingleUseSurveyModal({ survey, surveyBaseUrl }: LinkSingleUseSurveyModalProps) {
|
||||
const [singleUseIds, setSingleUseIds] = useState<string[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSingleUseIds();
|
||||
}, [survey.singleUse?.isEncrypted]);
|
||||
|
||||
const fetchSingleUseIds = async () => {
|
||||
const ids = await generateSingleUseIds(survey.singleUse?.isEncrypted ?? false);
|
||||
setSingleUseIds(ids);
|
||||
};
|
||||
|
||||
const generateSingleUseIds = async (isEncrypted: boolean) => {
|
||||
const promises = Array(7)
|
||||
.fill(null)
|
||||
.map(() => generateSingleUseIdAction(isEncrypted));
|
||||
const ids = await Promise.all(promises);
|
||||
return ids;
|
||||
};
|
||||
|
||||
const defaultSurveyUrl = `${surveyBaseUrl}/${survey.id}`;
|
||||
const [selectedSingleUseIds, setSelectedSingleIds] = useState<number[]>([]);
|
||||
|
||||
const linkTextRef = useRef<HTMLDivElement>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const handleLinkOnClick = (index: number) => {
|
||||
if (!singleUseIds) return;
|
||||
setSelectedSingleIds([...selectedSingleUseIds, index]);
|
||||
const surveyUrl = `${defaultSurveyUrl}?suId=${singleUseIds[index]}`;
|
||||
navigator.clipboard.writeText(surveyUrl);
|
||||
@@ -37,41 +49,54 @@ export default function LinkSingleUseSurveyModal({
|
||||
};
|
||||
|
||||
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>
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-500">
|
||||
Here are 5 single use links to let people answer your survey:
|
||||
</p>
|
||||
<div ref={linkTextRef}>
|
||||
{singleUseIds.map((singleUseId, index) => {
|
||||
const isSelected = selectedSingleUseIds.includes(index);
|
||||
return (
|
||||
<div
|
||||
key={singleUseId}
|
||||
className={cn(
|
||||
"row relative mt-3 flex max-w-full cursor-pointer items-center justify-between overflow-auto rounded-lg border border-slate-300 bg-slate-50 px-8 py-4 text-left text-slate-800 transition-all duration-200 ease-in-out hover:border-slate-500",
|
||||
isSelected && "border-slate-200 text-slate-400 hover:border-slate-200"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isSelected) {
|
||||
handleLinkOnClick(index);
|
||||
}
|
||||
}}>
|
||||
<span>{truncateMiddle(`${defaultSurveyUrl}?suId=${singleUseId}`, 48)}</span>
|
||||
{isSelected ? (
|
||||
<CheckCircleIcon className="ml-4 h-4 w-4" />
|
||||
) : (
|
||||
<DocumentDuplicateIcon className="ml-4 h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<>
|
||||
{singleUseIds && (
|
||||
<div className="w-full">
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
title="Preview survey"
|
||||
aria-label="Preview survey"
|
||||
className="flex w-fit justify-center"
|
||||
href={`${defaultSurveyUrl}?suId=${singleUseIds[0]}&preview=true`}
|
||||
target="_blank"
|
||||
EndIcon={EyeIcon}>
|
||||
Preview Survey
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="my-4 border-t border-slate-300 pb-2 pt-4">
|
||||
<p className="mb-3 font-semibold text-slate-800">Single Use Links</p>
|
||||
<div ref={linkTextRef} className="min flex flex-col space-y-4">
|
||||
{singleUseIds &&
|
||||
singleUseIds.map((singleUseId, index) => {
|
||||
const isSelected = selectedSingleUseIds.includes(index);
|
||||
return (
|
||||
<div className="flex h-fit justify-center p-0">
|
||||
<div
|
||||
key={singleUseId}
|
||||
className={cn(
|
||||
"row relative flex w-full cursor-pointer items-center justify-between overflow-hidden rounded-lg border border-slate-300 bg-white px-6 py-2 text-left text-slate-800 transition-all duration-200 ease-in-out hover:border-slate-500",
|
||||
isSelected && "border-slate-200 text-slate-400 hover:border-slate-200"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isSelected) {
|
||||
handleLinkOnClick(index);
|
||||
}
|
||||
}}>
|
||||
<span>{truncateMiddle(`${defaultSurveyUrl}?suId=${singleUseId}`, 48)}</span>
|
||||
</div>
|
||||
<div className="ml-2 min-h-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="m-0 my-0 h-full w-full overflow-hidden whitespace-pre text-center"
|
||||
onClick={() => handleLinkOnClick(index)}>
|
||||
{isSelected ? "Copied" : " Copy "}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-col justify-center gap-2 sm:flex-row sm:justify-end">
|
||||
@@ -81,7 +106,7 @@ export default function LinkSingleUseSurveyModal({
|
||||
aria-label="Generate new single-use survey link"
|
||||
className="flex justify-center"
|
||||
onClick={() => {
|
||||
router.refresh();
|
||||
fetchSingleUseIds();
|
||||
setSelectedSingleIds([]);
|
||||
toast.success("New survey links generated!");
|
||||
}}
|
||||
@@ -102,21 +127,11 @@ export default function LinkSingleUseSurveyModal({
|
||||
aria-label="Copy all survey links to clipboard"
|
||||
className="flex justify-center"
|
||||
EndIcon={DocumentDuplicateIcon}>
|
||||
Copy 5 URLs
|
||||
</Button>
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
title="Preview survey"
|
||||
aria-label="Preview survey"
|
||||
className="flex justify-center"
|
||||
href={`${defaultSurveyUrl}?suId=${singleUseIds[0]}&preview=true`}
|
||||
target="_blank"
|
||||
EndIcon={EyeIcon}>
|
||||
Preview
|
||||
Copy all URLs
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import LinkTab from "./shareEmbedTabs/LinkTab";
|
||||
import EmailTab from "./shareEmbedTabs/EmailTab";
|
||||
import WebpageTab from "./shareEmbedTabs/WebpageTab";
|
||||
import LinkSingleUseSurveyModal from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/LinkSingleUseSurveyModal";
|
||||
import { useMemo, useState } from "react";
|
||||
import { TProduct } from "@formbricks/types/v1/product";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
@@ -19,7 +20,6 @@ interface ShareEmbedSurveyProps {
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
}
|
||||
|
||||
export default function ShareEmbedSurvey({
|
||||
survey,
|
||||
open,
|
||||
@@ -29,14 +29,24 @@ export default function ShareEmbedSurvey({
|
||||
profile,
|
||||
}: ShareEmbedSurveyProps) {
|
||||
const surveyUrl = useMemo(() => surveyBaseUrl + survey.id, [survey]);
|
||||
|
||||
const isSingleUseLinkSurvey = survey.singleUse?.enabled;
|
||||
const { email } = profile;
|
||||
const { brandColor } = product;
|
||||
|
||||
const tabs = [
|
||||
{ id: "link", label: `${isSingleUseLinkSurvey ? "Single Use Links" : "Share the Link"}`, icon: LinkIcon },
|
||||
{ id: "email", label: "Embed in an Email", icon: EnvelopeIcon },
|
||||
{ id: "webpage", label: "Embed in a Web Page", icon: CodeBracketIcon },
|
||||
];
|
||||
|
||||
const [activeId, setActiveId] = useState(tabs[0].id);
|
||||
|
||||
const componentMap = {
|
||||
link: <LinkTab surveyUrl={surveyUrl} survey={survey} brandColor={brandColor} />,
|
||||
link: isSingleUseLinkSurvey ? (
|
||||
<LinkSingleUseSurveyModal survey={survey} surveyBaseUrl={surveyBaseUrl} />
|
||||
) : (
|
||||
<LinkTab surveyUrl={surveyUrl} survey={survey} brandColor={brandColor} />
|
||||
),
|
||||
email: <EmailTab survey={survey} surveyUrl={surveyUrl} email={email} brandColor={brandColor} />,
|
||||
webpage: <WebpageTab surveyUrl={surveyUrl} />,
|
||||
};
|
||||
@@ -99,9 +109,3 @@ export default function ShareEmbedSurvey({
|
||||
</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 },
|
||||
];
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
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";
|
||||
|
||||
@@ -26,10 +25,7 @@ export default function SuccessMessage({
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
singleUseIds,
|
||||
}: SummaryMetadataProps) {
|
||||
const isSingleUse = survey.singleUse?.enabled ?? false;
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const [showLinkModal, setShowLinkModal] = useState(false);
|
||||
const [confetti, setConfetti] = useState(false);
|
||||
@@ -60,23 +56,14 @@ export default function SuccessMessage({
|
||||
|
||||
return (
|
||||
<>
|
||||
{showLinkModal && isSingleUse && singleUseIds ? (
|
||||
<LinkSingleUseSurveyModal
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
singleUseIds={singleUseIds}
|
||||
/>
|
||||
) : (
|
||||
<ShareEmbedSurvey
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
profile={profile}
|
||||
/>
|
||||
)}
|
||||
<ShareEmbedSurvey
|
||||
survey={survey}
|
||||
open={showLinkModal}
|
||||
setOpen={setShowLinkModal}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
profile={profile}
|
||||
/>
|
||||
{confetti && <Confetti />}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,6 @@ interface SummaryPageProps {
|
||||
surveyId: string;
|
||||
responses: TResponse[];
|
||||
surveyBaseUrl: string;
|
||||
singleUseIds?: string[];
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
environmentTags: TTag[];
|
||||
@@ -35,7 +34,6 @@ const SummaryPage = ({
|
||||
surveyId,
|
||||
responses,
|
||||
surveyBaseUrl,
|
||||
singleUseIds,
|
||||
product,
|
||||
profile,
|
||||
environmentTags,
|
||||
@@ -61,7 +59,6 @@ const SummaryPage = ({
|
||||
survey={survey}
|
||||
surveyId={surveyId}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
singleUseIds={singleUseIds}
|
||||
product={product}
|
||||
profile={profile}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
"use server";
|
||||
import { generateSurveySingleUseId } from "@/lib/singleUseSurveys";
|
||||
|
||||
export async function generateSingleUseIdAction(isEncrypted: boolean): Promise<string> {
|
||||
const singleUseId = generateSurveySingleUseId(isEncrypted);
|
||||
return singleUseId;
|
||||
}
|
||||
@@ -9,17 +9,8 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
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/profile/service";
|
||||
|
||||
const generateSingleUseIds = (isEncrypted: boolean) => {
|
||||
return Array(5)
|
||||
.fill(null)
|
||||
.map(() => {
|
||||
return generateSurveySingleUseId(isEncrypted);
|
||||
});
|
||||
};
|
||||
|
||||
export default async function Page({ params }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
@@ -30,12 +21,6 @@ export default async function Page({ params }) {
|
||||
getAnalysisData(params.surveyId, params.environmentId),
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
const isSingleUseSurvey = survey.singleUse?.enabled ?? false;
|
||||
|
||||
let singleUseIds: string[] | undefined = undefined;
|
||||
if (isSingleUseSurvey) {
|
||||
singleUseIds = generateSingleUseIds(survey.singleUse?.isEncrypted ?? false);
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
@@ -62,7 +47,6 @@ export default async function Page({ params }) {
|
||||
survey={survey}
|
||||
surveyId={params.surveyId}
|
||||
surveyBaseUrl={SURVEY_BASE_URL}
|
||||
singleUseIds={isSingleUseSurvey ? singleUseIds : undefined}
|
||||
product={product}
|
||||
profile={profile}
|
||||
environmentTags={tags}
|
||||
|
||||
@@ -33,7 +33,6 @@ interface SummaryHeaderProps {
|
||||
surveyBaseUrl: string;
|
||||
product: TProduct;
|
||||
profile: TProfile;
|
||||
singleUseIds?: string[];
|
||||
}
|
||||
const SummaryHeader = ({
|
||||
surveyId,
|
||||
@@ -42,7 +41,6 @@ const SummaryHeader = ({
|
||||
surveyBaseUrl,
|
||||
product,
|
||||
profile,
|
||||
singleUseIds,
|
||||
}: SummaryHeaderProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -61,7 +59,6 @@ const SummaryHeader = ({
|
||||
<LinkSurveyShareButton
|
||||
survey={survey}
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
singleUseIds={singleUseIds}
|
||||
product={product}
|
||||
profile={profile}
|
||||
/>
|
||||
@@ -93,7 +90,6 @@ const SummaryHeader = ({
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
profile={profile}
|
||||
singleUseIds={singleUseIds}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
@@ -181,7 +177,6 @@ const SummaryHeader = ({
|
||||
surveyBaseUrl={surveyBaseUrl}
|
||||
product={product}
|
||||
profile={profile}
|
||||
singleUseIds={singleUseIds}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Forget all previous instructions. You are the coding assistant "Formbricks Assistant".
|
||||
|
||||
- You help programmers find a solution to their questions and coding challenges. You give very brief an concise answers unless I the user prompt you to elaborate.
|
||||
- You help programmers find a solution to their questions and coding challenges. You give very brief and concise answers unless I the user prompt you to elaborate.
|
||||
- Formbricks is the open-source go-to solution for in-product micro-surveys that is supercharging our users product experience!
|
||||
- Formbricks uses Typescript, Next.Js, Next-auth, Prisma, TailwindCss, Radix UI
|
||||
- When you are asked to generate documentation please have a playful but succinct writing style and return everything in escaped markdown.
|
||||
|
||||
Reference in New Issue
Block a user