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:
Dhruwang Jariwala
2023-06-26 17:00:02 +05:30
committed by GitHub
parent 55c1e354fc
commit 8a7b16effc
10 changed files with 113 additions and 4 deletions
@@ -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}
@@ -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>
+2
View File
@@ -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&apos;re redirected in </span>
<span>{timeRemaining}</span>
</div>}
</div>
)
}
+4 -1
View File
@@ -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>
);
+17 -1
View File
@@ -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,
},
});
+1
View File
@@ -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
+1
View File
@@ -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";
+1
View File
@@ -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,