mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-21 18:18:48 -06:00
chore: vercel style guide for main app (2) (#4525)
Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
c4c98bda31
commit
a4ffc03e55
@@ -60,7 +60,7 @@ export const EmbedView = ({
|
||||
onClick={() => setActiveId(tab.id)}
|
||||
autoFocus={tab.id === activeId}
|
||||
className={cn(
|
||||
"rounded-md border px-4 py-2 text-slate-600",
|
||||
"flex justify-start rounded-md border px-4 py-2 text-slate-600",
|
||||
// "focus:ring-0 focus:ring-offset-0", // enable these classes to remove the focus rings on buttons
|
||||
tab.id === activeId
|
||||
? "border-slate-200 bg-slate-100 font-semibold text-slate-900"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use server";
|
||||
|
||||
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
|
||||
import { actionClient } from "@/lib/utils/action-client";
|
||||
import { sendLinkSurveyToVerifiedEmail } from "@/modules/email";
|
||||
import { z } from "zod";
|
||||
@@ -9,11 +8,13 @@ import { getIfResponseWithSurveyIdAndEmailExist } from "@formbricks/lib/response
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { ZId } from "@formbricks/types/common";
|
||||
import { ZLinkSurveyEmailData } from "@formbricks/types/email";
|
||||
import { InvalidInputError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
|
||||
export const sendLinkSurveyEmailAction = actionClient
|
||||
.schema(ZLinkSurveyEmailData)
|
||||
.action(async ({ parsedInput }) => {
|
||||
return await sendLinkSurveyToVerifiedEmail(parsedInput);
|
||||
await sendLinkSurveyToVerifiedEmail(parsedInput);
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
const ZVerifyTokenAction = z.object({
|
||||
@@ -34,12 +35,12 @@ export const validateSurveyPinAction = actionClient
|
||||
.schema(ZValidateSurveyPinAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const survey = await getSurvey(parsedInput.surveyId);
|
||||
if (!survey) return { error: TSurveyPinValidationResponseError.NOT_FOUND };
|
||||
if (!survey) throw new ResourceNotFoundError("Survey", parsedInput.surveyId);
|
||||
|
||||
const originalPin = survey.pin?.toString();
|
||||
|
||||
if (!originalPin) return { survey };
|
||||
if (originalPin !== parsedInput.pin) return { error: TSurveyPinValidationResponseError.INCORRECT_PIN };
|
||||
if (originalPin !== parsedInput.pin) throw new InvalidInputError("Invalid pin");
|
||||
|
||||
return { survey };
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { LinkSurveyWrapper } from "@/app/s/[surveyId]/components/LinkSurveyWrapper";
|
||||
import { SurveyLinkUsed } from "@/app/s/[surveyId]/components/SurveyLinkUsed";
|
||||
import { VerifyEmail } from "@/app/s/[surveyId]/components/VerifyEmail";
|
||||
import { VerifyEmail } from "@/app/s/[surveyId]/components/verify-email";
|
||||
import { getPrefillValue } from "@/app/s/[surveyId]/lib/prefilling";
|
||||
import { SurveyInline } from "@/modules/ui/components/survey";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { validateSurveyPinAction } from "@/app/s/[surveyId]/actions";
|
||||
import { LinkSurvey } from "@/app/s/[surveyId]/components/LinkSurvey";
|
||||
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { OTPInput } from "@/modules/ui/components/otp-input";
|
||||
import { useTranslations } from "next-intl";
|
||||
@@ -53,7 +52,7 @@ export const PinScreen = (props: PinScreenProps) => {
|
||||
const [localPinEntry, setLocalPinEntry] = useState<string>("");
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const t = useTranslations();
|
||||
const [error, setError] = useState<TSurveyPinValidationResponseError>();
|
||||
const [error, setError] = useState("");
|
||||
const [survey, setSurvey] = useState<TSurvey>();
|
||||
|
||||
const _validateSurveyPinAsync = useCallback(async (surveyId: string, pin: string) => {
|
||||
@@ -62,7 +61,7 @@ export const PinScreen = (props: PinScreenProps) => {
|
||||
if (response?.data) {
|
||||
setSurvey(response.data.survey);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(response) as TSurveyPinValidationResponseError;
|
||||
const errorMessage = getFormattedErrorMessage(response);
|
||||
setError(errorMessage);
|
||||
}
|
||||
|
||||
@@ -70,7 +69,7 @@ export const PinScreen = (props: PinScreenProps) => {
|
||||
}, []);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setError(undefined);
|
||||
setError("");
|
||||
setLoading(false);
|
||||
setLocalPinEntry("");
|
||||
}, []);
|
||||
@@ -95,7 +94,7 @@ export const PinScreen = (props: PinScreenProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
setError(undefined);
|
||||
setError("");
|
||||
setLoading(false);
|
||||
}, [_validateSurveyPinAsync, localPinEntry, surveyId]);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
getIfResponseWithSurveyIdAndEmailExistAction,
|
||||
sendLinkSurveyEmailAction,
|
||||
} from "@/app/s/[surveyId]/actions";
|
||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { FormControl, FormError, FormField, FormItem } from "@/modules/ui/components/form";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
@@ -52,7 +53,7 @@ export const VerifyEmail = ({
|
||||
},
|
||||
resolver: zodResolver(ZVerifyEmailInput),
|
||||
});
|
||||
survey = useMemo(() => {
|
||||
const localSurvey = useMemo(() => {
|
||||
return replaceHeadlineRecall(survey, "default", contactAttributeKeys);
|
||||
}, [survey, contactAttributeKeys]);
|
||||
|
||||
@@ -62,9 +63,9 @@ export const VerifyEmail = ({
|
||||
|
||||
const submitEmail = async (emailInput: TVerifyEmailInput) => {
|
||||
const email = emailInput.email.toLowerCase();
|
||||
if (survey.isSingleResponsePerEmailEnabled) {
|
||||
if (localSurvey.isSingleResponsePerEmailEnabled) {
|
||||
const actionResult = await getIfResponseWithSurveyIdAndEmailExistAction({
|
||||
surveyId: survey.id,
|
||||
surveyId: localSurvey.id,
|
||||
email,
|
||||
});
|
||||
if (actionResult?.data) {
|
||||
@@ -76,17 +77,19 @@ export const VerifyEmail = ({
|
||||
}
|
||||
}
|
||||
const data = {
|
||||
surveyId: survey.id,
|
||||
email: email as string,
|
||||
surveyName: survey.name,
|
||||
surveyId: localSurvey.id,
|
||||
email: email,
|
||||
surveyName: localSurvey.name,
|
||||
suId: singleUseId ?? "",
|
||||
locale,
|
||||
};
|
||||
try {
|
||||
await sendLinkSurveyEmailAction(data);
|
||||
|
||||
const actionResult = await sendLinkSurveyEmailAction(data);
|
||||
if (actionResult?.data) {
|
||||
setEmailSent(true);
|
||||
} catch (error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(actionResult);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,9 +107,9 @@ export const VerifyEmail = ({
|
||||
<div className="flex h-[100vh] w-[100vw] flex-col items-center justify-center bg-slate-50">
|
||||
<span className="h-24 w-24 rounded-full bg-slate-300 p-6 text-5xl">🤔</span>
|
||||
<p className="mt-8 text-4xl font-bold">{t("s.this_looks_fishy")}</p>
|
||||
<p className="mt-4 cursor-pointer text-sm text-slate-400" onClick={handleGoBackClick}>
|
||||
<Button variant="ghost" className="mt-4" onClick={handleGoBackClick}>
|
||||
{t("s.please_try_again_with_the_original_link")}
|
||||
</p>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -116,10 +119,16 @@ export const VerifyEmail = ({
|
||||
<Toaster />
|
||||
<StackedCardsContainer
|
||||
cardArrangement={
|
||||
survey.styling?.cardArrangement?.linkSurveys ?? styling.cardArrangement?.linkSurveys ?? "straight"
|
||||
localSurvey.styling?.cardArrangement?.linkSurveys ??
|
||||
styling.cardArrangement?.linkSurveys ??
|
||||
"straight"
|
||||
}>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(submitEmail)}>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await form.handleSubmit(submitEmail)(e);
|
||||
}}>
|
||||
{!emailSent && !showPreviewQuestions && (
|
||||
<div className="flex flex-col">
|
||||
<div className="mx-auto rounded-full border bg-slate-200 p-6">
|
||||
@@ -139,7 +148,9 @@ export const VerifyEmail = ({
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(email) => field.onChange(email)}
|
||||
onChange={(email) => {
|
||||
field.onChange(email);
|
||||
}}
|
||||
type="email"
|
||||
placeholder="engineering@acme.com"
|
||||
className="h-10 bg-white"
|
||||
@@ -154,9 +165,9 @@ export const VerifyEmail = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<p className="mt-6 cursor-pointer text-xs text-slate-400" onClick={handlePreviewClick}>
|
||||
{t("s.just_curious")} <span className="underline">{t("s.preview_survey_questions")}</span>
|
||||
</p>
|
||||
<Button variant="ghost" className="mt-6" onClick={handlePreviewClick}>
|
||||
{t("s.just_curious")} <span>{t("s.preview_survey_questions")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
@@ -165,15 +176,15 @@ export const VerifyEmail = ({
|
||||
<div>
|
||||
<p className="text-4xl font-bold">{t("s.question_preview")}</p>
|
||||
<div className="mt-4 flex w-full flex-col justify-center rounded-lg border border-slate-200 bg-slate-50 bg-opacity-20 p-8 text-slate-700">
|
||||
{survey.questions.map((question, index) => (
|
||||
{localSurvey.questions.map((question, index) => (
|
||||
<p
|
||||
key={index}
|
||||
className="my-1">{`${index + 1}. ${getLocalizedValue(question.headline, languageCode)}`}</p>
|
||||
className="my-1">{`${(index + 1).toString()}. ${getLocalizedValue(question.headline, languageCode)}`}</p>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-6 cursor-pointer text-xs text-slate-400" onClick={handlePreviewClick}>
|
||||
{t("s.want_to_respond")} <span className="underline">{t("s.verify_email")}</span>
|
||||
</p>
|
||||
<Button variant="ghost" className="mt-6" onClick={handlePreviewClick}>
|
||||
{t("s.want_to_respond")} <span>{t("s.verify_email")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{emailSent && (
|
||||
@@ -13,7 +13,7 @@ export const getEmailVerificationDetails = async (
|
||||
return { status: "not-verified" };
|
||||
} else {
|
||||
try {
|
||||
const verifiedEmail = await verifyTokenForLinkSurvey(token, surveyId);
|
||||
const verifiedEmail = verifyTokenForLinkSurvey(token, surveyId);
|
||||
if (verifiedEmail) {
|
||||
return { status: "verified", email: verifiedEmail };
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TSurvey, TSurveyQuestion, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { FORBIDDEN_IDS } from "@formbricks/types/surveys/validation";
|
||||
|
||||
export const getPrefillValue = (
|
||||
survey: TSurvey,
|
||||
@@ -7,20 +8,22 @@ export const getPrefillValue = (
|
||||
languageId: string
|
||||
): TResponseData | undefined => {
|
||||
const prefillAnswer: TResponseData = {};
|
||||
let questionIdxMap: { [key: string]: number } = {};
|
||||
let questionIdxMap: Record<string, number> = {};
|
||||
|
||||
survey.questions.forEach((q, idx) => {
|
||||
questionIdxMap[q.id] = idx;
|
||||
});
|
||||
|
||||
searchParams.forEach((value, key) => {
|
||||
if (FORBIDDEN_IDS.includes(key)) return;
|
||||
const questionId = key;
|
||||
const questionIdx = questionIdxMap[questionId];
|
||||
const question = survey.questions[questionIdx];
|
||||
const answer = value;
|
||||
|
||||
if (question && checkValidity(question, answer, languageId)) {
|
||||
prefillAnswer[questionId] = transformAnswer(question, answer, languageId);
|
||||
if (question) {
|
||||
if (checkValidity(question, answer, languageId)) {
|
||||
prefillAnswer[questionId] = transformAnswer(question, answer, languageId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -62,8 +65,8 @@ export const checkValidity = (question: TSurveyQuestion, answer: string, languag
|
||||
return true;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.NPS: {
|
||||
answer = answer.replace(/&/g, ";");
|
||||
const answerNumber = Number(JSON.parse(answer));
|
||||
const cleanedAnswer = answer.replace(/&/g, ";");
|
||||
const answerNumber = Number(JSON.parse(cleanedAnswer));
|
||||
|
||||
if (isNaN(answerNumber)) return false;
|
||||
if (answerNumber < 0 || answerNumber > 10) return false;
|
||||
@@ -80,8 +83,8 @@ export const checkValidity = (question: TSurveyQuestion, answer: string, languag
|
||||
return true;
|
||||
}
|
||||
case TSurveyQuestionTypeEnum.Rating: {
|
||||
answer = answer.replace(/&/g, ";");
|
||||
const answerNumber = Number(JSON.parse(answer));
|
||||
const cleanedAnswer = answer.replace(/&/g, ";");
|
||||
const answerNumber = Number(JSON.parse(cleanedAnswer));
|
||||
if (answerNumber < 1 || answerNumber > question.range) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -115,8 +118,8 @@ export const transformAnswer = (
|
||||
|
||||
case TSurveyQuestionTypeEnum.Rating:
|
||||
case TSurveyQuestionTypeEnum.NPS: {
|
||||
answer = answer.replace(/&/g, ";");
|
||||
return Number(JSON.parse(answer));
|
||||
const cleanedAnswer = answer.replace(/&/g, ";");
|
||||
return Number(JSON.parse(cleanedAnswer));
|
||||
}
|
||||
|
||||
case TSurveyQuestionTypeEnum.PictureSelection: {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const getMetadataForLinkSurvey = async (surveyId: string): Promise<Metada
|
||||
throw new Error("Project not found");
|
||||
}
|
||||
|
||||
const brandColor = getBrandColorForURL(survey.styling?.brandColor?.light || COLOR_DEFAULTS.brandColor);
|
||||
const brandColor = getBrandColorForURL(survey.styling?.brandColor?.light ?? COLOR_DEFAULTS.brandColor);
|
||||
const surveyName = getNameForURL(survey.name);
|
||||
|
||||
const ogImgURL = `/api/v1/og?brandColor=${brandColor}&name=${surveyName}`;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { HelpCircleIcon } from "lucide-react";
|
||||
import { StaticImport } from "next/dist/shared/lib/get-img-props";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import footerLogo from "./lib/footerlogo.svg";
|
||||
@@ -18,7 +19,7 @@ const NotFound = () => {
|
||||
</div>
|
||||
<div>
|
||||
<Link href="https://formbricks.com">
|
||||
<Image src={footerLogo} alt="Brand logo" className="mx-auto w-40" />
|
||||
<Image src={footerLogo as StaticImport} alt="Brand logo" className="mx-auto w-40" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,18 +54,18 @@ const Page = async (props: LinkSurveyPageProps) => {
|
||||
const langParam = searchParams.lang; //can either be language code or alias
|
||||
const isSingleUseSurvey = survey?.singleUse?.enabled;
|
||||
const isSingleUseSurveyEncrypted = survey?.singleUse?.isEncrypted;
|
||||
const isEmbed = searchParams.embed === "true" ? true : false;
|
||||
const isEmbed = searchParams.embed === "true";
|
||||
if (!survey || survey.type !== "link" || survey.status === "draft") {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const organization = await getOrganizationByEnvironmentId(survey?.environmentId);
|
||||
const organization = await getOrganizationByEnvironmentId(survey.environmentId);
|
||||
if (!organization) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
|
||||
|
||||
if (survey && survey.status !== "inProgress" && !isPreview) {
|
||||
if (survey.status !== "inProgress" && !isPreview) {
|
||||
return (
|
||||
<SurveyInactive
|
||||
status={survey.status}
|
||||
@@ -105,14 +105,11 @@ const Page = async (props: LinkSurveyPageProps) => {
|
||||
}
|
||||
|
||||
// verify email: Check if the survey requires email verification
|
||||
let emailVerificationStatus: string = "";
|
||||
let emailVerificationStatus = "";
|
||||
let verifiedEmail: string | undefined = undefined;
|
||||
|
||||
if (survey.isVerifyEmailEnabled) {
|
||||
const token =
|
||||
searchParams && Object.keys(searchParams).length !== 0 && searchParams.hasOwnProperty("verify")
|
||||
? searchParams.verify
|
||||
: undefined;
|
||||
const token = searchParams.verify;
|
||||
|
||||
if (token) {
|
||||
const emailVerificationDetails = await getEmailVerificationDetails(survey.id, token);
|
||||
@@ -141,13 +138,13 @@ const Page = async (props: LinkSurveyPageProps) => {
|
||||
if (selectedLanguage?.default || !selectedLanguage?.enabled) {
|
||||
return "default";
|
||||
}
|
||||
return selectedLanguage ? selectedLanguage.language.code : "default";
|
||||
return selectedLanguage.language.code;
|
||||
}
|
||||
};
|
||||
|
||||
const languageCode = getLanguageCode();
|
||||
|
||||
const isSurveyPinProtected = Boolean(!!survey && survey.pin);
|
||||
const isSurveyPinProtected = Boolean(survey.pin);
|
||||
const responseCount = await getResponseCountBySurveyId(survey.id);
|
||||
|
||||
if (isSurveyPinProtected) {
|
||||
@@ -172,7 +169,7 @@ const Page = async (props: LinkSurveyPageProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
return survey ? (
|
||||
return (
|
||||
<LinkSurvey
|
||||
survey={survey}
|
||||
project={project}
|
||||
@@ -191,7 +188,7 @@ const Page = async (props: LinkSurveyPageProps) => {
|
||||
locale={locale}
|
||||
isPreview={isPreview}
|
||||
/>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum TSurveyPinValidationResponseError {
|
||||
INCORRECT_PIN = "INCORRECT_PIN",
|
||||
INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
|
||||
NOT_FOUND = "NOT_FOUND",
|
||||
}
|
||||
@@ -8,27 +8,20 @@ export const metadata: Metadata = {
|
||||
description: "Open-source Experience Management. Free & open source.",
|
||||
};
|
||||
|
||||
const renderRichText = async (text: string) => {
|
||||
const t = await getTranslations();
|
||||
return <p>{t.rich(text, { b: (chunks) => <b>{chunks}</b> })}</p>;
|
||||
};
|
||||
|
||||
const Page = async () => {
|
||||
const t = await getTranslations();
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<h2 className="mb-6 text-xl font-medium">{t("setup.intro.welcome_to_formbricks")}</h2>
|
||||
<div className="mx-auto max-w-sm space-y-4 text-sm leading-6 text-slate-600">
|
||||
<p>
|
||||
{t.rich("setup.intro.paragraph_1", {
|
||||
b: (chunks) => <b>{chunks}</b>,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t.rich("setup.intro.paragraph_2", {
|
||||
b: (chunks) => <b>{chunks}</b>,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t.rich("setup.intro.paragraph_3", {
|
||||
b: (chunks) => <b>{chunks}</b>,
|
||||
})}
|
||||
</p>
|
||||
{renderRichText("setup.intro.paragraph_1")}
|
||||
{renderRichText("setup.intro.paragraph_2")}
|
||||
{renderRichText("setup.intro.paragraph_3")}
|
||||
</div>
|
||||
<Button className="mt-6" asChild>
|
||||
<Link href="/setup/signup">{t("setup.intro.get_started")}</Link>
|
||||
|
||||
@@ -7,10 +7,9 @@ const FreshInstanceLayout = async ({ children }: { children: React.ReactNode })
|
||||
const session = await getServerSession(authOptions);
|
||||
const isFreshInstance = await getIsFreshInstance();
|
||||
|
||||
if (session || !isFreshInstance) {
|
||||
if (session ?? !isFreshInstance) {
|
||||
return notFound();
|
||||
}
|
||||
if (!isFreshInstance) return notFound();
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
|
||||
@@ -45,17 +45,15 @@ export const inviteOrganizationMemberAction = authenticatedActionClient
|
||||
currentUserId: ctx.user.id,
|
||||
});
|
||||
|
||||
if (invite) {
|
||||
await sendInviteMemberEmail(
|
||||
invite.id,
|
||||
parsedInput.email,
|
||||
ctx.user.name ?? "",
|
||||
"",
|
||||
false, // is onboarding invite
|
||||
undefined,
|
||||
ctx.user.locale
|
||||
);
|
||||
}
|
||||
await sendInviteMemberEmail(
|
||||
invite.id,
|
||||
parsedInput.email,
|
||||
ctx.user.name,
|
||||
"",
|
||||
false, // is onboarding invite
|
||||
undefined,
|
||||
ctx.user.locale
|
||||
);
|
||||
|
||||
return invite;
|
||||
});
|
||||
|
||||
@@ -31,13 +31,7 @@ export const InviteMembers = ({ IS_SMTP_CONFIGURED, organizationId }: InviteMemb
|
||||
const { isSubmitting } = form.formState;
|
||||
|
||||
const inviteTeamMembers = async (data: TInviteMembersFormSchema) => {
|
||||
const members = Object.values(data).filter((member) => member.email && member.email.trim());
|
||||
if (!members.length) {
|
||||
router.push("/");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const member of members) {
|
||||
for (const member of Object.values(data)) {
|
||||
try {
|
||||
if (!member.email) continue;
|
||||
await inviteOrganizationMemberAction({
|
||||
@@ -49,7 +43,6 @@ export const InviteMembers = ({ IS_SMTP_CONFIGURED, organizationId }: InviteMemb
|
||||
toast.success(`${t("setup.invite.invitation_sent_to")} ${member.email}!`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to invite:", member.email, error);
|
||||
toast.error(`${t("setup.invite.failed_to_invite")} ${member.email}.`);
|
||||
}
|
||||
}
|
||||
@@ -69,16 +62,21 @@ export const InviteMembers = ({ IS_SMTP_CONFIGURED, organizationId }: InviteMemb
|
||||
<AlertDescription>{t("setup.invite.smtp_not_configured_description")}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<form onSubmit={form.handleSubmit(inviteTeamMembers)} className="space-y-4">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void form.handleSubmit(inviteTeamMembers)(e);
|
||||
}}
|
||||
className="space-y-4">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<h2 className="text-2xl font-medium">{t("setup.invite.invite_your_organization_members")}</h2>
|
||||
<p>{t("setup.invite.life_s_no_fun_alone")}</p>
|
||||
|
||||
{Array.from({ length: membersCount }).map((_, index) => (
|
||||
<div key={`member-${index}`} className="space-y-2">
|
||||
<div key={`member-${index.toString()}`} className="space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`member-${index}.email`}
|
||||
name={`member-${index.toString()}.email`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
@@ -88,7 +86,7 @@ export const InviteMembers = ({ IS_SMTP_CONFIGURED, organizationId }: InviteMemb
|
||||
{...field}
|
||||
placeholder={`user@example.com`}
|
||||
className="w-80"
|
||||
isInvalid={!!error?.message}
|
||||
isInvalid={Boolean(error?.message)}
|
||||
/>
|
||||
</div>
|
||||
{error?.message && <FormError className="text-left">{error.message}</FormError>}
|
||||
@@ -99,7 +97,7 @@ export const InviteMembers = ({ IS_SMTP_CONFIGURED, organizationId }: InviteMemb
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`member-${index}.name`}
|
||||
name={`member-${index.toString()}.name`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
@@ -109,7 +107,7 @@ export const InviteMembers = ({ IS_SMTP_CONFIGURED, organizationId }: InviteMemb
|
||||
{...field}
|
||||
placeholder={`Full Name (optional)`}
|
||||
className="w-80"
|
||||
isInvalid={!!error?.message}
|
||||
isInvalid={Boolean(error?.message)}
|
||||
/>
|
||||
</div>
|
||||
{error?.message && <FormError className="text-left">{error.message}</FormError>}
|
||||
@@ -121,7 +119,12 @@ export const InviteMembers = ({ IS_SMTP_CONFIGURED, organizationId }: InviteMemb
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Button variant="ghost" onClick={() => setMembersCount((count) => count + 1)} type="button">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setMembersCount((count) => count + 1);
|
||||
}}
|
||||
type="button">
|
||||
<PlusIcon />
|
||||
{t("setup.invite.add_another_member")}
|
||||
</Button>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { InviteMembers } from "@/app/setup/organization/[organizationId]/invite/components/InviteMembers";
|
||||
import { InviteMembers } from "@/app/setup/organization/[organizationId]/invite/components/invite-members";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { Metadata } from "next";
|
||||
import { getServerSession } from "next-auth";
|
||||
@@ -8,15 +8,22 @@ import { SMTP_HOST, SMTP_PASSWORD, SMTP_PORT, SMTP_USER } from "@formbricks/lib/
|
||||
import { verifyUserRoleAccess } from "@formbricks/lib/organization/auth";
|
||||
import { AuthenticationError } from "@formbricks/types/errors";
|
||||
|
||||
type Params = Promise<{
|
||||
organizationId: string;
|
||||
}>;
|
||||
export const metadata: Metadata = {
|
||||
title: "Invite",
|
||||
description: "Open-source Experience Management. Free & open source.",
|
||||
};
|
||||
|
||||
const Page = async (props) => {
|
||||
interface InvitePageProps {
|
||||
params: Params;
|
||||
}
|
||||
|
||||
const Page = async (props: InvitePageProps) => {
|
||||
const params = await props.params;
|
||||
const t = await getTranslations();
|
||||
const IS_SMTP_CONFIGURED: boolean = SMTP_HOST && SMTP_PORT && SMTP_USER && SMTP_PASSWORD ? true : false;
|
||||
const IS_SMTP_CONFIGURED = Boolean(SMTP_HOST && SMTP_PORT && SMTP_USER && SMTP_PASSWORD);
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthenticationError(t("common.session_not_found"));
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { authenticatedActionClient } from "@/lib/utils/action-client";
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { z } from "zod";
|
||||
import { gethasNoOrganizations } from "@formbricks/lib/instance/service";
|
||||
import { createMembership } from "@formbricks/lib/membership/service";
|
||||
import { createOrganization } from "@formbricks/lib/organization/service";
|
||||
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
|
||||
const ZCreateOrganizationAction = z.object({
|
||||
organizationName: z.string(),
|
||||
});
|
||||
|
||||
export const createOrganizationAction = authenticatedActionClient
|
||||
.schema(ZCreateOrganizationAction)
|
||||
.action(async ({ ctx, parsedInput }) => {
|
||||
const hasNoOrganizations = await gethasNoOrganizations();
|
||||
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
|
||||
|
||||
if (!hasNoOrganizations && !isMultiOrgEnabled) {
|
||||
throw new OperationNotAllowedError("This action can only be performed on a fresh instance.");
|
||||
}
|
||||
|
||||
const newOrganization = await createOrganization({
|
||||
name: parsedInput.organizationName,
|
||||
});
|
||||
|
||||
await createMembership(newOrganization.id, ctx.user.id, {
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
return newOrganization;
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { createOrganizationAction } from "@/app/setup/organization/create/actions";
|
||||
import { createOrganizationAction } from "@/modules/organization/actions";
|
||||
import { Button } from "@/modules/ui/components/button";
|
||||
import { FormControl, FormError, FormField, FormItem, FormProvider } from "@/modules/ui/components/form";
|
||||
import { Input } from "@/modules/ui/components/input";
|
||||
@@ -31,10 +31,9 @@ export const CreateOrganization = () => {
|
||||
|
||||
const organizationName = form.watch("name");
|
||||
|
||||
const onSubmit: SubmitHandler<TCreateOrganizationForm> = async (data) => {
|
||||
const onSubmit: SubmitHandler<TCreateOrganizationForm> = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
const organizationName = data.name.trim();
|
||||
const createOrganizationResponse = await createOrganizationAction({ organizationName });
|
||||
if (createOrganizationResponse?.data) {
|
||||
router.push(`/setup/organization/${createOrganizationResponse.data.id}/invite`);
|
||||
@@ -47,7 +46,11 @@ export const CreateOrganization = () => {
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
void form.handleSubmit(onSubmit)(e);
|
||||
}}>
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<h2 className="text-2xl font-medium">{t("setup.organization.create.title")}</h2>
|
||||
<p>{t("setup.organization.create.description")}</p>
|
||||
@@ -59,7 +62,7 @@ export const CreateOrganization = () => {
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
isInvalid={!!form.formState.errors.name}
|
||||
isInvalid={Boolean(form.formState.errors.name)}
|
||||
placeholder="e.g., Acme Inc"
|
||||
className="w-80"
|
||||
required
|
||||
@@ -32,7 +32,12 @@ export const RemovedFromOrganization = ({ user, isFormbricksCloud }: RemovedFrom
|
||||
formbricksLogout={formbricksLogout}
|
||||
organizationsWithSingleOwner={[]}
|
||||
/>
|
||||
<Button onClick={() => setIsModalOpen(true)}>{t("setup.organization.create.delete_account")}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsModalOpen(true);
|
||||
}}>
|
||||
{t("setup.organization.create.delete_account")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RemovedFromOrganization } from "@/app/setup/organization/create/components/RemovedFromOrganization";
|
||||
import { RemovedFromOrganization } from "@/app/setup/organization/create/components/removed-from-organization";
|
||||
import { authOptions } from "@/modules/auth/lib/authOptions";
|
||||
import { getIsMultiOrgEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||
import { ClientLogout } from "@/modules/ui/components/client-logout";
|
||||
@@ -11,7 +11,7 @@ import { gethasNoOrganizations } from "@formbricks/lib/instance/service";
|
||||
import { getOrganizationsByUserId } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { AuthenticationError } from "@formbricks/types/errors";
|
||||
import { CreateOrganization } from "./components/CreateOrganization";
|
||||
import { CreateOrganization } from "./components/create-organization";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Organization",
|
||||
@@ -37,7 +37,7 @@ const Page = async () => {
|
||||
return <CreateOrganization />;
|
||||
}
|
||||
|
||||
if (!hasNoOrganizations && userOrganizations.length === 0 && !isMultiOrgEnabled) {
|
||||
if (userOrganizations.length === 0) {
|
||||
return <RemovedFromOrganization user={user} isFormbricksCloud={IS_FORMBRICKS_CLOUD} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,17 @@ import { getSurvey, getSurveyIdByResultShareKey } from "@formbricks/lib/survey/s
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { findMatchingLocale } from "@formbricks/lib/utils/locale";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
type Params = Promise<{
|
||||
sharingKey: string;
|
||||
}>;
|
||||
|
||||
interface ResponsesPageProps {
|
||||
params: Params;
|
||||
}
|
||||
|
||||
const Page = async (props: ResponsesPageProps) => {
|
||||
const t = await getTranslations();
|
||||
const params = await props.params;
|
||||
const surveyId = await getSurveyIdByResultShareKey(params.sharingKey);
|
||||
|
||||
if (!surveyId) {
|
||||
|
||||
@@ -10,9 +10,17 @@ import { getProjectByEnvironmentId } from "@formbricks/lib/project/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey, getSurveyIdByResultShareKey } from "@formbricks/lib/survey/service";
|
||||
|
||||
const Page = async (props) => {
|
||||
const params = await props.params;
|
||||
type Params = Promise<{
|
||||
sharingKey: string;
|
||||
}>;
|
||||
|
||||
interface SummaryPageProps {
|
||||
params: Params;
|
||||
}
|
||||
|
||||
const Page = async (props: SummaryPageProps) => {
|
||||
const t = await getTranslations();
|
||||
const params = await props.params;
|
||||
const surveyId = await getSurveyIdByResultShareKey(params.sharingKey);
|
||||
|
||||
if (!surveyId) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const Page = async (props) => {
|
||||
type Params = Promise<{
|
||||
sharingKey: string;
|
||||
}>;
|
||||
|
||||
const Page = async (props: { params: Params }) => {
|
||||
const params = await props.params;
|
||||
return redirect(`/share/${params.sharingKey}/summary`);
|
||||
};
|
||||
|
||||
@@ -15,8 +15,8 @@ interface DeleteAccountModalProps {
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
user: TUser;
|
||||
isFormbricksCloud: boolean;
|
||||
formbricksLogout: () => void;
|
||||
organizationsWithSingleOwner: TOrganization[];
|
||||
formbricksLogout: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const DeleteAccountModal = ({
|
||||
|
||||
@@ -11,7 +11,7 @@ import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
|
||||
const ZCreateOrganizationAction = z.object({
|
||||
organizationName: z.string(),
|
||||
organizationName: z.string().min(1, "Organization name must be at least 1 character long"),
|
||||
});
|
||||
|
||||
export const createOrganizationAction = authenticatedActionClient
|
||||
|
||||
@@ -35,7 +35,7 @@ export const createInviteToken = (inviteId: string, email: string, options = {})
|
||||
return jwt.sign({ inviteId: encryptedInviteId, email: encryptedEmail }, env.NEXTAUTH_SECRET, options);
|
||||
};
|
||||
|
||||
export const verifyTokenForLinkSurvey = (token: string, surveyId: string) => {
|
||||
export const verifyTokenForLinkSurvey = (token: string, surveyId: string): string | null => {
|
||||
try {
|
||||
const { email } = jwt.verify(token, env.NEXTAUTH_SECRET + surveyId) as JwtPayload;
|
||||
try {
|
||||
|
||||
@@ -2023,7 +2023,7 @@
|
||||
"made_with_love_in_kiel": "Made with 🤍 in Germany",
|
||||
"paragraph_1": "Formbricks is an Experience Management Suite built of the <b>fastest growing open source survey platform</b> worldwide.",
|
||||
"paragraph_2": "Run targeted surveys on websites, in apps or anywhere online. Gather valuable insights to <b>craft irresistible experiences</b> for customers, users and employees.",
|
||||
"paragraph_3": "We're commited to highest degree of data privacy. Self-host to keep <b>full control over your data</b>. Always",
|
||||
"paragraph_3": "We're commited to highest degree of data privacy. Self-host to keep <b>full control over your data</b>.",
|
||||
"welcome_to_formbricks": "Welcome to Formbricks!"
|
||||
},
|
||||
"invite": {
|
||||
|
||||
@@ -39,7 +39,7 @@ export type TInviteUpdateInput = z.infer<typeof ZInviteUpdateInput>;
|
||||
|
||||
export const ZInviteMembersFormSchema = z.record(
|
||||
z.object({
|
||||
email: z.string().email("Invalid email address").optional().or(z.literal("")),
|
||||
email: z.string().email("Invalid email address"),
|
||||
name: ZUserName,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ export const FORBIDDEN_IDS = [
|
||||
"verifiedEmail",
|
||||
"multiLanguage",
|
||||
"embed",
|
||||
"verify",
|
||||
];
|
||||
|
||||
const FIELD_TO_LABEL_MAP: Record<string, string> = {
|
||||
|
||||
Reference in New Issue
Block a user