Merge branch 'main' of github.com:formbricks/formbricks into more-formbricks-in-formbricks

This commit is contained in:
Johannes
2023-08-18 15:08:19 +02:00
37 changed files with 435 additions and 138 deletions

View File

@@ -1,3 +1,6 @@
NEXT_PUBLIC_FORMBRICKS_COM_API_HOST=http://localhost:3000
NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID=
NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID=
NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID=
# Strapi API Key
STRAPI_API_KEY=

View File

@@ -1,8 +1,8 @@
import { useEffect } from "react";
import Footer from "./Footer";
import Header from "./Header";
import MetaInformation from "./MetaInformation";
import { Prose } from "./Prose";
import { useEffect } from "react";
const useExternalLinks = (selector: string) => {
useEffect(() => {

View File

@@ -4,6 +4,7 @@ interface Props {
title: string;
description: string;
publishedTime?: string;
updatedTime?: string;
authors?: string[];
section?: string;
tags?: string[];
@@ -13,6 +14,7 @@ export default function MetaInformation({
title,
description,
publishedTime,
updatedTime,
authors,
section,
tags,
@@ -31,9 +33,10 @@ export default function MetaInformation({
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="en_US" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Open Source Experience Management, Privacy-first" />
<meta property="article:publisher" content="Formbricks" />
<meta property="og:site_name" content="Formbricks Privacy-first Experience Management Solution" />
<meta property="article:publisher" content="Formbricks GmbH" />
{publishedTime && <meta property="article:published_time" content={publishedTime} />}
{updatedTime && <meta property="article:updated_time" content={updatedTime} />}
{authors && <meta property="article:author" content={authors.join(", ")} />}
{section && <meta property="article:section" content={section} />}
{tags && <meta property="article:tag" content={tags.join(", ")} />}

View File

@@ -1,14 +1,23 @@
/** @type {import('next').NextConfig} */
import rehypePrism from "@mapbox/rehype-prism";
import nextMDX from "@next/mdx";
import { withPlausibleProxy } from "next-plausible";
import remarkGfm from "remark-gfm";
import rehypePrism from "@mapbox/rehype-prism";
const nextConfig = {
reactStrictMode: true,
pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
transpilePackages: ["@formbricks/ui", "@formbricks/lib"],
images: {
remotePatterns: [
{
protocol: "https",
hostname: "seo-strapi-aws-s3.s3.eu-central-1.amazonaws.com",
port: "",
},
],
},
async redirects() {
return [
{

View File

@@ -27,12 +27,15 @@
"lottie-web": "^5.12.2",
"next": "13.4.12",
"next-plausible": "^3.10.1",
"next-seo": "^6.1.0",
"next-sitemap": "^4.1.8",
"node-fetch": "^3.3.2",
"prism-react-renderer": "^2.0.6",
"prismjs": "^1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.10.1",
"react-markdown": "^8.0.7",
"react-responsive-embed": "^2.1.0",
"remark-gfm": "^3.0.1",
"sharp": "^0.32.4"

View File

@@ -1,12 +1,12 @@
import Image from "next/image";
import LayoutMdx from "@/components/shared/LayoutMdx";
import AuthorBox from "@/components/shared/AuthorBox";
import Formbricks from "./open-source-survey-software-free-2023-formbricks-typeform-alternative.png";
import Typebot from "./typebot-open-source-free-conversational-form-builder-survey-software-opensource.jpg";
import LimeSurvey from "./free-survey-tool-limesurvey-open-source-software-opensource.png";
import OpnForm from "./opnform-free-open-source-form-survey-tools-builder-2023-self-hostign.jpg";
import HeaderImage from "./2023-title-best-open-source-survey-software-tools-and-alternatives.png";
import SurveyJS from "./surveyjs-free-opensource-form-survey-tool-software-to-make-surveys-2023.png";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "5 Open Source Survey and Form Tools maintained in 2023",

View File

@@ -0,0 +1,141 @@
import LayoutMdx from "@/components/shared/LayoutMdx";
import { FAQPageJsonLd } from "next-seo";
import Image from "next/image";
import fetch from "node-fetch";
import ReactMarkdown from "react-markdown";
type Article = {
id?: number;
attributes?: {
author?: string;
title?: string;
text?: string;
slug?: string;
createdAt?: string;
updatedAt?: string;
publishedAt?: string;
meta?: {
id?: number;
description?: string;
title?: string;
publisher?: string;
section?: string;
tags?: {
id?: number;
tag?: string;
}[];
};
faq?: {
id?: number;
question?: string;
answer?: string;
}[];
};
};
type ArticlePageProps = {
article?: Article;
};
interface ArticleResponse {
data: Article[];
meta: {
pagination: {
page: number;
pageSize: number;
pageCount: number;
total: number;
};
};
}
export async function getStaticPaths() {
const response = await fetch(
"https://strapi.formbricks.com/api/articles?populate[meta][populate]=*&filters[category][name][$eq]=learn",
{
headers: {
Authorization: `Bearer ${process.env.STRAPI_API_KEY}`,
},
}
);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const articles = (await response.json()) as ArticleResponse;
const paths = articles.data.map((article) => ({
params: { slug: article.attributes.slug },
}));
return { paths, fallback: true };
}
export async function getStaticProps({ params }) {
const res = await fetch(
`https://strapi.formbricks.com/api/articles?populate[meta][populate]=*&populate[faq][populate]=*&filters[slug][$eq]=${params.slug}`,
{
headers: {
Authorization: `Bearer ${process.env.STRAPI_API_KEY}`,
},
}
);
if (!res.ok) {
throw new Error("Something went wrong");
}
const resData = (await res.json()) as ArticleResponse;
const article = resData.data[0];
return { props: { article } };
}
export default function ArticlePage({ article = {} }: ArticlePageProps) {
if (!article || !article.attributes) return <div>Loading...</div>;
// Use next/image to render images in markdown
const renderers = {
img: (image) => {
return <Image src={image.src} alt={image.alt} width={1000} height={500} />;
},
};
const {
attributes: {
author,
publishedAt,
text,
faq,
meta: {
title,
description,
section,
tags = [], // default empty array if tags are not provided
} = {}, // default empty object if meta is not provided
} = {}, // default empty object if attributes are not provided
} = article;
const metaTags = tags.map((tag) => tag.tag);
const meta = {
title,
description,
publishedTime: publishedAt,
authors: [author],
section,
tags: metaTags,
};
// Convert the FAQ details into the desired format for FAQPageJsonLd
const faqEntities = faq.map(({ question, answer }) => ({
questionName: question,
acceptedAnswerText: answer,
}));
return (
<LayoutMdx meta={meta}>
<>
<ReactMarkdown components={renderers}>{text}</ReactMarkdown>
<FAQPageJsonLd mainEntity={faqEntities} />
</>
</LayoutMdx>
);
}

View File

@@ -42,6 +42,7 @@ export default function ActionSettingsTab({ environmentId, actionClass, setOpen
},
});
const [isUpdatingAction, setIsUpdatingAction] = useState(false);
const [isDeletingAction, setIsDeletingAction] = useState(false);
const onSubmit = async (data) => {
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as NoCodeConfig);
@@ -81,6 +82,20 @@ export default function ActionSettingsTab({ environmentId, actionClass, setOpen
if (match === "no") toast.error("Your survey would not be shown.");
};
const handleDeleteAction = async () => {
try {
setIsDeletingAction(true);
await deleteActionClass(environmentId, actionClass.id);
router.refresh();
toast.success("Action deleted successfully");
setOpen(false);
} catch (error) {
toast.error("Something went wrong. Please try again.");
} finally {
setIsDeletingAction(false);
}
};
return (
<div>
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
@@ -288,18 +303,10 @@ export default function ActionSettingsTab({ environmentId, actionClass, setOpen
<DeleteDialog
open={openDeleteDialog}
setOpen={setOpenDeleteDialog}
isDeleting={isDeletingAction}
deleteWhat={"Action"}
text="Are you sure you want to delete this action? This also removes this action as a trigger from all your surveys."
onDelete={async () => {
setOpen(false);
try {
await deleteActionClass(environmentId, actionClass.id);
router.refresh();
toast.success("Action deleted successfully");
} catch (error) {
toast.error("Something went wrong. Please try again.");
}
}}
onDelete={handleDeleteAction}
/>
</div>
);

View File

@@ -19,10 +19,19 @@ export default function HeadingSection({
const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [isDeletingPerson, setIsDeletingPerson] = useState(false);
const handleDeletePerson = async () => {
await deletePersonAction(person.id);
router.push(`/environments/${environmentId}/people`);
toast.success("Person deleted successfully.");
try {
setIsDeletingPerson(true);
await deletePersonAction(person.id);
router.push(`/environments/${environmentId}/people`);
toast.success("Person deleted successfully.");
} catch (error) {
toast.error(error.message);
} finally {
setIsDeletingPerson(false);
}
};
return (
@@ -46,6 +55,7 @@ export default function HeadingSection({
setOpen={setDeleteDialogOpen}
deleteWhat="person"
onDelete={handleDeletePerson}
isDeleting={isDeletingPerson}
/>
</>
);

View File

@@ -1,15 +1,15 @@
"use client";
import toast from "react-hot-toast";
import DeleteDialog from "@/components/shared/DeleteDialog";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { useState, Dispatch, SetStateAction } from "react";
import { useRouter } from "next/navigation";
import { useMembers } from "@/lib/members";
import { useProfile } from "@/lib/profile";
import { Button, ErrorComponent, Input } from "@formbricks/ui";
import { useTeam, deleteTeam } from "@/lib/teams/teams";
import { useMemberships } from "@/lib/memberships";
import { useProfile } from "@/lib/profile";
import { deleteTeam, useTeam } from "@/lib/teams/teams";
import { Button, ErrorComponent, Input } from "@formbricks/ui";
import { useRouter } from "next/navigation";
import { Dispatch, SetStateAction, useState } from "react";
import toast from "react-hot-toast";
export default function DeleteTeam({ environmentId }) {
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);

View File

@@ -3,6 +3,7 @@
import DeleteDialog from "@/components/shared/DeleteDialog";
import AvatarPlaceholder from "@/images/avatar-placeholder.png";
import { formbricksLogout } from "@/lib/formbricks";
import { TProfile } from "@formbricks/types/v1/profile";
import { Button, Input, ProfileAvatar } from "@formbricks/ui";
import { Session } from "next-auth";
import { signOut } from "next-auth/react";
@@ -10,7 +11,6 @@ import Image from "next/image";
import { Dispatch, SetStateAction, useState } from "react";
import toast from "react-hot-toast";
import { profileDeleteAction } from "./actions";
import { TProfile } from "@formbricks/types/v1/profile";
export function EditAvatar({ session }) {
return (

View File

@@ -451,7 +451,7 @@ export default function PreviewSurvey({
</Modal>
) : (
<div className="flex flex-grow flex-col overflow-y-auto" ref={ContentRef}>
<div className="flex w-full flex-grow flex-col items-center justify-center bg-white py-6">
<div className="flex w-full flex-grow p-4 flex-col items-center justify-center bg-white py-6">
<div className="w-full max-w-md">
<QuestionRenderer
activeQuestionId={activeQuestionId}

View File

@@ -207,9 +207,6 @@ export default function LogicEditor({
updateQuestion(questionIdx, { logic: updatedLogic });
};
const truncate = (str: string, n: number) =>
str && str.length > n ? str.substring(0, n - 1) + "..." : str;
if (!(question.type in conditions)) {
return <></>;
}
@@ -221,19 +218,23 @@ export default function LogicEditor({
{question?.logic && question?.logic?.length !== 0 && (
<div className="mt-2 space-y-3">
{question?.logic?.map((logic, logicIdx) => (
<div key={logicIdx} className="flex items-center space-x-2 space-y-1 text-sm">
<div key={logicIdx} className="flex items-center space-x-2 space-y-1 text-xs xl:text-sm">
<BsArrowReturnRight className="h-4 w-4" />
<p className="text-slate-700">If this answer</p>
<p className="text-slate-800">If this answer</p>
<Select value={logic.condition} onValueChange={(e) => updateLogic(logicIdx, { condition: e })}>
<SelectTrigger className="min-w-fit flex-1">
<SelectValue placeholder="Select condition" />
<SelectTrigger className=" min-w-fit flex-1">
<SelectValue placeholder="Select condition" className="text-xs lg:text-sm" />
</SelectTrigger>
<SelectContent>
{conditions[question.type].map(
(condition) =>
!(question.required && condition === "skipped") && (
<SelectItem key={condition} value={condition}>
<SelectItem
key={condition}
value={condition}
title={logicConditions[condition].label}
className="text-xs lg:text-sm">
{logicConditions[condition].label}
</SelectItem>
)
@@ -251,7 +252,7 @@ export default function LogicEditor({
<SelectContent>
{logicConditions[logic.condition].values?.map((value) => (
<SelectItem key={value} value={value} title={value}>
{truncate(value, 20)}
{value}
</SelectItem>
))}
</SelectContent>
@@ -259,11 +260,15 @@ export default function LogicEditor({
) : (
<DropdownMenu>
<DropdownMenuTrigger className="z-10 cursor-pointer" asChild>
<div className="flex h-10 w-full items-center justify-between rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ">
<div className="flex h-10 w-40 items-center justify-between rounded-md border border-slate-300 bg-transparent px-3 py-2 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 ">
{logic.value?.length === 0 ? (
<p className="text-slate-400">Select match type</p>
<p className="truncate text-slate-400" title="Select match type">
Select match type
</p>
) : (
<p>{logic.value.join(", ")}</p>
<p className="truncate" title={logic.value.join(", ")}>
{logic.value.join(", ")}
</p>
)}
<ChevronDown className="h-4 w-4 opacity-50" />
</div>
@@ -275,6 +280,7 @@ export default function LogicEditor({
{logicConditions[logic.condition].values?.map((value) => (
<DropdownMenuCheckboxItem
key={value}
title={value}
checked={logic.value?.includes(value)}
onSelect={(e) => e.preventDefault()}
onCheckedChange={(e) => updateMultiSelectLogic(logicIdx, e, value)}>
@@ -287,7 +293,7 @@ export default function LogicEditor({
</div>
)}
<p className="text-slate-700">skip to</p>
<p className="text-slate-800">jump to</p>
<Select
value={logic.destination}
@@ -300,7 +306,11 @@ export default function LogicEditor({
(question, idx) =>
idx !== questionIdx && (
<SelectItem key={question.id} value={question.id} title={question.headline}>
{idx + 1} - {truncate(question.headline, 14)}
<div className="w-40">
<p className="truncate">
{idx + 1} - {question.headline}
</p>
</div>
</SelectItem>
)
)}

View File

@@ -169,7 +169,7 @@ export default function SurveyMenuBar({
}}>
Back
</Button>
<p className="pl-4 font-semibold hidden md:block">{product.name} / </p>
<p className="hidden pl-4 font-semibold md:block">{product.name} / </p>
<Input
defaultValue={localSurvey.name}
onChange={(e) => {
@@ -180,9 +180,11 @@ export default function SurveyMenuBar({
/>
</div>
{!!localSurvey?.responseRate && (
<div className="mx-auto flex items-center rounded-full border border-amber-200 bg-amber-100 p-2 text-sm text-amber-700 shadow-sm">
<ExclamationTriangleIcon className="mr-2 h-5 w-5 text-amber-400" />
This survey received responses. To keep the data consistent, make changes with caution.
<div className="mx-auto flex items-center rounded-full border border-amber-200 bg-amber-100 p-2 text-amber-700 shadow-sm">
<ExclamationTriangleIcon className=" h-5 w-5 text-amber-400" />
<p className="max-w-[90%] pl-1 text-xs lg:text-sm">
This survey received responses. To keep the data consistent, make changes with caution.
</p>
</div>
)}
<div className="mt-3 flex sm:ml-4 sm:mt-0">

View File

@@ -5,24 +5,21 @@ import Progress from "@/components/preview/Progress";
import QuestionConditional from "@/components/preview/QuestionConditional";
import ThankYouCard from "@/components/preview/ThankYouCard";
import ContentWrapper from "@/components/shared/ContentWrapper";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { useLinkSurveyUtils } from "@/lib/linkSurvey/linkSurvey";
import { cn } from "@formbricks/lib/cn";
import { Confetti } from "@formbricks/ui";
import { ArrowPathIcon } from "@heroicons/react/24/solid";
import type { Survey } from "@formbricks/types/surveys";
import { useEffect, useRef, useState } from "react";
type EnhancedSurvey = Survey & {
brandColor: string;
formbricksSignature: boolean;
};
import { TSurvey } from "@formbricks/types/v1/surveys";
import Loading from "@/app/s/[surveyId]/loading";
import { TProduct } from "@formbricks/types/v1/product";
interface LinkSurveyProps {
survey: EnhancedSurvey;
survey: TSurvey;
product: TProduct;
}
export default function LinkSurvey({ survey }: LinkSurveyProps) {
export default function LinkSurvey({ survey, product }: LinkSurveyProps) {
const {
currentQuestion,
finished,
@@ -61,7 +58,7 @@ export default function LinkSurvey({ survey }: LinkSurveyProps) {
if (!currentQuestion || prefilling) {
return (
<div className="flex h-full flex-1 items-center justify-center">
<LoadingSpinner />
<Loading />
</div>
);
}
@@ -88,18 +85,18 @@ export default function LinkSurvey({ survey }: LinkSurveyProps) {
)}
{finished ? (
<div>
<Confetti colors={[survey.brandColor, "#eee"]} />
<Confetti colors={[product.brandColor, "#eee"]} />
<ThankYouCard
headline={survey.thankYouCard.headline || "Thank you!"}
subheader={survey.thankYouCard.subheader || "Your response has been recorded."}
brandColor={survey.brandColor}
brandColor={product.brandColor}
initiateCountdown={initiateCountdown}
/>
</div>
) : (
<QuestionConditional
question={currentQuestion}
brandColor={survey.brandColor}
brandColor={product.brandColor}
lastQuestion={lastQuestion}
onSubmit={submitResponse}
storedResponseValue={storedResponseValue}
@@ -112,8 +109,8 @@ export default function LinkSurvey({ survey }: LinkSurveyProps) {
</div>
<div className="top-0 z-10 w-full border-b bg-white">
<div className="mx-auto max-w-md space-y-6 p-6">
<Progress progress={progress} brandColor={survey.brandColor} />
{survey.formbricksSignature && <FormbricksSignature />}
<Progress progress={progress} brandColor={product.brandColor} />
{product.formbricksSignature && <FormbricksSignature />}
</div>
</div>
</>

View File

@@ -10,7 +10,7 @@ const SurveyInactive = ({
surveyClosedMessage,
}: {
status: string;
surveyClosedMessage?: { heading: string; subheading: string };
surveyClosedMessage?: { heading?: string | undefined; subheading?: string | undefined };
}) => {
const icons = {
"not found": <QuestionMarkCircleIcon className="h-20 w-20" />,

View File

@@ -1,43 +0,0 @@
"use client";
import LinkSurvey from "@/app/s/[surveyId]/LinkSurvey";
import SurveyInactive from "@/app/s/[surveyId]/SurveyInactive";
import LegalFooter from "@/components/shared/LegalFooter";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { useLinkSurvey } from "@/lib/linkSurvey/linkSurvey";
interface SurveyPageProps {
surveyId: string;
}
export default function SurveyPage({ surveyId }: SurveyPageProps) {
const { survey, isLoadingSurvey, isErrorSurvey } = useLinkSurvey(surveyId);
if (isLoadingSurvey) {
return (
<div className="flex h-full flex-1 items-center justify-center">
<LoadingSpinner />
</div>
);
}
if (isErrorSurvey && isErrorSurvey.status === 404) {
return <SurveyInactive status="not found" />;
}
if (isErrorSurvey && isErrorSurvey.status === 403) {
return (
<SurveyInactive
status={isErrorSurvey.info.reason}
surveyClosedMessage={isErrorSurvey.info?.surveyClosedMessage}
/>
);
}
return (
<>
<LinkSurvey survey={survey} />
<LegalFooter />
</>
);
}

View File

@@ -0,0 +1,10 @@
export default function Loading() {
return (
<div className="flex h-full w-full items-center justify-center">
<div className="flex h-1/2 w-1/4 flex-col">
<div className="ph-no-capture h-16 w-1/3 animate-pulse rounded-lg bg-gray-200 font-medium text-slate-900"></div>
<div className="ph-no-capture mt-4 h-full animate-pulse rounded-lg bg-gray-200 text-slate-900"></div>
</div>
</div>
);
}

View File

@@ -1,5 +1,32 @@
import SurveyPage from "./SurveyPage";
export const revalidate = REVALIDATION_INTERVAL;
export default function LinkSurveyPage({ params }) {
return <SurveyPage surveyId={params.surveyId} />;
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
import LinkSurvey from "@/app/s/[surveyId]/LinkSurvey";
import LegalFooter from "@/app/s/[surveyId]/LegalFooter";
import { getSurvey } from "@formbricks/lib/services/survey";
import { getProductByEnvironmentId } from "@formbricks/lib/services/product";
import SurveyInactive from "@/app/s/[surveyId]/SurveyInactive";
export default async function LinkSurveyPage({ params }) {
const [survey, product] = await Promise.all([getSurvey(params.surveyId), getProductByEnvironmentId(params.environmentId)]);
if (survey && survey.status !== "inProgress") {
return <SurveyInactive status={survey.status} surveyClosedMessage={survey.surveyClosedMessage} />;
}
if (survey === null) {
return <SurveyInactive status="not found" />;
}
return (
<>
{survey && (
<>
<LinkSurvey survey={survey} product={product} />
<LegalFooter />
</>
)}
</>
);
}

View File

@@ -5,9 +5,10 @@ import { cn } from "@/../../packages/lib/cn";
import { isLight } from "@/lib/utils";
import { Response } from "@formbricks/types/js";
import { BackButton } from "@/components/preview/BackButton";
import { TSurveyCTAQuestion } from "@formbricks/types/v1/surveys";
interface CTAQuestionProps {
question: CTAQuestion;
question: CTAQuestion | TSurveyCTAQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;

View File

@@ -6,9 +6,10 @@ import type { ConsentQuestion } from "@formbricks/types/questions";
import { useEffect, useState } from "react";
import Headline from "./Headline";
import HtmlBody from "./HtmlBody";
import { TSurveyConsentQuestion } from "@formbricks/types/v1/surveys";
interface ConsentQuestionProps {
question: ConsentQuestion;
question: ConsentQuestion | TSurveyConsentQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;

View File

@@ -4,14 +4,15 @@ import { shuffleArray } from "@/lib/utils";
import { cn } from "@formbricks/lib/cn";
import { symmetricDifference } from "@formbricks/lib/utils/array";
import { Response } from "@formbricks/types/js";
import type { Choice, MultipleChoiceMultiQuestion } from "@formbricks/types/questions";
import type { MultipleChoiceMultiQuestion } from "@formbricks/types/questions";
import { TSurveyChoice, TSurveyMultipleChoiceMultiQuestion } from "@formbricks/types/v1/surveys";
import { Input } from "@formbricks/ui";
import { useEffect, useState } from "react";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface MultipleChoiceMultiProps {
question: MultipleChoiceMultiQuestion;
question: MultipleChoiceMultiQuestion | TSurveyMultipleChoiceMultiQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
@@ -57,7 +58,7 @@ export default function MultipleChoiceMultiQuestion({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [storedResponseValue, question.id]);
const [questionChoices, setQuestionChoices] = useState<Choice[]>(
const [questionChoices, setQuestionChoices] = useState<TSurveyChoice[]>(
question.choices
? question.shuffleOption !== "none"
? shuffleArray(question.choices, question.shuffleOption)

View File

@@ -3,7 +3,7 @@ import { shuffleArray } from "@/lib/utils";
import { cn } from "@formbricks/lib/cn";
import { Response } from "@formbricks/types/js";
import { MultipleChoiceSingleQuestion } from "@formbricks/types/questions";
import { TSurveyChoice } from "@formbricks/types/v1/surveys";
import { TSurveyChoice, TSurveyMultipleChoiceSingleQuestion } from "@formbricks/types/v1/surveys";
import { Input } from "@formbricks/ui";
import { useEffect, useRef, useState } from "react";
import Headline from "./Headline";
@@ -11,7 +11,7 @@ import Subheader from "./Subheader";
import { BackButton } from "@/components/preview/BackButton";
interface MultipleChoiceSingleProps {
question: MultipleChoiceSingleQuestion;
question: MultipleChoiceSingleQuestion | TSurveyMultipleChoiceSingleQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;

View File

@@ -6,9 +6,10 @@ import Subheader from "./Subheader";
import SubmitButton from "@/components/preview/SubmitButton";
import { Response } from "@formbricks/types/js";
import { BackButton } from "@/components/preview/BackButton";
import { TSurveyNPSQuestion } from "@formbricks/types/v1/surveys";
interface NPSQuestionProps {
question: NPSQuestion;
question: NPSQuestion | TSurveyNPSQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;

View File

@@ -5,9 +5,10 @@ import Subheader from "./Subheader";
import SubmitButton from "@/components/preview/SubmitButton";
import { Response } from "@formbricks/types/js";
import { BackButton } from "@/components/preview/BackButton";
import { TSurveyOpenTextQuestion } from "@formbricks/types/v1/surveys";
interface OpenTextQuestionProps {
question: OpenTextQuestion;
question: TSurveyOpenTextQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;

View File

@@ -6,9 +6,10 @@ import NPSQuestion from "./NPSQuestion";
import CTAQuestion from "./CTAQuestion";
import RatingQuestion from "./RatingQuestion";
import ConsentQuestion from "./ConsentQuestion";
import { TSurveyQuestion } from "@formbricks/types/v1/surveys";
interface QuestionConditionalProps {
question: Question;
question: Question | TSurveyQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;

View File

@@ -21,9 +21,10 @@ import SubmitButton from "@/components/preview/SubmitButton";
import { Response } from "@formbricks/types/js";
import { BackButton } from "@/components/preview/BackButton";
import { TSurveyRatingQuestion } from "@formbricks/types/v1/surveys";
interface RatingQuestionProps {
question: RatingQuestion;
question: RatingQuestion | TSurveyRatingQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;

View File

@@ -1,9 +1,10 @@
import { cn } from "@/../../packages/lib/cn";
import { isLight } from "@/lib/utils";
import { Question } from "@formbricks/types/questions";
import { TSurveyQuestion } from "@formbricks/types/v1/surveys";
type SubmitButtonProps = {
question: Question;
question: Question | TSurveyQuestion;
lastQuestion: boolean;
brandColor: string;
};

View File

@@ -2,13 +2,13 @@ import { createDisplay, markDisplayResponded } from "@formbricks/lib/client/disp
import { createResponse, updateResponse } from "@formbricks/lib/client/response";
import { fetcher } from "@formbricks/lib/fetcher";
import { Response } from "@formbricks/types/js";
import { QuestionType, type Logic, type Question } from "@formbricks/types/questions";
import type { Survey } from "@formbricks/types/surveys";
import { Question, QuestionType } from "@formbricks/types/questions";
import { TResponseInput } from "@formbricks/types/v1/responses";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import useSWR from "swr";
import { useGetOrCreatePerson } from "../people/people";
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/v1/surveys";
interface StoredResponse {
id: string | null;
@@ -27,8 +27,8 @@ export const useLinkSurvey = (surveyId: string) => {
};
};
export const useLinkSurveyUtils = (survey: Survey) => {
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null);
export const useLinkSurveyUtils = (survey: TSurvey) => {
const [currentQuestion, setCurrentQuestion] = useState<TSurveyQuestion | Question | null>(null);
const [prefilling, setPrefilling] = useState(true);
const [progress, setProgress] = useState(0); // [0, 1]
const [finished, setFinished] = useState(false);
@@ -39,7 +39,7 @@ export const useLinkSurveyUtils = (survey: Survey) => {
const [initiateCountdown, setinitiateCountdown] = useState<boolean>(false);
const [storedResponseValue, setStoredResponseValue] = useState<string | null>(null);
const router = useRouter();
const URLParams = new URLSearchParams(window.location.search);
const URLParams = new URLSearchParams(typeof window !== "undefined" ? window.location.search : "");
const isPreview = URLParams.get("preview") === "true";
const hasFirstQuestionPrefill = URLParams.has(survey.questions[0].id);
const firstQuestionPrefill = hasFirstQuestionPrefill ? URLParams.get(survey.questions[0].id) : null;
@@ -331,7 +331,7 @@ const clearStoredResponses = (surveyId: string) => {
localStorage.removeItem(`formbricks-${surveyId}-response`);
};
const checkValidity = (question: Question, answer: any): boolean => {
const checkValidity = (question: TSurveyQuestion | Question, answer: any): boolean => {
if (question.required && (!answer || answer === "")) return false;
try {
switch (question.type) {
@@ -388,7 +388,7 @@ const checkValidity = (question: Question, answer: any): boolean => {
}
};
const createAnswer = (question: Question, answer: string): string | number | string[] => {
const createAnswer = (question: TSurveyQuestion | Question, answer: string): string | number | string[] => {
switch (question.type) {
case QuestionType.OpenText:
case QuestionType.MultipleChoiceSingle:
@@ -421,7 +421,7 @@ const createAnswer = (question: Question, answer: string): string | number | str
}
};
const evaluateCondition = (logic: Logic, responseValue: any): boolean => {
const evaluateCondition = (logic: TSurveyLogic, responseValue: any): boolean => {
switch (logic.condition) {
case "equals":
return (

View File

@@ -53,7 +53,7 @@ const shuffle = (array: any[]) => {
}
};
export const shuffleArray = (array: any[], shuffleOption: string) => {
export const shuffleArray = (array: any[], shuffleOption: string | undefined) => {
const arrayCopy = [...array];
const otherIndex = arrayCopy.findIndex((element) => element.id === "other");
const otherElement = otherIndex !== -1 ? arrayCopy.splice(otherIndex, 1)[0] : null;

View File

@@ -11,7 +11,7 @@
"eslint": "^8.46.0",
"eslint-config-next": "^13.4.12",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-react": "7.33.1",
"eslint-config-turbo": "latest"
"eslint-config-turbo": "latest",
"eslint-plugin-react": "7.33.1"
}
}

View File

@@ -2,8 +2,10 @@ import { TJsState } from "@formbricks/types/v1/js";
import { trackAction } from "./actions";
import { Config } from "./config";
import { NetworkError, Result, err, ok } from "./errors";
import { Logger } from "./logger";
const config = Config.getInstance();
const logger = Logger.getInstance();
const syncWithBackend = async (): Promise<Result<TJsState, NetworkError>> => {
const url = `${config.get().apiHost}/api/v1/js/sync`;
@@ -42,6 +44,9 @@ export const sync = async (): Promise<void> => {
const state = syncResult.value;
const oldState = config.get().state;
config.update({ state });
const surveyNames = state.surveys.map((s) => s.name);
logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames);
// if session is new, track action
if (!oldState?.session || oldState.session.id !== state.session.id) {
const trackActionResult = await trackAction("New Session");

View File

@@ -15,12 +15,12 @@
"@formbricks/database": "*",
"@formbricks/errors": "*",
"@formbricks/types": "*",
"@paralleldrive/cuid2": "^2.2.1",
"date-fns": "^2.30.0",
"markdown-it": "^13.0.1",
"posthog-node": "^3.1.1",
"server-only": "^0.0.1",
"tailwind-merge": "^1.14.0",
"@paralleldrive/cuid2": "^2.2.1"
"tailwind-merge": "^1.14.0"
},
"devDependencies": {
"@formbricks/tsconfig": "*",

View File

@@ -3,7 +3,7 @@ import { ZPipelineTrigger } from "./pipelines";
export const ZWebhook = z.object({
id: z.string().cuid2(),
name: z.string().nullable(),
name: z.string().nullish(),
createdAt: z.date(),
updatedAt: z.date(),
url: z.string().url(),
@@ -16,7 +16,7 @@ export type TWebhook = z.infer<typeof ZWebhook>;
export const ZWebhookInput = z.object({
url: z.string().url(),
name: z.string().nullable(),
name: z.string().nullish(),
triggers: z.array(ZPipelineTrigger),
surveyIds: z.array(z.string().cuid2()).optional(),
});

112
pnpm-lock.yaml generated
View File

@@ -50,7 +50,7 @@ importers:
version: 1.3.0(react-dom@18.2.0)(react@18.2.0)
'@docsearch/react':
specifier: ^3.5.1
version: 3.5.1(@algolia/client-search@4.14.2)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.6.0)
version: 3.5.1(@algolia/client-search@4.14.2)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.6.0)
'@formbricks/lib':
specifier: workspace:*
version: link:../../packages/lib
@@ -93,9 +93,15 @@ importers:
next-plausible:
specifier: ^3.10.1
version: 3.10.1(next@13.4.12)(react-dom@18.2.0)(react@18.2.0)
next-seo:
specifier: ^6.1.0
version: 6.1.0(next@13.4.12)(react-dom@18.2.0)(react@18.2.0)
next-sitemap:
specifier: ^4.1.8
version: 4.1.8(next@13.4.12)
node-fetch:
specifier: ^3.3.2
version: 3.3.2
prism-react-renderer:
specifier: ^2.0.6
version: 2.0.6(react@18.2.0)
@@ -111,6 +117,9 @@ importers:
react-icons:
specifier: ^4.10.1
version: 4.10.1(react@18.2.0)
react-markdown:
specifier: ^8.0.7
version: 8.0.7(@types/react@18.2.18)(react@18.2.0)
react-responsive-embed:
specifier: ^2.1.0
version: 2.1.0(prop-types@15.8.1)(react@18.2.0)
@@ -2586,7 +2595,7 @@ packages:
resolution: {integrity: sha512-2Pu9HDg/uP/IT10rbQ+4OrTQuxIWdKVUEdcw9/w7kZJv9NeHS6skJx1xuRiFyoGKwAzcHXnLp7csE99sj+O1YA==}
dev: false
/@docsearch/react@3.5.1(@algolia/client-search@4.14.2)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.6.0):
/@docsearch/react@3.5.1(@algolia/client-search@4.14.2)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)(search-insights@2.6.0):
resolution: {integrity: sha512-t5mEODdLzZq4PTFAm/dvqcvZFdPDMdfPE5rJS5SC8OUq9mPzxEy6b+9THIqNM9P0ocCb4UC5jqBrxKclnuIbzQ==}
peerDependencies:
'@types/react': '>= 16.8.0 < 19.0.0'
@@ -2603,6 +2612,7 @@ packages:
'@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.14.2)(algoliasearch@4.14.2)(search-insights@2.6.0)
'@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.14.2)(algoliasearch@4.14.2)
'@docsearch/css': 3.5.1
'@types/react': 18.2.18
algoliasearch: 4.14.2
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
@@ -6617,6 +6627,7 @@ packages:
/anymatch@2.0.0:
resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==}
requiresBuild: true
dependencies:
micromatch: 3.1.10
normalize-path: 2.1.1
@@ -6835,6 +6846,7 @@ packages:
/async-each@1.0.3:
resolution: {integrity: sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==}
requiresBuild: true
dev: true
optional: true
@@ -7154,6 +7166,7 @@ packages:
/binary-extensions@1.13.1:
resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dev: true
optional: true
@@ -7754,6 +7767,7 @@ packages:
/chokidar@2.1.8:
resolution: {integrity: sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==}
deprecated: Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies
requiresBuild: true
dependencies:
anymatch: 2.0.0
async-each: 1.0.3
@@ -8852,6 +8866,11 @@ packages:
assert-plus: 1.0.0
dev: true
/data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
dev: false
/data-urls@1.1.0:
resolution: {integrity: sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==}
dependencies:
@@ -10457,6 +10476,14 @@ packages:
bser: 2.1.1
dev: true
/fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.2.1
dev: false
/fflate@0.4.8:
resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==}
dev: false
@@ -10678,6 +10705,13 @@ packages:
combined-stream: 1.0.8
mime-types: 2.1.35
/formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
dependencies:
fetch-blob: 3.2.0
dev: false
/forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
@@ -10938,6 +10972,7 @@ packages:
/glob-parent@3.1.0:
resolution: {integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==}
requiresBuild: true
dependencies:
is-glob: 3.1.0
path-dirname: 1.0.2
@@ -11801,6 +11836,7 @@ packages:
/is-binary-path@1.0.1:
resolution: {integrity: sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
binary-extensions: 1.13.1
dev: true
@@ -11956,6 +11992,7 @@ packages:
/is-glob@3.1.0:
resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
is-extglob: 2.1.1
dev: true
@@ -12219,7 +12256,7 @@ packages:
/isomorphic-fetch@3.0.0:
resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==}
dependencies:
node-fetch: 2.6.7
node-fetch: 2.6.12
whatwg-fetch: 3.6.2
transitivePeerDependencies:
- encoding
@@ -14648,6 +14685,18 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/next-seo@6.1.0(next@13.4.12)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-iMBpFoJsR5zWhguHJvsoBDxDSmdYTHtnVPB1ij+CD0NReQCP78ZxxbdL9qkKIf4oEuZEqZkrjAQLB0bkII7RYA==}
peerDependencies:
next: ^8.1.1-canary.54 || >=9.0.0
react: '>=16.0.0'
react-dom: '>=16.0.0'
dependencies:
next: 13.4.12(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/next-sitemap@4.1.8(next@13.4.12):
resolution: {integrity: sha512-XAXpBHX4o89JfMgvrm0zimlZwpu2iBPXHpimJMUrqOZSc4C2oB1Lv89mxuVON9IE8HOezaM+w4GjJxcYCuGPTQ==}
engines: {node: '>=14.18'}
@@ -14765,6 +14814,11 @@ packages:
resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==}
dev: false
/node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
dev: false
/node-fetch@2.6.12:
resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==}
engines: {node: 4.x || >=6.0.0}
@@ -14788,6 +14842,15 @@ packages:
whatwg-url: 5.0.0
dev: true
/node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
dev: false
/node-forge@1.3.1:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'}
@@ -14849,6 +14912,7 @@ packages:
/normalize-path@2.1.1:
resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==}
engines: {node: '>=0.10.0'}
requiresBuild: true
dependencies:
remove-trailing-separator: 1.1.0
dev: true
@@ -15396,6 +15460,7 @@ packages:
/path-dirname@1.0.2:
resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==}
requiresBuild: true
dev: true
optional: true
@@ -17331,7 +17396,33 @@ packages:
/react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: true
/react-markdown@8.0.7(@types/react@18.2.18)(react@18.2.0):
resolution: {integrity: sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==}
peerDependencies:
'@types/react': '>=16'
react: '>=16'
dependencies:
'@types/hast': 2.3.4
'@types/prop-types': 15.7.5
'@types/react': 18.2.18
'@types/unist': 2.0.6
comma-separated-tokens: 2.0.3
hast-util-whitespace: 2.0.0
prop-types: 15.8.1
property-information: 6.2.0
react: 18.2.0
react-is: 18.2.0
remark-parse: 10.0.1
remark-rehype: 10.1.0
space-separated-tokens: 2.0.2
style-to-object: 0.4.2
unified: 10.1.2
unist-util-visit: 4.1.1
vfile: 5.3.6
transitivePeerDependencies:
- supports-color
dev: false
/react-radio-group@3.0.3(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-MUNRRjZqQ2y+1K6rBuH0zO+gLVmCnWIcc5GnNwr9WNoUwZ9FUAKJ1UfsKXwYS93whR6/qrZKoVgiOltRkbzezw==}
@@ -17553,6 +17644,7 @@ packages:
/readdirp@2.2.1:
resolution: {integrity: sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==}
engines: {node: '>=0.10'}
requiresBuild: true
dependencies:
graceful-fs: 4.2.10
micromatch: 3.1.10
@@ -17731,6 +17823,7 @@ packages:
/remove-trailing-separator@1.1.0:
resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==}
requiresBuild: true
dev: true
optional: true
@@ -19070,6 +19163,12 @@ packages:
inline-style-parser: 0.1.1
dev: false
/style-to-object@0.4.2:
resolution: {integrity: sha512-1JGpfPB3lo42ZX8cuPrheZbfQ6kqPPnPHlKMyeRYtfKD+0jG+QsXgXN57O/dvJlzlB2elI6dGmrPnl5VPQFPaA==}
dependencies:
inline-style-parser: 0.1.1
dev: false
/styled-jsx@5.1.1(react@18.2.0):
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
engines: {node: '>= 12.0.0'}
@@ -20588,6 +20687,11 @@ packages:
defaults: 1.0.4
dev: true
/web-streams-polyfill@3.2.1:
resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==}
engines: {node: '>= 8'}
dev: false
/webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}

View File

@@ -75,6 +75,7 @@
"RAILWAY_STATIC_URL",
"RENDER_EXTERNAL_URL",
"SENTRY_DSN",
"STRAPI_API_KEY",
"STRIPE_SECRET_KEY",
"STRIPE_WEBHOOK_SECRET",
"TELEMETRY_DISABLED",