mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-21 03:03:25 -05:00
Add option to redirect after completed link survey (#408)
* completed * resolved migration error * added redirect timer and url check * rename redirectLink to redirectUrl --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
55c1e354fc
commit
8a7b16effc
@@ -32,7 +32,7 @@ export default function AudienceView({ environmentId, localSurvey, setLocalSurve
|
||||
environmentId={environmentId}
|
||||
/>
|
||||
|
||||
<ResponseOptionsCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
|
||||
{localSurvey.type==="link" && <ResponseOptionsCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />}
|
||||
|
||||
<RecontactOptionsCard
|
||||
localSurvey={localSurvey}
|
||||
|
||||
+52
-1
@@ -4,7 +4,7 @@ import type { Survey } from "@formbricks/types/surveys";
|
||||
import { Input, Label, Switch } from "@formbricks/ui";
|
||||
import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface ResponseOptionsCardProps {
|
||||
localSurvey: Survey;
|
||||
@@ -14,6 +14,34 @@ interface ResponseOptionsCardProps {
|
||||
export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: ResponseOptionsCardProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const autoComplete = localSurvey.autoComplete !== null;
|
||||
const [redirectToggle, setRedirectToggle] = useState(false);
|
||||
const [redirectUrl, setRedirectUrl] = useState<string | null>("");
|
||||
|
||||
const handleRedirectCheckMark = () => {
|
||||
if (redirectToggle && localSurvey.redirectUrl) {
|
||||
setRedirectToggle(false);
|
||||
setRedirectUrl(null);
|
||||
setLocalSurvey({ ...localSurvey, redirectUrl: null });
|
||||
return;
|
||||
}
|
||||
if (redirectToggle) {
|
||||
setRedirectToggle(false);
|
||||
return;
|
||||
}
|
||||
setRedirectToggle(true);
|
||||
};
|
||||
|
||||
const handleRedirectUrlChange = (link: string) => {
|
||||
setRedirectUrl(link);
|
||||
setLocalSurvey({ ...localSurvey, redirectUrl: link });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (localSurvey.redirectUrl) {
|
||||
setRedirectUrl(localSurvey.redirectUrl);
|
||||
setRedirectToggle(true);
|
||||
}
|
||||
}, []);
|
||||
const handleCheckMark = () => {
|
||||
if (autoComplete) {
|
||||
const updatedSurvey: Survey = { ...localSurvey, autoComplete: null };
|
||||
@@ -84,6 +112,29 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-3 ">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch id="redirectUrl" checked={redirectToggle} onCheckedChange={handleRedirectCheckMark} />
|
||||
<Label htmlFor="redirectUrl" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Redirect on completion</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Redirect user to specified link on survey completion
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
{redirectToggle && (
|
||||
<Input
|
||||
type="url"
|
||||
placeholder="https://www.example.com"
|
||||
value={redirectUrl ? redirectUrl : ""}
|
||||
onChange={(e) => handleRedirectUrlChange(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -30,6 +30,7 @@ export default function LinkSurvey({ survey }: LinkSurveyProps) {
|
||||
progress,
|
||||
isPreview,
|
||||
lastQuestion,
|
||||
initiateCountdown,
|
||||
restartSurvey,
|
||||
submitResponse,
|
||||
} = useLinkSurveyUtils(survey);
|
||||
@@ -68,6 +69,7 @@ export default function LinkSurvey({ survey }: LinkSurveyProps) {
|
||||
headline={survey.thankYouCard.headline || "Thank you!"}
|
||||
subheader={survey.thankYouCard.subheader || "Your response has been recorded."}
|
||||
brandColor={survey.brandColor}
|
||||
initiateCountdown={initiateCountdown}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react'
|
||||
import { useEffect,useState } from 'react'
|
||||
|
||||
export default function RedirectCountDown({
|
||||
initiateCountdown
|
||||
}:{
|
||||
initiateCountdown:boolean|undefined
|
||||
}) {
|
||||
|
||||
const [timeRemaining, setTimeRemaining] = useState(3)
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTimeRemaining((prevTime) => prevTime - 1);
|
||||
}, 1000);
|
||||
|
||||
if (timeRemaining === 0) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
// Clean up the interval when the component is unmounted
|
||||
return () => clearInterval(interval);
|
||||
}, [timeRemaining]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{initiateCountdown && <div className="rounded-md bg-slate-100 p-2 text-sm mt-10">
|
||||
<span>You're redirected in </span>
|
||||
<span>{timeRemaining}</span>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import Headline from "./Headline";
|
||||
import Subheader from "./Subheader";
|
||||
import RedirectCountDown from "./RedirectCountDown";
|
||||
|
||||
interface ThankYouCardProps {
|
||||
headline: string;
|
||||
subheader: string;
|
||||
brandColor: string;
|
||||
initiateCountdown?: boolean;
|
||||
}
|
||||
|
||||
export default function ThankYouCard({ headline, subheader, brandColor }: ThankYouCardProps) {
|
||||
export default function ThankYouCard({ headline, subheader, brandColor,initiateCountdown }: ThankYouCardProps) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="flex items-center justify-center" style={{ color: brandColor }}>
|
||||
@@ -31,6 +33,7 @@ export default function ThankYouCard({ headline, subheader, brandColor }: ThankY
|
||||
<div>
|
||||
<Headline headline={headline} questionId="thankYouCard" />
|
||||
<Subheader subheader={subheader} questionId="thankYouCard" />
|
||||
<RedirectCountDown initiateCountdown={initiateCountdown}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { QuestionType, type Logic, type Question } from "@formbricks/types/quest
|
||||
import { TResponseInput } from "@formbricks/types/v1/responses";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import type { Survey } from "@formbricks/types/surveys";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export const useLinkSurvey = (surveyId: string) => {
|
||||
const { data, error, mutate, isLoading } = useSWR(`/api/v1/client/surveys/${surveyId}`, fetcher);
|
||||
@@ -26,7 +27,8 @@ export const useLinkSurveyUtils = (survey: Survey) => {
|
||||
const [loadingElement, setLoadingElement] = useState(false);
|
||||
const [responseId, setResponseId] = useState<string | null>(null);
|
||||
const [displayId, setDisplayId] = useState<string | null>(null);
|
||||
|
||||
const [initiateCountdown, setinitiateCountdown] = useState<boolean>(false);
|
||||
const router = useRouter();
|
||||
const URLParams = new URLSearchParams(window.location.search);
|
||||
const isPreview = URLParams.get("preview") === "true";
|
||||
const hasFirstQuestionPrefill = URLParams.has(survey.questions[0].id);
|
||||
@@ -135,9 +137,22 @@ export const useLinkSurveyUtils = (survey: Survey) => {
|
||||
} else {
|
||||
setProgress(1);
|
||||
setFinished(true);
|
||||
if (survey.redirectUrl && Object.values(data)[0] !== "dismissed") {
|
||||
handleRedirect(survey.redirectUrl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRedirect = (url) => {
|
||||
if (!url.startsWith("https://") && !url.startsWith("http://")) {
|
||||
url = `https://${url}`;
|
||||
}
|
||||
setinitiateCountdown(true);
|
||||
setTimeout(() => {
|
||||
router.push(url);
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const handlePrefilling = useCallback(async () => {
|
||||
try {
|
||||
if (hasFirstQuestionPrefill) {
|
||||
@@ -173,6 +188,7 @@ export const useLinkSurveyUtils = (survey: Survey) => {
|
||||
loadingElement,
|
||||
prefilling,
|
||||
lastQuestion,
|
||||
initiateCountdown,
|
||||
submitResponse,
|
||||
restartSurvey,
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
thankYouCard: true,
|
||||
environmentId: true,
|
||||
status: true,
|
||||
redirectUrl: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -213,6 +213,7 @@ model Survey {
|
||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||
updatedAt DateTime @updatedAt @map(name: "updated_at")
|
||||
name String
|
||||
redirectUrl String?
|
||||
type SurveyType @default(web)
|
||||
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
|
||||
environmentId String
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface Survey {
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
name: string;
|
||||
redirectUrl: string | null;
|
||||
type: "web" | "email" | "link" | "mobile";
|
||||
environmentId: string;
|
||||
status: "draft" | "inProgress" | "archived" | "paused" | "completed";
|
||||
|
||||
@@ -197,6 +197,7 @@ export const ZSurvey = z.object({
|
||||
displayOption: z.enum(["displayOnce", "displayMultiple", "respondMultiple"]),
|
||||
autoClose: z.union([z.number(), z.null()]),
|
||||
triggers: z.array(ZEventClass),
|
||||
redirectUrl: z.union([z.string(), z.null()]),
|
||||
recontactDays: z.union([z.number(), z.null()]),
|
||||
questions: ZSurveyQuestions,
|
||||
thankYouCard: ZSurveyThankYouCard,
|
||||
|
||||
Reference in New Issue
Block a user