Compare commits

...

2 Commits

Author SHA1 Message Date
Matti Nannt
acc6674ec5 chore: update surveys package to 1.0.1 (#1763) 2023-12-08 11:41:44 +00:00
Anshuman Pandey
dd0d296c6a feat: question-date (#1660)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-12-08 11:22:19 +00:00
27 changed files with 1487 additions and 319 deletions

View File

@@ -0,0 +1,95 @@
import React, { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
import { timeSince } from "@formbricks/lib/time";
import type { TSurveyDateQuestion, TSurveyQuestionSummary } from "@formbricks/types/surveys";
import { PersonAvatar } from "@formbricks/ui/Avatars";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
import { questionTypes } from "@/app/lib/questions";
import { formatDateWithOrdinal } from "@formbricks/lib/utils/datetime";
interface DateQuestionSummary {
questionSummary: TSurveyQuestionSummary<TSurveyDateQuestion>;
environmentId: string;
responsesPerPage: number;
}
export default function DateQuestionSummary({
questionSummary,
environmentId,
responsesPerPage,
}: DateQuestionSummary) {
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
const [displayCount, setDisplayCount] = useState(responsesPerPage);
return (
<div className="rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
<Headline headline={questionSummary.question.headline} required={questionSummary.question.required} />
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
{questionTypeInfo && <questionTypeInfo.icon className="mr-2 h-4 w-4 " />}
{questionTypeInfo ? questionTypeInfo.label : "Unknown Question Type"} Question
</div>
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
<InboxStackIcon className="mr-2 h-4 w-4" />
{questionSummary.responses.length} Responses
</div>
</div>
</div>
<div className="rounded-b-lg bg-white ">
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
<div className="pl-4 md:pl-6">User</div>
<div className="col-span-2 pl-4 md:pl-6">Response</div>
<div className="px-4 md:px-6">Time</div>
</div>
{questionSummary.responses.slice(0, displayCount).map((response) => {
const displayIdentifier = getPersonIdentifier(response.person!);
return (
<div
key={response.id}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
<div className="pl-4 md:pl-6">
{response.person ? (
<Link
className="ph-no-capture group flex items-center"
href={`/environments/${environmentId}/people/${response.person.id}`}>
<div className="hidden md:flex">
<PersonAvatar personId={response.person.id} />
</div>
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{displayIdentifier}
</p>
</Link>
) : (
<div className="group flex items-center">
<div className="hidden md:flex">
<PersonAvatar personId="anonymous" />
</div>
<p className="break-all text-slate-600 md:ml-2">Anonymous</p>
</div>
)}
</div>
<div className="ph-no-capture col-span-2 whitespace-pre-wrap pl-6 font-semibold">
{formatDateWithOrdinal(new Date(response.value as string))}
</div>
<div className="px-4 text-slate-500 md:px-6">{timeSince(response.updatedAt.toISOString())}</div>
</div>
);
})}
<div className="my-1 flex justify-center">
{displayCount < questionSummary.responses.length && (
<button
onClick={() => setDisplayCount((prevCount) => prevCount + responsesPerPage)}
className="my-2 flex h-8 items-center justify-center rounded-lg border border-gray-300 bg-white px-3 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-700">
Show more
</button>
)}
</div>
</div>
</div>
);
}

View File

@@ -4,6 +4,7 @@ import HiddenFieldsSummary from "@/app/(app)/environments/[environmentId]/survey
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import type {
TSurveyDateQuestion,
TSurveyFileUploadQuestion,
TSurveyPictureSelectionQuestion,
TSurveyQuestionSummary,
@@ -26,6 +27,7 @@ import MultipleChoiceSummary from "./MultipleChoiceSummary";
import NPSSummary from "./NPSSummary";
import OpenTextSummary from "./OpenTextSummary";
import RatingSummary from "./RatingSummary";
import DateQuestionSummary from "./DateQuestionSummary";
import FileUploadSummary from "./FileUploadSummary";
import PictureChoiceSummary from "./PictureChoiceSummary";
@@ -47,6 +49,7 @@ export default function SummaryList({ environment, survey, responses, responsesP
updatedAt: r.updatedAt,
person: r.person,
}));
return {
question,
responses: questionResponses,
@@ -54,115 +57,123 @@ export default function SummaryList({ environment, survey, responses, responsesP
});
return (
<>
<div className="mt-10 space-y-8">
{survey.type === "web" && responses.length === 0 && !environment.widgetSetupCompleted ? (
<EmptyInAppSurveys environment={environment} />
) : responses.length === 0 ? (
<EmptySpaceFiller
type="response"
environment={environment}
noWidgetRequired={survey.type === "link"}
/>
) : (
<>
{getSummaryData().map((questionSummary) => {
if (questionSummary.question.type === TSurveyQuestionType.OpenText) {
return (
<OpenTextSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyOpenTextQuestion>}
environmentId={environment.id}
responsesPerPage={responsesPerPage}
/>
);
}
if (
questionSummary.question.type === TSurveyQuestionType.MultipleChoiceSingle ||
questionSummary.question.type === TSurveyQuestionType.MultipleChoiceMulti
) {
return (
<MultipleChoiceSummary
key={questionSummary.question.id}
questionSummary={
questionSummary as TSurveyQuestionSummary<
TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceSingleQuestion
>
}
environmentId={environment.id}
surveyType={survey.type}
responsesPerPage={responsesPerPage}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.NPS) {
return (
<NPSSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyNPSQuestion>}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.CTA) {
return (
<CTASummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyCTAQuestion>}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.Rating) {
return (
<RatingSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyRatingQuestion>}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.Consent) {
return (
<ConsentSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyConsentQuestion>}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.FileUpload) {
return (
<FileUploadSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyFileUploadQuestion>}
environmentId={environment.id}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.PictureSelection) {
return (
<PictureChoiceSummary
key={questionSummary.question.id}
questionSummary={
questionSummary as TSurveyQuestionSummary<TSurveyPictureSelectionQuestion>
}
/>
);
}
return null;
<div className="mt-10 space-y-8">
{survey.type === "web" && responses.length === 0 && !environment.widgetSetupCompleted ? (
<EmptyInAppSurveys environment={environment} />
) : responses.length === 0 ? (
<EmptySpaceFiller
type="response"
environment={environment}
noWidgetRequired={survey.type === "link"}
/>
) : (
<>
{getSummaryData().map((questionSummary) => {
if (questionSummary.question.type === TSurveyQuestionType.OpenText) {
return (
<OpenTextSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyOpenTextQuestion>}
environmentId={environment.id}
responsesPerPage={responsesPerPage}
/>
);
}
if (
questionSummary.question.type === TSurveyQuestionType.MultipleChoiceSingle ||
questionSummary.question.type === TSurveyQuestionType.MultipleChoiceMulti
) {
return (
<MultipleChoiceSummary
key={questionSummary.question.id}
questionSummary={
questionSummary as TSurveyQuestionSummary<
TSurveyMultipleChoiceMultiQuestion | TSurveyMultipleChoiceSingleQuestion
>
}
environmentId={environment.id}
surveyType={survey.type}
responsesPerPage={responsesPerPage}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.NPS) {
return (
<NPSSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyNPSQuestion>}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.CTA) {
return (
<CTASummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyCTAQuestion>}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.Rating) {
return (
<RatingSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyRatingQuestion>}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.Consent) {
return (
<ConsentSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyConsentQuestion>}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.PictureSelection) {
return (
<PictureChoiceSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyPictureSelectionQuestion>}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.Date) {
return (
<DateQuestionSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyDateQuestion>}
environmentId={environment.id}
responsesPerPage={responsesPerPage}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.FileUpload) {
return (
<FileUploadSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyFileUploadQuestion>}
environmentId={environment.id}
/>
);
}
return null;
})}
{survey.hiddenFields?.enabled &&
survey.hiddenFields.fieldIds?.map((question) => {
return (
<HiddenFieldsSummary
environment={environment}
question={question}
responses={responses}
survey={survey}
key={question}
/>
);
})}
{survey.hiddenFields?.enabled &&
survey.hiddenFields.fieldIds?.map((question) => {
return (
<HiddenFieldsSummary
environment={environment}
question={question}
responses={responses}
survey={survey}
key={question}
/>
);
})}
</>
)}
</div>
</>
</>
)}
</div>
);
}

View File

@@ -17,6 +17,7 @@ import {
Text,
} from "@react-email/components";
import { render } from "@react-email/render";
import { CalendarDaysIcon } from "lucide-react";
interface EmailTemplateProps {
survey: TSurvey;
@@ -283,6 +284,22 @@ const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) =>
<EmailFooter />
</EmailTemplateWrapper>
);
case TSurveyQuestionType.Date:
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 flex h-12 w-full items-center justify-center rounded-lg border border-solid border-gray-200 bg-white">
<CalendarDaysIcon className="mb-1 inline h-4 w-4" />
<Text className="inline text-sm font-medium">Select a date</Text>
</Section>
<EmailFooter />
</EmailTemplateWrapper>
);
}
};

View File

@@ -0,0 +1,94 @@
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Label } from "@formbricks/ui/Label";
import { Input } from "@formbricks/ui/Input";
import { PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
import { useState } from "react";
import QuestionFormInput from "./QuestionFormInput";
import { OptionsSwitcher } from "@formbricks/ui/QuestionTypeSelector";
interface IDateQuestionFormProps {
localSurvey: TSurvey;
question: TSurveyDateQuestion;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
lastQuestion: boolean;
isInValid: boolean;
}
const dateOptions = [
{
value: "M-d-y",
label: "MM-DD-YYYY",
},
{
value: "d-M-y",
label: "DD-MM-YYYY",
},
{
value: "y-M-d",
label: "YYYY-MM-DD",
},
];
export default function DateQuestionForm({
question,
questionIdx,
updateQuestion,
isInValid,
localSurvey,
}: IDateQuestionFormProps): JSX.Element {
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
return (
<form>
<QuestionFormInput
environmentId={localSurvey.environmentId}
isInValid={isInValid}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
/>
<div className="mt-3">
{showSubheader && (
<>
<Label htmlFor="subheader">Description</Label>
<div className="mt-2 inline-flex w-full items-center">
<Input
id="subheader"
name="subheader"
value={question.subheader}
onChange={(e) => updateQuestion(questionIdx, { subheader: e.target.value })}
/>
<TrashIcon
className="ml-2 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => {
setShowSubheader(false);
updateQuestion(questionIdx, { subheader: "" });
}}
/>
</div>
</>
)}
{!showSubheader && (
<Button size="sm" variant="minimal" type="button" onClick={() => setShowSubheader(true)}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
</Button>
)}
</div>
<div className="mt-3">
<Label htmlFor="questionType">Date Format</Label>
<div className="mt-2 flex items-center">
<OptionsSwitcher
options={dateOptions}
currentOption={question.format}
handleTypeChange={(value) => updateQuestion(questionIdx, { format: value })}
/>
</div>
</div>
</form>
);
}

View File

@@ -9,16 +9,23 @@ import {
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { QuestionTypeSelector } from "@formbricks/ui/QuestionTypeSelector";
import { OptionsSwitcher } from "@formbricks/ui/QuestionTypeSelector";
import { PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
import { useState } from "react";
import {
ChatBubbleBottomCenterTextIcon,
EnvelopeIcon,
HashtagIcon,
LinkIcon,
PhoneIcon,
} from "@heroicons/react/24/solid";
const questionTypes = [
{ value: "text", label: "Text" },
{ value: "email", label: "Email" },
{ value: "url", label: "URL" },
{ value: "number", label: "Number" },
{ value: "phone", label: "Phone" },
{ value: "text", label: "Text", icon: <ChatBubbleBottomCenterTextIcon /> },
{ value: "email", label: "Email", icon: <EnvelopeIcon /> },
{ value: "url", label: "URL", icon: <LinkIcon /> },
{ value: "number", label: "Number", icon: <HashtagIcon /> },
{ value: "phone", label: "Phone", icon: <PhoneIcon /> },
];
interface OpenQuestionFormProps {
@@ -106,9 +113,9 @@ export default function OpenQuestionForm({
<div className="mt-3">
<Label htmlFor="questionType">Input Type</Label>
<div className="mt-2 flex items-center">
<QuestionTypeSelector
questionTypes={questionTypes}
currentType={question.inputType}
<OptionsSwitcher
options={questionTypes}
currentOption={question.inputType}
handleTypeChange={handleInputChange} // Use the merged function
/>
</div>

View File

@@ -20,6 +20,7 @@ import {
StarIcon,
ArrowUpTrayIcon,
PhotoIcon,
CalendarDaysIcon,
} from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
@@ -33,6 +34,7 @@ import NPSQuestionForm from "./NPSQuestionForm";
import OpenQuestionForm from "./OpenQuestionForm";
import QuestionDropdown from "./QuestionMenu";
import RatingQuestionForm from "./RatingQuestionForm";
import DateQuestionForm from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/DateQuestionForm";
import PictureSelectionForm from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/PictureSelectionForm";
import { TProduct } from "@formbricks/types/product";
@@ -146,6 +148,8 @@ export default function QuestionCard({
<CheckIcon />
) : question.type === TSurveyQuestionType.PictureSelection ? (
<PhotoIcon />
) : question.type === TSurveyQuestionType.Date ? (
<CalendarDaysIcon />
) : null}
</div>
<div>
@@ -234,6 +238,15 @@ export default function QuestionCard({
updateQuestion={updateQuestion}
isInValid={isInValid}
/>
) : question.type === TSurveyQuestionType.Date ? (
<DateQuestionForm
localSurvey={localSurvey}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
isInValid={isInValid}
/>
) : question.type === TSurveyQuestionType.PictureSelection ? (
<PictureSelectionForm
localSurvey={localSurvey}

View File

@@ -2,7 +2,7 @@ export const revalidate = REVALIDATION_INTERVAL;
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import { REVALIDATION_INTERVAL, colours } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
@@ -12,7 +12,6 @@ import { getSurvey } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { getServerSession } from "next-auth";
import { colours } from "@formbricks/lib/constants";
import SurveyEditor from "./components/SurveyEditor";
export const generateMetadata = async ({ params }) => {
@@ -59,17 +58,15 @@ export default async function SurveysEditPage({ params }) {
}
return (
<>
<SurveyEditor
survey={survey}
product={product}
environment={environment}
actionClasses={actionClasses}
attributeClasses={attributeClasses}
responseCount={responseCount}
membershipRole={currentUserMembership?.role}
colours={colours}
/>
</>
<SurveyEditor
survey={survey}
product={product}
environment={environment}
actionClasses={actionClasses}
attributeClasses={attributeClasses}
responseCount={responseCount}
membershipRole={currentUserMembership?.role}
colours={colours}
/>
);
}

View File

@@ -20,11 +20,14 @@ export default function Modal({
const modalRef = useRef<HTMLDivElement | null>(null);
const highlightBorderColorStyle = useMemo(() => {
if (!highlightBorderColor) return {};
if (!highlightBorderColor)
return {
overflow: "visible",
};
return {
border: `2px solid ${highlightBorderColor}`,
overflow: "hidden",
overflow: "visible",
};
}, [highlightBorderColor]);
@@ -51,7 +54,7 @@ export default function Modal({
: "";
return (
<div aria-live="assertive" className="relative h-full w-full overflow-hidden">
<div aria-live="assertive" className="relative h-full w-full overflow-visible">
<div
ref={modalRef}
style={highlightBorderColorStyle}

View File

@@ -5,7 +5,7 @@ import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
if (process.env.NODE_ENV === "development") {
console.log(error.message);
console.error(error.message);
}
return (

View File

@@ -2,13 +2,14 @@ import { TSurveyQuestionType as QuestionId } from "@formbricks/types/surveys";
import {
ArrowUpTrayIcon,
ChatBubbleBottomCenterTextIcon,
CheckIcon,
CursorArrowRippleIcon,
ListBulletIcon,
PhotoIcon,
PresentationChartBarIcon,
QueueListIcon,
StarIcon,
CheckIcon,
CalendarDaysIcon,
} from "@heroicons/react/24/solid";
import { createId } from "@paralleldrive/cuid2";
import { replaceQuestionPresetPlaceholders } from "./templates";
@@ -133,6 +134,16 @@ export const questionTypes: TSurveyQuestionType[] = [
dismissButtonLabel: "Skip",
},
},
{
id: QuestionId.Date,
label: "Date",
description: "Ask your users to select a date",
icon: CalendarDaysIcon,
preset: {
headline: "When is your birthday?",
format: "M-d-y",
},
},
{
id: QuestionId.FileUpload,
label: "File Upload",

View File

@@ -3,6 +3,11 @@
"license": "MIT",
"version": "1.2.7",
"description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.",
"homepage": "https://formbricks.com",
"repository": {
"type": "git",
"url": "https://github.com/formbricks/formbricks"
},
"keywords": [
"Formbricks",
"surveys",

View File

@@ -2,12 +2,13 @@
import "server-only";
import { prisma } from "@formbricks/database";
import { SERVICES_REVALIDATION_INTERVAL, ITEMS_PER_PAGE } from "../constants";
import { TActionClass, TActionClassInput, ZActionClassInput } from "@formbricks/types/actionClasses";
import { ZId } from "@formbricks/types/environment";
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
import { ZId } from "@formbricks/types/environment";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import { Prisma } from "@prisma/client";
import { unstable_cache } from "next/cache";
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
import { validateInputs } from "../utils/validate";
import { actionClassCache } from "./cache";
@@ -198,6 +199,9 @@ export const updateActionClass = async (
return result;
} catch (error) {
throw new DatabaseError(`Database error when updating an action for environment ${environmentId}`);
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
};

View File

@@ -5,3 +5,34 @@ export const diffInDays = (date1: Date, date2: Date) => {
const diffTime = Math.abs(date2.getTime() - date1.getTime());
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
};
export const formatDateWithOrdinal = (date: Date): string => {
const getOrdinalSuffix = (day: number) => {
const suffixes = ["th", "st", "nd", "rd"];
const relevantDigits = day < 30 ? day % 20 : day % 30;
return suffixes[relevantDigits <= 3 ? relevantDigits : 0];
};
const dayOfWeekNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
const dayOfWeek = dayOfWeekNames[date.getDay()];
const day = date.getDate();
const monthIndex = date.getMonth();
const year = date.getFullYear();
return `${dayOfWeek}, ${monthNames[monthIndex]} ${day}${getOrdinalSuffix(day)}, ${year}`;
};

View File

@@ -1,13 +1,21 @@
{
"name": "@formbricks/surveys",
"private": true,
"license": "MIT",
"version": "0.0.0",
"version": "1.0.1",
"description": "Formbricks-surveys is a helper library to embed surveys into your application",
"homepage": "https://formbricks.com",
"repository": {
"type": "git",
"url": "https://github.com/formbricks/formbricks"
},
"sideEffects": false,
"source": "./src/index.ts",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"exports": {
".": {
"import": "./dist/index.mjs",
@@ -15,9 +23,11 @@
}
},
"scripts": {
"dev": "vite build --watch",
"build": "tsc && vite build",
"go": "vite build --watch",
"dev": "SURVEYS_PACKAGE_MODE=development vite build --watch",
"build": "pnpm run build:surveys && pnpm run build:question-date",
"build:surveys": "tsc && SURVEYS_PACKAGE_BUILD=surveys vite build",
"build:question-date": "tsc && SURVEYS_PACKAGE_BUILD=question-date vite build",
"go": "concurrently \"pnpm dev\" \"serve dist -p 3003\"",
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
"preview": "vite preview",
"clean": "rimraf .turbo node_modules dist"
@@ -28,14 +38,17 @@
"@formbricks/types": "workspace:*",
"@preact/preset-vite": "^2.7.0",
"autoprefixer": "^10.4.16",
"eslint-config-prettier": "^9.1.0",
"eslint-config-turbo": "latest",
"postcss": "^8.4.32",
"preact": "^10.19.2",
"react-date-picker": "^10.5.2",
"tailwindcss": "^3.3.6",
"terser": "^5.25.0",
"vite": "^5.0.6",
"vite-plugin-dts": "^3.6.4",
"vite-tsconfig-paths": "^4.2.1",
"eslint-config-prettier": "^9.1.0",
"eslint-config-turbo": "latest"
"serve": "14.2.1",
"concurrently": "8.2.2"
}
}

View File

@@ -7,6 +7,7 @@ import NPSQuestion from "@/components/questions/NPSQuestion";
import OpenTextQuestion from "@/components/questions/OpenTextQuestion";
import PictureSelectionQuestion from "@/components/questions/PictureSelectionQuestion";
import RatingQuestion from "@/components/questions/RatingQuestion";
import DateQuestion from "@/components/questions/DateQuestion";
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import { TUploadFileConfig } from "@formbricks/types/storage";
import { TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
@@ -125,6 +126,18 @@ export default function QuestionConditional({
ttc={ttc}
setTtc={setTtc}
/>
) : question.type === TSurveyQuestionType.Date ? (
<DateQuestion
question={question}
value={value}
onChange={onChange}
onSubmit={onSubmit}
onBack={onBack}
isFirstQuestion={isFirstQuestion}
isLastQuestion={isLastQuestion}
ttc={ttc}
setTtc={setTtc}
/>
) : question.type === TSurveyQuestionType.PictureSelection ? (
<PictureSelectionQuestion
question={question}

View File

@@ -0,0 +1,160 @@
import { BackButton } from "@/components/buttons/BackButton";
import SubmitButton from "@/components/buttons/SubmitButton";
import Headline from "@/components/general/Headline";
import Subheader from "@/components/general/Subheader";
import { getUpdatedTtc, useTtc } from "@/lib/ttc";
import { cn } from "@/lib/utils";
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import type { TSurveyDateQuestion } from "@formbricks/types/surveys";
import { useEffect, useState } from "preact/hooks";
interface DateQuestionProps {
question: TSurveyDateQuestion;
value: string | number | string[];
onChange: (responseData: TResponseData) => void;
onSubmit: (data: TResponseData, ttc: TResponseTtc) => void;
onBack: () => void;
isFirstQuestion: boolean;
isLastQuestion: boolean;
autoFocus?: boolean;
ttc: TResponseTtc;
setTtc: (ttc: TResponseTtc) => void;
}
export default function DateQuestion({
question,
value,
onSubmit,
onBack,
isFirstQuestion,
isLastQuestion,
onChange,
setTtc,
ttc,
}: DateQuestionProps) {
const [startTime, setStartTime] = useState(performance.now());
const [errorMessage, setErrorMessage] = useState("");
const [loading, setLoading] = useState(true);
useTtc(question.id, ttc, setTtc, startTime, setStartTime);
const defaultDate = value ? new Date(value as string) : undefined;
useEffect(() => {
// Check if the DatePicker has already been loaded
if (!window.initDatePicker) {
const script = document.createElement("script");
script.src =
process.env.SURVEYS_PACKAGE_MODE === "development"
? "http://localhost:3003/question-date.umd.js"
: "https://unpkg.com/@formbricks/surveys@^1.0.1/dist/question-date.umd.js";
script.async = true;
document.body.appendChild(script);
script.onload = () => {
// Initialize the DatePicker once the script is loaded
// @ts-expect-error
window.initDatePicker(document.getElementById("date-picker-root"), defaultDate, question.format);
setLoading(false);
};
return () => {
document.body.removeChild(script);
};
} else {
// If already loaded, remove the date picker and re-initialize it
setLoading(false);
const datePickerContainer = document.getElementById("datePickerContainer");
if (datePickerContainer) {
datePickerContainer.remove();
}
// @ts-ignore
window.initDatePicker(document.getElementById("date-picker-root"), defaultDate, question.format);
}
return () => {};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [question.format, question.id]);
useEffect(() => {
window.addEventListener("dateChange", (e) => {
// @ts-expect-error
const date = e.detail as Date;
// Get the timezone offset in minutes and convert it to milliseconds
const timezoneOffset = date.getTimezoneOffset() * 60000;
// Adjust the date by subtracting the timezone offset
const adjustedDate = new Date(date.getTime() - timezoneOffset);
// Format the date as YYYY-MM-DD
const dateString = adjustedDate.toISOString().split("T")[0];
onChange({ [question.id]: dateString });
});
}, [onChange, question.id]);
useEffect(() => {
if (value && errorMessage) {
setErrorMessage("");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (question.required && !value) {
// alert("Please select a date");
setErrorMessage("Please select a date.");
return;
}
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value }, updatedTtcObj);
}}
className="w-full">
<Headline headline={question.headline} questionId={question.id} required={question.required} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className={"text-red-600"}>
<span>{errorMessage}</span>
</div>
<div className={cn("my-4", errorMessage && "rounded-lg border-2 border-red-500")} id="date-picker-root">
{loading && (
<div className="relative flex h-12 w-full cursor-pointer appearance-none items-center justify-center rounded-lg border border-slate-300 bg-white text-left text-base font-normal text-slate-900 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1">
<span
className="h-6 w-6 animate-spin rounded-full border-b-2 border-neutral-900"
style={{ borderTopColor: "transparent" }}></span>
</div>
)}
</div>
<div className="mt-4 flex w-full justify-between">
<div>
{!isFirstQuestion && (
<BackButton
backButtonLabel={question.backButtonLabel}
onClick={() => {
const updatedTtcObj = getUpdatedTtc(ttc, question.id, performance.now() - startTime);
setTtc(updatedTtcObj);
onBack();
}}
/>
)}
</div>
<SubmitButton isLastQuestion={isLastQuestion} onClick={() => {}} buttonLabel={question.buttonLabel} />
</div>
</form>
);
}

View File

@@ -68,12 +68,14 @@ export default function Modal({
};
const highlightBorderColorStyle = useMemo(() => {
if (!highlightBorderColor) return {};
if (!highlightBorderColor)
return {
overflow: "visible",
};
return {
borderRadius: "8px",
border: "2px solid",
overflow: "hidden",
borderColor: highlightBorderColor,
};
}, [highlightBorderColor]);
@@ -101,7 +103,7 @@ export default function Modal({
className={cn(
getPlacementStyle(placement),
show ? "opacity-100" : "opacity-0",
"border-border pointer-events-auto absolute bottom-0 h-fit w-full overflow-hidden rounded-lg border bg-white shadow-lg transition-all duration-500 ease-in-out sm:m-4 sm:max-w-sm"
"border-border pointer-events-auto absolute bottom-0 h-fit w-full overflow-visible rounded-lg border bg-white shadow-lg transition-all duration-500 ease-in-out sm:m-4 sm:max-w-sm"
)}>
{!isCenter && (
<div class="absolute right-0 top-0 block pr-2 pt-2">

View File

@@ -0,0 +1,126 @@
import { useState, useEffect, useMemo } from "preact/hooks";
import DatePicker from "react-date-picker";
const CalendarIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-5 w-5">
<path d="M12.75 12.75a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM7.5 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM8.25 17.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM9.75 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM10.5 17.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM12 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM12.75 17.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM14.25 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM15 17.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM16.5 15.75a.75.75 0 100-1.5.75.75 0 000 1.5zM15 12.75a.75.75 0 11-1.5 0 .75.75 0 011.5 0zM16.5 13.5a.75.75 0 100-1.5.75.75 0 000 1.5z" />
<path
fill-rule="evenodd"
d="M6.75 2.25A.75.75 0 017.5 3v1.5h9V3A.75.75 0 0118 3v1.5h.75a3 3 0 013 3v11.25a3 3 0 01-3 3H5.25a3 3 0 01-3-3V7.5a3 3 0 013-3H6V3a.75.75 0 01.75-.75zm13.5 9a1.5 1.5 0 00-1.5-1.5H5.25a1.5 1.5 0 00-1.5 1.5v7.5a1.5 1.5 0 001.5 1.5h13.5a1.5 1.5 0 001.5-1.5v-7.5z"
clip-rule="evenodd"
/>
</svg>
);
export default function Question({ defaultDate, format }: { defaultDate?: Date; format?: string }) {
const [datePickerOpen, setDatePickerOpen] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | undefined>(defaultDate);
const [hideInvalid, setHideInvalid] = useState(!selectedDate);
useEffect(() => {
if (datePickerOpen) {
const input = document.querySelector(".react-date-picker__inputGroup__input") as HTMLInputElement;
if (input) {
input.focus();
}
}
}, [datePickerOpen]);
useEffect(() => {
if (!!selectedDate) {
if (hideInvalid) {
setHideInvalid(false);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedDate]);
const formattedDate = useMemo(() => {
if (!selectedDate) return "";
if (format === "M-d-y") {
return `${selectedDate?.getMonth() + 1}-${selectedDate?.getDate()}-${selectedDate?.getFullYear()}`;
}
if (format === "d-M-y") {
return `${selectedDate?.getDate()}-${selectedDate?.getMonth() + 1}-${selectedDate?.getFullYear()}`;
}
return `${selectedDate?.getFullYear()}-${selectedDate?.getMonth() + 1}-${selectedDate?.getDate()}`;
}, [format, selectedDate]);
return (
<div className="relative h-12">
{!datePickerOpen && (
<div
onClick={() => setDatePickerOpen(true)}
className="relative flex h-12 w-full cursor-pointer appearance-none items-center justify-center rounded-lg border border-slate-300 bg-white text-left text-base font-normal text-slate-900 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1">
<div className="flex items-center gap-2">
<CalendarIcon />
<span>{selectedDate ? formattedDate : "Select a date"}</span>
</div>
</div>
)}
{/* @ts-expect-error */}
<DatePicker
key={datePickerOpen}
value={selectedDate}
isOpen={datePickerOpen}
onChange={(value) => {
const event = new CustomEvent("dateChange", { detail: value });
setSelectedDate(value as Date);
window.dispatchEvent(event);
}}
minDate={new Date(new Date().getFullYear() - 100, new Date().getMonth(), new Date().getDate())}
maxDate={new Date("3000-12-31")}
dayPlaceholder="DD"
monthPlaceholder="MM"
yearPlaceholder="YYYY"
format={format ?? "M-d-y"}
className={`dp-input-root rounded-lg ${!datePickerOpen ? "wrapper-hide" : ""}
${hideInvalid ? "hide-invalid" : ""}
`}
calendarClassName="calendar-root w-80 rounded-lg border border-[#e5e7eb] p-3 shadow-md"
clearIcon={null}
onCalendarOpen={() => {
setDatePickerOpen(true);
}}
onCalendarClose={() => {
// reset state
setDatePickerOpen(false);
setSelectedDate(selectedDate);
}}
// @ts-ignore
calendarIcon={<CalendarIcon />}
tileClassName={({ date }) => {
const baseClass =
"hover:bg-slate-200 rounded-md h-9 p-0 mt-1 font-normal text-slate-900 aria-selected:opacity-100";
// today's date class
if (
date.getDate() === new Date().getDate() &&
date.getMonth() === new Date().getMonth() &&
date.getFullYear() === new Date().getFullYear()
) {
return `${baseClass} bg-slate-100`;
}
// active date class
if (
date.getDate() === selectedDate?.getDate() &&
date.getMonth() === selectedDate?.getMonth() &&
date.getFullYear() === selectedDate?.getFullYear()
) {
return `${baseClass} !bg-slate-900 !text-slate-100`;
}
return baseClass;
}}
formatShortWeekday={(_, date) => {
return date.toLocaleDateString("en-US", { weekday: "short" }).slice(0, 2);
}}
showNeighboringMonth={false}
showLeadingZeros={false}
/>
</div>
);
}

View File

@@ -0,0 +1,31 @@
import { render } from "preact";
import Question from "./Question.tsx";
import globalCss from "./styles/globals.css?inline";
import calendarCss from "react-calendar/dist/Calendar.css?inline";
import datePickerCss from "react-date-picker/dist/DatePicker.css?inline";
declare global {
interface Window {
initDatePicker: (element: HTMLElement, selectedDate?: Date) => void;
selectedDate: Date;
}
}
const addStylesToDom = () => {
if (document.getElementById("formbricks__question_date_css") === null) {
const styleElement = document.createElement("style");
styleElement.id = "formbricks__question_date_css";
styleElement.innerHTML = globalCss + datePickerCss + calendarCss;
document.head.appendChild(styleElement);
}
};
const init = (element: HTMLElement, selectedDate?: Date, format?: string) => {
addStylesToDom();
const container = document.createElement("div");
container.id = "datePickerContainer";
element.appendChild(container);
render(<Question defaultDate={selectedDate} format={format} />, container);
};
window.initDatePicker = init;

View File

@@ -0,0 +1,75 @@
.dp-input-root {
width: 100%;
}
.dp-input-root [class$="wrapper"] {
height: 48px;
display: flex;
flex-direction: row-reverse;
gap: 8px;
justify-content: center;
align-items: center;
border-radius: 8px;
border: 1px solid rgb(203 213 225) !important;
padding-left: 24px;
padding-right: 24px;
padding-top: 8px;
padding-bottom: 8px;
margin-bottom: 8px;
}
.dp-input-root [class$="inputGroup"] {
flex: none;
font-size: 16px;
}
.wrapper-hide .react-date-picker__inputGroup {
display: none;
}
.dp-input-root .react-date-picker__inputGroup__input {
background: #f1f5f9 !important;
border-radius: 2px;
}
.dp-input-root .react-date-picker__inputGroup__input:invalid {
background: #fecaca !important;
border-radius: 2px;
}
.hide-invalid .react-date-picker__inputGroup__input:invalid {
background: #f1f5f9 !important;
}
.wrapper-hide .react-date-picker__wrapper {
display: none;
}
.calendar-root [class$="navigation"] {
height: 36px;
margin-bottom: 0px;
}
.calendar-root [class$="navigation"] button {
border-radius: 8px;
}
.calendar-root [class$="navigation"] button:hover {
background: rgb(226 232 240) !important;
}
.calendar-root [class$="navigation"] [class$="navigation__label"] {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
}
.react-calendar__month-view__weekdays__weekday {
color: rgb(100, 116, 139);
font-weight: 400;
text-transform: capitalize;
}
.react-calendar__month-view__weekdays__weekday > abbr {
text-decoration: none;
}

View File

@@ -4,7 +4,7 @@
"compilerOptions": {
"allowImportingTsExtensions": true,
"isolatedModules": true,
"noEmit": true,
"emitDeclarationOnly": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",
"baseUrl": ".",

View File

@@ -1,23 +1,35 @@
import { resolve } from "path";
import { defineConfig } from "vite";
import { defineConfig, loadEnv } from "vite";
import preact from "@preact/preset-vite";
import dts from "vite-plugin-dts";
import tsconfigPaths from "vite-tsconfig-paths";
// https://vitejs.dev/config/
export default defineConfig({
build: {
emptyOutDir: false, // keep the dist folder to avoid errors with pnpm go when folder is empty during build
minify: "terser",
sourcemap: true,
lib: {
// Could also be a dictionary or array of multiple entry points
entry: resolve(__dirname, "src/index.ts"),
name: "formbricks-surveys",
formats: ["cjs", "es", "umd"],
// the proper extensions will be added
fileName: "index",
const buildPackage = process.env.SURVEYS_PACKAGE_BUILD || "surveys";
const entryPoint = buildPackage === "surveys" ? "src/index.ts" : "src/sideload/question-date/index.tsx";
const name = buildPackage === "surveys" ? "formbricks-surveys" : "formbricks-question-date";
const fileName = buildPackage === "surveys" ? "index" : "question-date";
const config = ({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
return defineConfig({
define: {
"process.env": env,
},
},
plugins: [preact(), dts({ rollupTypes: true }), tsconfigPaths()],
});
build: {
emptyOutDir: false,
minify: "terser",
sourcemap: true,
lib: {
entry: resolve(__dirname, entryPoint),
name,
formats: ["cjs", "es", "umd"],
fileName,
},
},
plugins: [preact(), dts({ rollupTypes: true }), tsconfigPaths()],
});
};
export default config;

View File

@@ -18,6 +18,7 @@ export enum TSurveyQuestionType {
Rating = "rating",
Consent = "consent",
PictureSelection = "pictureSelection",
Date = "date",
}
export const ZSurveyWelcomeCard = z.object({
@@ -326,6 +327,14 @@ export const ZSurveyRatingQuestion = ZSurveyQuestionBase.extend({
logic: z.array(ZSurveyRatingLogic).optional(),
});
export const ZSurveyDateQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.Date),
html: z.string().optional(),
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
});
export type TSurveyDateQuestion = z.infer<typeof ZSurveyDateQuestion>;
export type TSurveyRatingQuestion = z.infer<typeof ZSurveyRatingQuestion>;
export const ZSurveyPictureSelectionQuestion = ZSurveyQuestionBase.extend({
@@ -346,6 +355,7 @@ export const ZSurveyQuestion = z.union([
ZSurveyCTAQuestion,
ZSurveyRatingQuestion,
ZSurveyPictureSelectionQuestion,
ZSurveyDateQuestion,
ZSurveyFileUploadQuestion,
]);
@@ -443,6 +453,7 @@ export const ZSurveyTSurveyQuestionType = z.union([
z.literal("rating"),
z.literal("consent"),
z.literal("pictureSelection"),
z.literal("date"),
]);
export type TSurveyTSurveyQuestionType = z.infer<typeof ZSurveyTSurveyQuestionType>;

View File

@@ -1,36 +1,22 @@
import {
ChatBubbleBottomCenterTextIcon,
EnvelopeIcon,
HashtagIcon,
LinkIcon,
PhoneIcon,
} from "@heroicons/react/24/solid";
import React from "react";
interface TSurveyQuestionType {
interface TOption {
value: string;
label: string;
icon?: React.ReactNode;
}
interface QuestionTypeSelectorProps {
questionTypes: TSurveyQuestionType[];
currentType: string | undefined;
options: TOption[];
currentOption: string | undefined;
handleTypeChange: (value: string) => void;
}
const typeIcons: { [key: string]: React.ReactNode } = {
text: <ChatBubbleBottomCenterTextIcon />,
email: <EnvelopeIcon />,
url: <LinkIcon />,
number: <HashtagIcon />,
phone: <PhoneIcon />,
};
export function QuestionTypeSelector({
questionTypes,
currentType,
export function OptionsSwitcher({
options: questionTypes,
currentOption,
handleTypeChange,
}: QuestionTypeSelectorProps): JSX.Element {
}: QuestionTypeSelectorProps) {
return (
<div className="flex w-full items-center justify-between rounded-md border p-1">
{questionTypes.map((type) => (
@@ -38,13 +24,15 @@ export function QuestionTypeSelector({
key={type.value}
onClick={() => handleTypeChange(type.value)}
className={`flex-grow cursor-pointer rounded-md bg-${
(currentType === undefined && type.value === "text") || currentType === type.value
(currentOption === undefined && type.value === "text") || currentOption === type.value
? "slate-100"
: "white"
} p-2 text-center`}>
<div className="flex items-center justify-center space-x-2">
<span className="text-sm text-slate-900">{type.label}</span>
<div className="h-4 w-4 text-slate-600 hover:text-slate-800">{typeIcons[type.value]}</div>
{type.icon ? (
<div className="h-4 w-4 text-slate-600 hover:text-slate-800">{type.icon}</div>
) : null}
</div>
</div>
))}

View File

@@ -29,6 +29,7 @@ import { FileUploadResponse } from "../FileUploadResponse";
import { useMembershipRole } from "@formbricks/lib/membership/hooks/useMembershipRole";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { LoadingWrapper } from "../LoadingWrapper";
import { formatDateWithOrdinal } from "@formbricks/lib/utils/datetime";
export interface SingleResponseCardProps {
survey: TSurvey;
@@ -61,6 +62,13 @@ function TooltipRenderer(props: TooltipRendererProps) {
return <>{children}</>;
}
function DateResponse({ date }: { date?: string }) {
if (!date) return null;
const formattedDateString = formatDateWithOrdinal(new Date(date));
return <p className="ph-no-capture my-1 font-semibold text-slate-700">{formattedDateString}</p>;
}
export default function SingleResponseCard({
survey,
response,
@@ -321,6 +329,8 @@ export default function SingleResponseCard({
range={question.range}
/>
</div>
) : question.type === TSurveyQuestionType.Date ? (
<DateResponse date={response.data[question.id] as string} />
) : (
<p className="ph-no-capture my-1 font-semibold text-slate-700">
{response.data[question.id]}

621
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -46,14 +46,20 @@
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"],
"env": [
"AIRTABLE_CLIENT_ID",
"ASSET_PREFIX_URL",
"AWS_ACCESS_KEY",
"AWS_SECRET_KEY",
"AZUREAD_AUTH_ENABLED",
"AZUREAD_CLIENT_ID",
"AZUREAD_CLIENT_SECRET",
"AZUREAD_TENANT_ID",
"CRON_SECRET",
"ENCRYPTION_KEY",
"DEBUG",
"EMAIL_VERIFICATION_DISABLED",
"ENCRYPTION_KEY",
"ENTERPRISE_LICENSE_KEY",
"FORMBRICKS_ENCRYPTION_KEY",
"GITHUB_ID",
"GITHUB_SECRET",
"GOOGLE_CLIENT_ID",
@@ -62,65 +68,55 @@
"GOOGLE_SHEETS_CLIENT_SECRET",
"GOOGLE_SHEETS_REDIRECT_URL",
"HEROKU_APP_NAME",
"IMPRINT_URL",
"INSTANCE_ID",
"INTERNAL_SECRET",
"MAIL_FROM",
"EMAIL_VERIFICATION_DISABLED",
"FORMBRICKS_ENCRYPTION_KEY",
"INVITE_DISABLED",
"IS_FORMBRICKS_CLOUD",
"GOOGLE_AUTH_ENABLED",
"GITHUB_AUTH_ENABLED",
"IS_FORMBRICKS_CLOUD",
"PASSWORD_RESET_DISABLED",
"PRIVACY_URL",
"NEXT_PUBLIC_SENTRY_DSN",
"INVITE_DISABLED",
"SIGNUP_DISABLED",
"NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID",
"TERMS_URL",
"NEXTAUTH_SECRET",
"NEXTAUTH_URL",
"SMTP_HOST",
"SMTP_PASSWORD",
"SMTP_PORT",
"SMTP_SECURE_ENABLED",
"SMTP_USER",
"MAIL_FROM",
"NEXT_PUBLIC_DOCSEARCH_APP_ID",
"NEXT_PUBLIC_DOCSEARCH_API_KEY",
"NEXT_PUBLIC_DOCSEARCH_INDEX_NAME",
"NEXT_PUBLIC_FORMBRICKS_API_HOST",
"NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID",
"NEXT_PUBLIC_FORMBRICKS_FORM_ID",
"NEXT_PUBLIC_FORMBRICKS_FEEDBACK_FORM_ID",
"NEXT_PUBLIC_FORMBRICKS_FEEDBACK_CUSTOM_FORM_ID",
"NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID",
"NEXT_PUBLIC_FORMBRICKS_PMF_FORM_ID",
"NEXT_PUBLIC_FORMBRICKS_URL",
"IMPRINT_URL",
"NEXT_PUBLIC_SENTRY_DSN",
"SHORT_URL_BASE",
"NODE_ENV",
"NEXT_PUBLIC_POSTHOG_API_HOST",
"NEXT_PUBLIC_POSTHOG_API_KEY",
"NEXT_PUBLIC_FORMBRICKS_COM_API_HOST",
"NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID",
"NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID",
"WEBAPP_URL",
"SENTRY_DSN",
"STRAPI_API_KEY",
"STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET",
"TELEMETRY_DISABLED",
"VERCEL_URL",
"WEBAPP_URL",
"AIRTABLE_CLIENT_ID",
"AWS_ACCESS_KEY",
"AWS_SECRET_KEY",
"NEXTAUTH_SECRET",
"NEXTAUTH_URL",
"NODE_ENV",
"PASSWORD_RESET_DISABLED",
"PLAYWRIGHT_CI",
"PRIVACY_URL",
"S3_ACCESS_KEY",
"S3_SECRET_KEY",
"S3_REGION",
"S3_BUCKET_NAME",
"ENTERPRISE_LICENSE_KEY",
"PLAYWRIGHT_CI"
"SENTRY_DSN",
"SHORT_URL_BASE",
"SIGNUP_DISABLED",
"SMTP_HOST",
"SMTP_PASSWORD",
"SMTP_PORT",
"SMTP_SECURE_ENABLED",
"SMTP_USER",
"STRAPI_API_KEY",
"STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET",
"SURVEYS_PACKAGE_MODE",
"SURVEYS_PACKAGE_BUILD",
"TELEMETRY_DISABLED",
"TERMS_URL",
"VERCEL_URL",
"WEBAPP_URL"
]
},
"post-install": {