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:
Dhruwang Jariwala
2024-12-27 10:50:50 +05:30
committed by GitHub
parent c4c98bda31
commit a4ffc03e55
29 changed files with 165 additions and 164 deletions

View File

@@ -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"

View File

@@ -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 };
});

View File

@@ -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";

View File

@@ -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]);

View File

@@ -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 && (

View File

@@ -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 {

View File

@@ -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: {

View File

@@ -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}`;

View File

@@ -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>

View File

@@ -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;

View File

@@ -1,5 +0,0 @@
export enum TSurveyPinValidationResponseError {
INCORRECT_PIN = "INCORRECT_PIN",
INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR",
NOT_FOUND = "NOT_FOUND",
}

View File

@@ -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>

View File

@@ -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}</>;
};

View File

@@ -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;
});

View File

@@ -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>

View File

@@ -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"));

View File

@@ -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;
});

View File

@@ -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

View File

@@ -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>
);
};

View File

@@ -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} />;
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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`);
};

View File

@@ -15,8 +15,8 @@ interface DeleteAccountModalProps {
setOpen: Dispatch<SetStateAction<boolean>>;
user: TUser;
isFormbricksCloud: boolean;
formbricksLogout: () => void;
organizationsWithSingleOwner: TOrganization[];
formbricksLogout: () => Promise<void>;
}
export const DeleteAccountModal = ({

View File

@@ -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

View File

@@ -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 {

View File

@@ -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": {

View File

@@ -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,
})
);

View File

@@ -21,6 +21,7 @@ export const FORBIDDEN_IDS = [
"verifiedEmail",
"multiLanguage",
"embed",
"verify",
];
const FIELD_TO_LABEL_MAP: Record<string, string> = {