mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 02:10:12 -06:00
Survey Completed message shown to the enduser can now be customized (#464)
* feat: added surveyClosedMessage field to database and also added it's types * feat: added the custom closed message to the frontend * fix: fixes build issue * fix: refactored the code to toggle surveyClosedMessage and redirectUrl * pnpm format * recreate prisma migration --------- Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
@@ -18,20 +18,20 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
|
||||
const [surveyCloseOnDateToggle, setSurveyCloseOnDateToggle] = useState(false);
|
||||
|
||||
const [redirectUrl, setRedirectUrl] = useState<string | null>("");
|
||||
const [surveyClosedMessageToggle, setSurveyClosedMessageToggle] = useState(false);
|
||||
const [surveyClosedMessage, setSurveyClosedMessage] = useState({
|
||||
heading: "Survey Completed",
|
||||
subheading: "This free & open-source survey has been closed",
|
||||
});
|
||||
const [closeOnDate, setCloseOnDate] = useState<Date>();
|
||||
|
||||
const handleRedirectCheckMark = () => {
|
||||
setRedirectToggle((prev) => !prev);
|
||||
|
||||
if (redirectToggle && localSurvey.redirectUrl) {
|
||||
setRedirectToggle(false);
|
||||
setRedirectUrl(null);
|
||||
setLocalSurvey({ ...localSurvey, redirectUrl: null });
|
||||
return;
|
||||
}
|
||||
if (redirectToggle) {
|
||||
setRedirectToggle(false);
|
||||
return;
|
||||
}
|
||||
setRedirectToggle(true);
|
||||
};
|
||||
|
||||
const handleSurveyCloseOnDateToggle = () => {
|
||||
@@ -54,6 +54,14 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
|
||||
setLocalSurvey({ ...localSurvey, redirectUrl: link });
|
||||
};
|
||||
|
||||
const handleCloseSurveyMessageToggle = () => {
|
||||
setSurveyClosedMessageToggle((prev) => !prev);
|
||||
|
||||
if (surveyClosedMessageToggle && localSurvey.surveyClosedMessage) {
|
||||
setLocalSurvey({ ...localSurvey, surveyClosedMessage: null });
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseOnDateChange = (date: Date) => {
|
||||
const equivalentDate = date?.getDate();
|
||||
date?.setUTCHours(0, 0, 0, 0);
|
||||
@@ -63,11 +71,36 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
|
||||
setLocalSurvey({ ...localSurvey, closeOnDate: date ?? null });
|
||||
};
|
||||
|
||||
const handleClosedSurveyMessageChange = ({
|
||||
heading,
|
||||
subheading,
|
||||
}: {
|
||||
heading?: string;
|
||||
subheading?: string;
|
||||
}) => {
|
||||
const message = {
|
||||
heading: heading ?? surveyClosedMessage.heading,
|
||||
subheading: subheading ?? surveyClosedMessage.subheading,
|
||||
};
|
||||
|
||||
setSurveyClosedMessage(message);
|
||||
setLocalSurvey({ ...localSurvey, surveyClosedMessage: message });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (localSurvey.redirectUrl) {
|
||||
setRedirectUrl(localSurvey.redirectUrl);
|
||||
setRedirectToggle(true);
|
||||
}
|
||||
|
||||
if (!!localSurvey.surveyClosedMessage) {
|
||||
setSurveyClosedMessage({
|
||||
heading: localSurvey.surveyClosedMessage.heading ?? surveyClosedMessage.heading,
|
||||
subheading: localSurvey.surveyClosedMessage.subheading ?? surveyClosedMessage.subheading,
|
||||
});
|
||||
setSurveyClosedMessageToggle(true);
|
||||
}
|
||||
|
||||
if (localSurvey.closeOnDate) {
|
||||
setCloseOnDate(localSurvey.closeOnDate);
|
||||
setSurveyCloseOnDateToggle(true);
|
||||
@@ -144,6 +177,75 @@ export default function ResponseOptionsCard({ localSurvey, setLocalSurvey }: Res
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{localSurvey.type === "link" && (
|
||||
<>
|
||||
<div className="ml-2 flex items-center space-x-1 p-4">
|
||||
<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>
|
||||
{redirectToggle && (
|
||||
<div className="ml-2 space-x-1 px-4 pb-4">
|
||||
<Input
|
||||
type="url"
|
||||
placeholder="https://www.example.com"
|
||||
value={redirectUrl ? redirectUrl : ""}
|
||||
onChange={(e) => handleRedirectUrlChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="ml-2 flex items-center space-x-1 p-4">
|
||||
<Switch
|
||||
id="redirectUrl"
|
||||
checked={surveyClosedMessageToggle}
|
||||
onCheckedChange={handleCloseSurveyMessageToggle}
|
||||
/>
|
||||
<Label htmlFor="redirectUrl" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">
|
||||
{"Adjust 'Survey Closed' Message"}
|
||||
</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Change the message visitors see when the survey is closed.
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
{surveyClosedMessageToggle && (
|
||||
<div className="ml-2 space-x-1 px-4 pb-4">
|
||||
<div>
|
||||
<Label htmlFor="headline">Heading</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="heading"
|
||||
name="heading"
|
||||
defaultValue={surveyClosedMessage.heading}
|
||||
onChange={(e) => handleClosedSurveyMessageChange({ heading: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="headline">Subheading</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
id="subheading"
|
||||
name="subheading"
|
||||
defaultValue={surveyClosedMessage.subheading}
|
||||
onChange={(e) => handleClosedSurveyMessageChange({ subheading: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="p-3 ">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch id="redirectUrl" checked={redirectToggle} onCheckedChange={handleRedirectCheckMark} />
|
||||
|
||||
@@ -5,7 +5,13 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@formbricks/ui";
|
||||
|
||||
const SurveyInactive = ({ status }) => {
|
||||
const SurveyInactive = ({
|
||||
status,
|
||||
surveyClosedMessage,
|
||||
}: {
|
||||
status: string;
|
||||
surveyClosedMessage?: { heading: string; subheading: string };
|
||||
}) => {
|
||||
const icons = {
|
||||
"not found": <QuestionMarkCircleIcon className="h-20 w-20" />,
|
||||
paused: <PauseCircleIcon className="h-20 w-20" />,
|
||||
@@ -23,11 +29,20 @@ const SurveyInactive = ({ status }) => {
|
||||
<div></div>
|
||||
<div className="flex flex-col items-center space-y-3 text-slate-300">
|
||||
{icons[status]}
|
||||
<h1 className="text-4xl font-bold text-slate-800">Survey {status}.</h1>
|
||||
<p className="text-lg leading-10 text-gray-500">{descriptions[status]}</p>
|
||||
<Button variant="darkCTA" className="mt-2" href="https://formbricks.com">
|
||||
Create your own
|
||||
</Button>
|
||||
<h1 className="text-4xl font-bold text-slate-800">
|
||||
{status === "completed" && surveyClosedMessage ? surveyClosedMessage.heading : `Survey ${status}.`}
|
||||
</h1>
|
||||
<p className="text-lg leading-10 text-gray-500">
|
||||
{" "}
|
||||
{status === "completed" && surveyClosedMessage
|
||||
? surveyClosedMessage.subheading
|
||||
: descriptions[status]}
|
||||
</p>
|
||||
{!(status === "completed" && surveyClosedMessage) && (
|
||||
<Button variant="darkCTA" className="mt-2" href="https://formbricks.com">
|
||||
Create your own
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Link href="https://formbricks.com">
|
||||
|
||||
@@ -26,7 +26,12 @@ export default function SurveyPage({ surveyId }: SurveyPageProps) {
|
||||
}
|
||||
|
||||
if (isErrorSurvey && isErrorSurvey.status === 403) {
|
||||
return <SurveyInactive status={isErrorSurvey.info.reason} />;
|
||||
return (
|
||||
<SurveyInactive
|
||||
status={isErrorSurvey.info.reason}
|
||||
surveyClosedMessage={isErrorSurvey.info?.surveyClosedMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -28,6 +28,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
environmentId: true,
|
||||
status: true,
|
||||
redirectUrl: true,
|
||||
surveyClosedMessage: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -57,6 +58,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
reason: survey.status,
|
||||
brandColor: product?.brandColor,
|
||||
formbricksSignature: product?.formbricksSignature,
|
||||
surveyClosedMessage: survey?.surveyClosedMessage,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { Prisma as prismaClient } from "@prisma/client/";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -144,6 +145,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
id: targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { Prisma as prismaClient } from "@prisma/client/";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -64,6 +65,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AttributeFilter } from "@formbricks/types/surveys";
|
||||
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { Prisma as prismaClient } from "@prisma/client/";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
|
||||
@@ -87,6 +88,10 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
if (body.type === "link") {
|
||||
delete body.triggers;
|
||||
delete body.recontactDays;
|
||||
// converts JSON field with null value to JsonNull as JSON fields can't be set to null since prisma 3.0
|
||||
if (!body.surveyClosedMessage) {
|
||||
body.surveyClosedMessage = prismaClient.JsonNull;
|
||||
}
|
||||
}
|
||||
|
||||
if (body.triggers) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TEventClassNoCodeConfig } from "@formbricks/types/v1/eventClasses";
|
||||
import { TResponsePersonAttributes, TResponseData } from "@formbricks/types/v1/responses";
|
||||
import { TSurveyQuestions, TSurveyThankYouCard } from "@formbricks/types/v1/surveys";
|
||||
import { TSurveyClosedMessage, TSurveyQuestions, TSurveyThankYouCard } from "@formbricks/types/v1/surveys";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/v1/users";
|
||||
|
||||
declare global {
|
||||
@@ -12,6 +12,7 @@ declare global {
|
||||
export type ResponsePersonAttributes = TResponsePersonAttributes;
|
||||
export type SurveyQuestions = TSurveyQuestions;
|
||||
export type SurveyThankYouCard = TSurveyThankYouCard;
|
||||
export type SurveyClosedMessage = TSurveyClosedMessage;
|
||||
export type UserNotificationSettings = TUserNotificationSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Survey" ADD COLUMN "surveyClosedMessage" JSONB;
|
||||
@@ -210,33 +210,36 @@ enum displayOptions {
|
||||
}
|
||||
|
||||
model Survey {
|
||||
id String @id @default(cuid())
|
||||
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
|
||||
status SurveyStatus @default(draft)
|
||||
id String @id @default(cuid())
|
||||
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
|
||||
status SurveyStatus @default(draft)
|
||||
/// @zod.custom(imports.ZSurveyQuestions)
|
||||
/// @zod.custom(imports.ZSurveyQuestions)
|
||||
/// [SurveyQuestions]
|
||||
questions Json @default("[]")
|
||||
questions Json @default("[]")
|
||||
/// @zod.custom(imports.ZSurveyThankYouCard)
|
||||
/// @zod.custom(imports.ZSurveyThankYouCard)
|
||||
/// [SurveyThankYouCard]
|
||||
thankYouCard Json @default("{\"enabled\": false}")
|
||||
responses Response[]
|
||||
displayOption displayOptions @default(displayOnce)
|
||||
recontactDays Int?
|
||||
triggers SurveyTrigger[]
|
||||
attributeFilters SurveyAttributeFilter[]
|
||||
displays Display[]
|
||||
autoClose Int?
|
||||
delay Int @default(0)
|
||||
autoComplete Int?
|
||||
closeOnDate DateTime?
|
||||
thankYouCard Json @default("{\"enabled\": false}")
|
||||
responses Response[]
|
||||
displayOption displayOptions @default(displayOnce)
|
||||
recontactDays Int?
|
||||
triggers SurveyTrigger[]
|
||||
attributeFilters SurveyAttributeFilter[]
|
||||
displays Display[]
|
||||
autoClose Int?
|
||||
delay Int @default(0)
|
||||
autoComplete Int?
|
||||
closeOnDate DateTime?
|
||||
/// @zod.custom(imports.ZSurveyClosedMessage)
|
||||
/// [SurveyClosedMessage]
|
||||
surveyClosedMessage Json?
|
||||
}
|
||||
|
||||
model Event {
|
||||
|
||||
@@ -6,6 +6,6 @@ export { ZEventClassNoCodeConfig } from "@formbricks/types/v1/eventClasses";
|
||||
export { ZResponseData, ZResponsePersonAttributes } from "@formbricks/types/v1/responses";
|
||||
export const ZResponseMeta = z.record(z.union([z.string(), z.number()]));
|
||||
|
||||
export { ZSurveyQuestions, ZSurveyThankYouCard } from "@formbricks/types/v1/surveys";
|
||||
export { ZSurveyQuestions, ZSurveyThankYouCard, ZSurveyClosedMessage } from "@formbricks/types/v1/surveys";
|
||||
|
||||
export { ZUserNotificationSettings } from "@formbricks/types/v1/users";
|
||||
|
||||
@@ -6,6 +6,11 @@ export interface ThankYouCard {
|
||||
subheader?: string;
|
||||
}
|
||||
|
||||
export interface SurveyClosedMessage {
|
||||
heading?: string;
|
||||
subheading?: string;
|
||||
}
|
||||
|
||||
export interface Survey {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
@@ -26,6 +31,7 @@ export interface Survey {
|
||||
autoClose: number | null;
|
||||
delay: number;
|
||||
autoComplete: number | null;
|
||||
surveyClosedMessage: SurveyClosedMessage | null;
|
||||
closeOnDate: Date | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,17 @@ export const ZSurveyThankYouCard = z.object({
|
||||
subheader: z.optional(z.string()),
|
||||
});
|
||||
|
||||
export const ZSurveyClosedMessage = z
|
||||
.object({
|
||||
heading: z.optional(z.string()),
|
||||
subheading: z.optional(z.string()),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export type TSurveyThankYouCard = z.infer<typeof ZSurveyThankYouCard>;
|
||||
|
||||
export type TSurveyClosedMessage = z.infer<typeof ZSurveyThankYouCard>;
|
||||
|
||||
export const ZSurveyChoice = z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
@@ -215,6 +224,7 @@ export const ZSurveyWithAnalytics = ZSurvey.extend({
|
||||
numDisplays: z.number(),
|
||||
responseRate: z.number(),
|
||||
}),
|
||||
surveyClosedMessage: ZSurveyClosedMessage,
|
||||
});
|
||||
|
||||
export type TSurveyWithAnalytics = z.infer<typeof ZSurveyWithAnalytics>;
|
||||
|
||||
Reference in New Issue
Block a user