chore: onboarding cleanup (#4479)

Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
This commit is contained in:
Dhruwang Jariwala
2024-12-18 13:36:45 +05:30
committed by GitHub
parent a0d02a843e
commit 5d1224e438
27 changed files with 193 additions and 563 deletions

View File

@@ -26,10 +26,6 @@ export const ConnectWithFormbricks = ({
const t = useTranslations();
const router = useRouter();
const handleFinishOnboarding = async () => {
if (!widgetSetupCompleted) {
router.push(`/environments/${environment.id}/connect/invite`);
return;
}
router.push(`/environments/${environment.id}/surveys`);
};
@@ -89,7 +85,7 @@ export const ConnectWithFormbricks = ({
onClick={handleFinishOnboarding}>
{widgetSetupCompleted
? t("environments.connect.finish_onboarding")
: t("environments.connect.i_dont_know_how_to_do_it")}
: t("environments.connect.do_it_later")}
<ArrowRight />
</Button>
</div>

View File

@@ -1,153 +0,0 @@
"use client";
import { inviteOrganizationMemberAction } from "@/app/(app)/(onboarding)/organizations/actions";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { Button } from "@/modules/ui/components/button";
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
import { Input } from "@/modules/ui/components/input";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { FormProvider, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { z } from "zod";
import { TOrganization } from "@formbricks/types/organizations";
import { ZUserName } from "@formbricks/types/user";
interface InviteOrganizationMemberProps {
organization: TOrganization;
environmentId: string;
}
const ZInviteOrganizationMemberDetails = z.object({
name: ZUserName,
email: z.string().email(),
inviteMessage: z
.string()
.trim()
.min(1)
.refine((value) => !/https?:\/\/|<script/i.test(value), "Invite message cannot contain URLs or scripts"),
});
type TInviteOrganizationMemberDetails = z.infer<typeof ZInviteOrganizationMemberDetails>;
export const InviteOrganizationMember = ({ organization, environmentId }: InviteOrganizationMemberProps) => {
const router = useRouter();
const t = useTranslations();
const form = useForm<TInviteOrganizationMemberDetails>({
defaultValues: {
name: "",
email: "",
inviteMessage: t("environments.connect.invite.invite_message_content"),
},
resolver: zodResolver(ZInviteOrganizationMemberDetails),
});
const { isSubmitting } = form.formState;
const handleInvite = async (data: TInviteOrganizationMemberDetails) => {
const response = await inviteOrganizationMemberAction({
organizationId: organization.id,
email: data.email,
name: data.name,
role: "member",
inviteMessage: data.inviteMessage,
});
if (response?.data) {
toast.success(t("environments.connect.invite.invite_sent_successfully"));
await finishOnboarding();
} else {
const errorMessage = getFormattedErrorMessage(response);
toast.error(errorMessage);
}
};
const finishOnboarding = async () => {
router.push(`/environments/${environmentId}/surveys`);
};
return (
<div className="mb-8 w-full max-w-xl space-y-8">
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(handleInvite)} className="w-full space-y-4">
<div className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<FormLabel>{t("common.name")}</FormLabel>
<FormControl>
<div>
<Input
value={field.value}
onChange={(name) => field.onChange(name)}
placeholder="John Doe"
className="bg-white"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<FormLabel>{t("common.email")}</FormLabel>
<FormControl>
<div>
<Input
value={field.value}
onChange={(email) => field.onChange(email)}
placeholder="engineering@acme.com"
className="bg-white"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="inviteMessage"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<FormLabel>{t("environments.connect.invite.invite_message")}</FormLabel>
<FormControl>
<div>
<textarea
rows={5}
className="focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-transparent bg-white px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300"
value={field.value}
onChange={(inviteMessage) => field.onChange(inviteMessage)}
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<div className="flex w-full justify-end space-x-2">
<Button
id="onboarding-inapp-invite-have-a-look-first"
className="text-slate-400"
variant="ghost"
onClick={(e) => {
e.preventDefault();
finishOnboarding();
}}>
{t("common.not_now")}
</Button>
<Button id="onboarding-inapp-invite-send-invite" type={"submit"} loading={isSubmitting}>
{t("common.invite")}
</Button>
</div>
</div>
</form>
</FormProvider>
</div>
);
};

View File

@@ -117,7 +117,10 @@ export const OnboardingSetupInstructions = ({
{channel === "app" ? npmSnippetForAppSurveys : npmSnippetForWebsiteSurveys}
</CodeBlock>
<Button id="onboarding-inapp-connect-read-npm-docs" className="mt-3" variant="secondary" asChild>
<Link href={`https://formbricks.com/docs/${channel}-surveys/framework-guides`} target="_blank">
<Link
className="no-underline"
href={`https://formbricks.com/docs/${channel}-surveys/framework-guides`}
target="_blank">
{t("common.read_docs")}
</Link>
</Button>
@@ -149,7 +152,8 @@ export const OnboardingSetupInstructions = ({
<Button id="onboarding-inapp-connect-step-by-step-manual" variant="secondary" asChild>
<Link
href={`https://formbricks.com/docs/${channel}-surveys/framework-guides#html`}
target="_blank">
target="_blank"
className="no-underline">
{t("common.step_by_step_manual")}
</Link>
</Button>

View File

@@ -1,59 +0,0 @@
import { InviteOrganizationMember } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/InviteOrganizationMember";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header";
import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { notFound, redirect } from "next/navigation";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
interface InvitePageProps {
params: Promise<{
environmentId: string;
}>;
}
const Page = async (props: InvitePageProps) => {
const params = await props.params;
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
}
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!organization) {
throw new Error("Organization not Found");
}
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
if (!membership || (membership.role !== "owner" && membership.role !== "manager")) {
return notFound();
}
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center">
<Header
title={t("environments.connect.invite.headline")}
subtitle={t("environments.connect.invite.subtitle")}
/>
<div className="space-y-4 text-center">
<p className="text-4xl font-medium text-slate-800"></p>
<p className="text-sm text-slate-500"></p>
</div>
<InviteOrganizationMember organization={organization} environmentId={params.environmentId} />
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={`/environments/${params.environmentId}`}>
<XIcon className="h-7 w-7" strokeWidth={1.5} />
</Link>
</Button>
</div>
);
};
export default Page;

View File

@@ -5,7 +5,7 @@ import { getXMTemplates } from "@/app/(app)/(onboarding)/environments/[environme
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { createSurveyAction } from "@/modules/surveys/components/TemplateList/actions";
import { ActivityIcon, ShoppingCartIcon, UsersIcon } from "lucide-react";
import { ActivityIcon, ShoppingCartIcon, SmileIcon, StarIcon, ThumbsUpIcon, UsersIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
@@ -45,7 +45,7 @@ export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListP
}
};
const handleTemplateClick = (templateIdx) => {
const handleTemplateClick = (templateIdx: number) => {
setActiveTemplateId(templateIdx);
const template = getXMTemplates(user.locale)[templateIdx];
const newTemplate = replacePresetPlaceholders(template, project);
@@ -60,7 +60,7 @@ export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListP
onClick: () => handleTemplateClick(0),
isLoading: activeTemplateId === 0,
},
/* {
{
title: t("environments.xm-templates.five_star_rating"),
description: t("environments.xm-templates.five_star_rating_description"),
icon: StarIcon,
@@ -73,7 +73,7 @@ export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListP
icon: ThumbsUpIcon,
onClick: () => handleTemplateClick(2),
isLoading: activeTemplateId === 2,
}, */
},
{
title: t("environments.xm-templates.ces"),
description: t("environments.xm-templates.ces_description"),
@@ -81,13 +81,13 @@ export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListP
onClick: () => handleTemplateClick(3),
isLoading: activeTemplateId === 3,
},
/* {
{
title: t("environments.xm-templates.smileys"),
description: t("environments.xm-templates.smileys_description"),
icon: SmileIcon,
onClick: () => handleTemplateClick(4),
isLoading: activeTemplateId === 4,
}, */
},
{
title: t("environments.xm-templates.enps"),
description: t("environments.xm-templates.enps_description"),

View File

@@ -71,9 +71,10 @@ const NPSSurvey = (locale: string): TXMTemplate => {
const StarRatingSurvey = (locale: string): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()];
const defaultSurvey = getXMSurveyDefault(locale);
return {
...getXMSurveyDefault(locale),
...defaultSurvey,
name: translate("star_rating_survey_name", locale),
questions: [
{
@@ -142,7 +143,7 @@ const StarRatingSurvey = (locale: string): TXMTemplate => {
{
id: createId(),
objective: "jumpToQuestion",
target: getXMSurveyDefault(locale).endings[0].id,
target: defaultSurvey.endings[0].id,
},
],
},
@@ -172,9 +173,10 @@ const StarRatingSurvey = (locale: string): TXMTemplate => {
const CSATSurvey = (locale: string): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()];
const defaultSurvey = getXMSurveyDefault(locale);
return {
...getXMSurveyDefault(locale),
...defaultSurvey,
name: translate("csat_survey_name", locale),
questions: [
{
@@ -242,7 +244,7 @@ const CSATSurvey = (locale: string): TXMTemplate => {
{
id: createId(),
objective: "jumpToQuestion",
target: getXMSurveyDefault(locale).endings[0].id,
target: defaultSurvey.endings[0].id,
},
],
},
@@ -303,9 +305,10 @@ const CESSurvey = (locale: string): TXMTemplate => {
const SmileysRatingSurvey = (locale: string): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()];
const defaultSurvey = getXMSurveyDefault(locale);
return {
...getXMSurveyDefault(locale),
...defaultSurvey,
name: translate("smileys_survey_name", locale),
questions: [
{
@@ -374,7 +377,7 @@ const SmileysRatingSurvey = (locale: string): TXMTemplate => {
{
id: createId(),
objective: "jumpToQuestion",
target: getXMSurveyDefault(locale).endings[0].id,
target: defaultSurvey.endings[0].id,
},
],
},

View File

@@ -1,12 +0,0 @@
import { TProjectConfigChannel } from "@formbricks/types/project";
export const getCustomHeadline = (channel?: TProjectConfigChannel) => {
switch (channel) {
case "website":
return "organizations.projects.new.settings.website_channel_headline";
case "app":
return "organizations.projects.new.settings.app_channel_headline";
default:
return "organizations.projects.new.settings.link_channel_headline";
}
};

View File

@@ -2,7 +2,7 @@ import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizatio
import { authOptions } from "@/modules/auth/lib/authOptions";
import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header";
import { GlobeIcon, GlobeLockIcon, LinkIcon, XIcon } from "lucide-react";
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import Link from "next/link";
@@ -25,27 +25,17 @@ const Page = async (props: ChannelPageProps) => {
const t = await getTranslations();
const channelOptions = [
{
title: t("organizations.projects.new.channel.public_website"),
description: t("organizations.projects.new.channel.public_website_description"),
icon: GlobeIcon,
iconText: t("organizations.projects.new.channel.public_website_icon_text"),
href: `/organizations/${params.organizationId}/projects/new/settings?channel=website`,
},
{
title: t("organizations.projects.new.channel.app_with_sign_up"),
description: t("organizations.projects.new.channel.app_with_sign_up_description"),
icon: GlobeLockIcon,
iconText: t("organizations.projects.new.channel.app_with_sign_up_icon_text"),
href: `/organizations/${params.organizationId}/projects/new/settings?channel=app`,
},
{
channel: "link",
title: t("organizations.projects.new.channel.link_and_email_surveys"),
description: t("organizations.projects.new.channel.link_and_email_surveys_description"),
icon: LinkIcon,
iconText: t("organizations.projects.new.channel.link_and_email_surveys_icon_text"),
icon: SendIcon,
href: `/organizations/${params.organizationId}/projects/new/settings?channel=link`,
},
{
title: t("organizations.projects.new.channel.in_product_surveys"),
description: t("organizations.projects.new.channel.in_product_surveys_description"),
icon: PictureInPicture2Icon,
href: `/organizations/${params.organizationId}/projects/new/settings?channel=app`,
},
];
const projects = await getUserProjects(session.user.id, params.organizationId);

View File

@@ -44,6 +44,7 @@ interface ProjectSettingsProps {
organizationTeams: TOrganizationTeam[];
canDoRoleManagement: boolean;
locale: string;
userProjectsCount: number;
}
export const ProjectSettings = ({
@@ -55,6 +56,7 @@ export const ProjectSettings = ({
organizationTeams,
canDoRoleManagement = false,
locale,
userProjectsCount,
}: ProjectSettingsProps) => {
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
@@ -105,8 +107,10 @@ export const ProjectSettings = ({
styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } },
teamIds: [],
},
resolver: zodResolver(ZProjectUpdateInput),
});
const projectName = form.watch("name");
const logoUrl = form.watch("logo.url");
const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor;
const { isSubmitting } = form.formState;
@@ -172,7 +176,7 @@ export const ProjectSettings = ({
)}
/>
{canDoRoleManagement && (
{canDoRoleManagement && userProjectsCount > 0 && (
<FormField
control={form.control}
name="teamIds"
@@ -180,8 +184,10 @@ export const ProjectSettings = ({
<FormItem className="w-full space-y-4">
<div className="flex items-center justify-between">
<div>
<FormLabel>Teams</FormLabel>
<FormDescription>Who all can access this project?</FormDescription>
<FormLabel>{t("common.teams")}</FormLabel>
<FormDescription>
{t("organizations.projects.new.settings.team_description")}
</FormDescription>
</div>
<Button
variant="secondary"
@@ -227,7 +233,7 @@ export const ProjectSettings = ({
<p className="text-sm text-slate-400">{t("common.preview")}</p>
<div className="z-0 h-3/4 w-3/4">
<SurveyInline
survey={getPreviewSurvey(locale)}
survey={getPreviewSurvey(locale, projectName || "my Product")}
styling={{ brandColor: { light: brandColor } }}
isBrandingEnabled={false}
languageCode="default"

View File

@@ -1,5 +1,4 @@
import { getTeamsByOrganizationId } from "@/app/(app)/(onboarding)/lib/onboarding";
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
import { ProjectSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/projects/new/settings/components/ProjectSettings";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
@@ -41,7 +40,6 @@ const Page = async (props: ProjectSettingsPageProps) => {
const industry = searchParams.industry || null;
const mode = searchParams.mode || "surveys";
const locale = session?.user.id ? await getUserLocale(session.user.id) : undefined;
const customHeadline = getCustomHeadline(channel);
const projects = await getUserProjects(session.user.id, params.organizationId);
const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
@@ -60,17 +58,10 @@ const Page = async (props: ProjectSettingsPageProps) => {
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
{channel === "link" || mode === "cx" ? (
<Header
title={t("organizations.projects.new.settings.channel_settings_title")}
subtitle={t("organizations.projects.new.settings.channel_settings_subtitle")}
/>
) : (
<Header
title={t(customHeadline)}
subtitle={t("organizations.projects.new.settings.channel_settings_description")}
/>
)}
<Header
title={t("organizations.projects.new.settings.project_settings_title")}
subtitle={t("organizations.projects.new.settings.project_settings_subtitle")}
/>
<ProjectSettings
organizationId={params.organizationId}
projectMode={mode}
@@ -80,6 +71,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
organizationTeams={organizationTeams}
canDoRoleManagement={canDoRoleManagement}
locale={locale ?? DEFAULT_LOCALE}
userProjectsCount={projects.length}
/>
{projects.length >= 1 && (
<Button

View File

@@ -1,62 +0,0 @@
"use server";
import { authenticatedActionClient } from "@/lib/utils/action-client";
import { checkAuthorizationUpdated } from "@/lib/utils/action-client-middleware";
import { sendInviteMemberEmail } from "@/modules/email";
import { z } from "zod";
import { INVITE_DISABLED } from "@formbricks/lib/constants";
import { inviteUser } from "@formbricks/lib/invite/service";
import { ZId } from "@formbricks/types/common";
import { AuthenticationError } from "@formbricks/types/errors";
import { ZOrganizationRole } from "@formbricks/types/memberships";
import { ZUserName } from "@formbricks/types/user";
const ZInviteOrganizationMemberAction = z.object({
organizationId: ZId,
email: z.string(),
name: ZUserName,
role: ZOrganizationRole,
inviteMessage: z.string(),
});
export const inviteOrganizationMemberAction = authenticatedActionClient
.schema(ZInviteOrganizationMemberAction)
.action(async ({ ctx, parsedInput }) => {
if (INVITE_DISABLED) {
throw new AuthenticationError("Invite disabled");
}
await checkAuthorizationUpdated({
userId: ctx.user.id,
organizationId: parsedInput.organizationId,
access: [
{
type: "organization",
roles: ["owner", "manager"],
},
],
});
const invite = await inviteUser({
organizationId: parsedInput.organizationId,
invitee: {
email: parsedInput.email,
name: parsedInput.name,
role: parsedInput.role,
},
currentUserId: ctx.user.id,
});
if (invite) {
await sendInviteMemberEmail(
invite.id,
parsedInput.email,
ctx.user.name ?? "",
parsedInput.name,
true, // is onboarding invite
parsedInput.inviteMessage,
ctx.user.locale
);
}
return invite;
});

View File

@@ -27,7 +27,7 @@ export const OnboardingOptionsContainer = ({ options }: OnboardingOptionsContain
description={option.description}
loading={option.isLoading || false}>
<div className="flex flex-col items-center">
<Icon className="h-16 w-16 text-slate-600" strokeWidth={0.5} />
<Icon className="h-16 w-16 text-slate-600" strokeWidth={1} />
{option.iconText && (
<p className="mt-4 w-fit rounded-xl bg-slate-200 px-4 text-sm text-slate-700">
{option.iconText}

View File

@@ -3,7 +3,6 @@ import { authOptions } from "@/modules/auth/lib/authOptions";
import { ClientLogout } from "@/modules/ui/components/client-logout";
import type { Session } from "next-auth";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { getFirstEnvironmentIdByUserId } from "@formbricks/lib/environment/service";
import { getIsFreshInstance } from "@formbricks/lib/instance/service";
@@ -12,7 +11,6 @@ import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getOrganizationsByUserId } from "@formbricks/lib/organization/service";
const Page = async () => {
const t = await getTranslations();
const session: Session | null = await getServerSession(authOptions);
const isFreshInstance = await getIsFreshInstance();
@@ -35,11 +33,7 @@ const Page = async () => {
}
let environmentId: string | null = null;
try {
environmentId = await getFirstEnvironmentIdByUserId(session?.user.id);
} catch (error) {
console.error(`error getting environment: ${error}`);
}
environmentId = await getFirstEnvironmentIdByUserId(session?.user.id);
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,
@@ -48,7 +42,6 @@ const Page = async () => {
const { isManager, isOwner } = getAccessFlags(currentUserMembership?.role);
if (!environmentId) {
console.error(t("common.failed_to_get_first_environment_of_user"));
if (isOwner || isManager) {
return redirect(`/organizations/${userOrganizations[0].id}/projects/new/mode`);
} else {

View File

@@ -6,7 +6,9 @@ export const BackToLoginButton = async () => {
const t = await getTranslations();
return (
<Button variant="secondary" className="w-full justify-center">
<Link href="/auth/login">{t("auth.signup.log_in")}</Link>
<Link href="/auth/login" className="h-full w-full">
{t("auth.signup.log_in")}
</Link>
</Button>
);
};

View File

@@ -13,8 +13,6 @@ export const TermsPrivacyLinks = ({ termsUrl, privacyUrl }: TermsPrivacyLinksPro
return (
<div className="mt-3 text-center text-xs text-slate-500">
{t("auth.signup.terms_of_service")}
<br />
{termsUrl && (
<Link className="font-semibold" href={termsUrl} rel="noreferrer" target="_blank">
{t("auth.signup.terms_of_service")}

View File

@@ -192,7 +192,7 @@ export const ThemeStyling = ({
<div className="relative w-1/2 rounded-lg bg-slate-100 pt-4">
<div className="sticky top-4 mb-4 h-[600px]">
<ThemeStylingPreviewSurvey
survey={getPreviewSurvey(locale) as TSurvey}
survey={getPreviewSurvey(locale, project.name) as TSurvey}
project={{
...project,
styling: form.watch(),

View File

@@ -12,9 +12,9 @@ interface PathwayOptionProps {
}
const sizeClasses = {
sm: "rounded-lg max-w-xs min-w-40 border border-slate-200 shadow-card-sm transition-all duration-150",
md: "rounded-xl max-w-xs min-w-40 border border-slate-200 shadow-card-md transition-all duration-300",
lg: "rounded-2xl max-w-sm min-w-40 border border-slate-200 shadow-card-lg transition-all duration-500",
sm: "p-4 rounded-lg w-60 shadow-md",
md: "p-6 rounded-xl w-80 shadow-lg",
lg: "p-8 rounded-2xl w-100 shadow-xl",
};
export const OptionCard: React.FC<PathwayOptionProps> = ({
@@ -25,26 +25,29 @@ export const OptionCard: React.FC<PathwayOptionProps> = ({
onSelect,
loading,
cssId,
}) => (
<div className="relative h-full">
<div
id={cssId}
className={`flex h-full cursor-pointer flex-col items-center justify-center bg-white p-6 hover:scale-105 hover:border-slate-300 ${sizeClasses[size]}`}
onClick={onSelect}
role="button"
tabIndex={0}>
<div className="space-y-4">
{children}
<div className="space-y-2 text-center">
<p className="text-xl font-medium text-slate-800">{title}</p>
<p className="text-balance text-sm text-slate-500">{description}</p>
}) => {
return (
<div className="relative h-full w-full">
<div
id={cssId}
className={`flex cursor-pointer flex-col items-center justify-center border border-slate-200 bg-white transition-transform duration-200 hover:scale-[1.03] hover:border-slate-300 ${sizeClasses[size]}`}
onClick={onSelect}
role="button"
tabIndex={0}>
<div className="flex flex-col items-center space-y-4">
{children}
<div className="text-center">
<p className="text-lg font-medium text-slate-800">{title}</p>
<p className="text-sm text-slate-500">{description}</p>
</div>
</div>
</div>
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-slate-100/70">
<LoadingSpinner />
</div>
)}
</div>
{loading && (
<div className="absolute inset-0 flex h-full w-full items-center justify-center bg-slate-100 opacity-50">
<LoadingSpinner />
</div>
)}
</div>
);
);
};

View File

@@ -12,7 +12,7 @@ test.describe("Onboarding Flow Test", async () => {
await page.waitForURL(/\/organizations\/[^/]+\/projects\/new\/mode/);
await page.getByRole("button", { name: "Formbricks Surveys Multi-" }).click();
await page.getByRole("button", { name: "Anywhere online Link" }).click();
await page.getByRole("button", { name: "Link & email surveys" }).click();
// await page.getByRole("button", { name: "B2B and B2C E-Commerce" }).click();
await page.getByPlaceholder("e.g. Formbricks").click();
await page.getByPlaceholder("e.g. Formbricks").fill(projectName);
@@ -29,15 +29,13 @@ test.describe("Onboarding Flow Test", async () => {
await page.waitForURL(/\/organizations\/[^/]+\/projects\/new\/mode/);
await page.getByRole("button", { name: "Formbricks Surveys Multi-" }).click();
await page.getByRole("button", { name: "Enrich user profiles App with" }).click();
await page.getByRole("button", { name: "In-product surveys" }).click();
// await page.getByRole("button", { name: "B2B and B2C E-Commerce" }).click();
await page.getByPlaceholder("e.g. Formbricks").click();
await page.getByPlaceholder("e.g. Formbricks").fill(projectName);
await page.locator("#form-next-button").click();
await page.getByRole("button", { name: "I don't know how to do it" }).click();
await page.waitForURL(/\/environments\/[^/]+\/connect\/invite/);
await page.getByRole("button", { name: "Not now" }).click();
await page.getByRole("button", { name: "I'll do it later" }).click();
await page.waitForURL(/\/environments\/[^/]+\/surveys/);
await expect(page.getByText(projectName)).toBeVisible();

View File

@@ -83,12 +83,10 @@ export const finishOnboarding = async (
await page.getByRole("button", { name: "Formbricks Surveys Multi-" }).click();
if (projectChannel === "website") {
await page.getByRole("button", { name: "Built for scale Public website" }).click();
} else if (projectChannel === "app") {
await page.getByRole("button", { name: "Enrich user profiles App with" }).click();
if (projectChannel === "app") {
await page.getByRole("button", { name: "In-product surveys" }).click();
} else {
await page.getByRole("button", { name: "Anywhere online Link" }).click();
await page.getByRole("button", { name: "Link & email surveys" }).click();
}
// await page.getByRole("button", { name: "Proven methods SaaS" }).click();
@@ -97,9 +95,7 @@ export const finishOnboarding = async (
await page.locator("#form-next-button").click();
if (projectChannel !== "link") {
await page.getByRole("button", { name: "I don't know how to do it" }).click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: "Not now" }).click();
await page.getByRole("button", { name: "I'll do it later" }).click();
}
await page.waitForURL(/\/environments\/[^/]+\/surveys/);

View File

@@ -132,23 +132,19 @@ export const getFirstEnvironmentIdByUserId = async (userId: string): Promise<str
try {
const organizations = await getOrganizationsByUserId(userId);
if (organizations.length === 0) {
throw new Error(`Unable to get first environment: User ${userId} has no organizations`);
return null;
}
const firstOrganization = organizations[0];
const projects = await getUserProjects(userId, firstOrganization.id);
if (projects.length === 0) {
throw new Error(
`Unable to get first environment: Organization ${firstOrganization.id} has no projects`
);
return null;
}
const firstProject = projects[0];
const productionEnvironment = firstProject.environments.find(
(environment) => environment.type === "production"
);
if (!productionEnvironment) {
throw new Error(
`Unable to get first environment: Project ${firstProject.id} has no production environment`
);
return null;
}
return productionEnvironment.id;
} catch (error) {

View File

@@ -568,18 +568,11 @@
"connect": {
"congrats": "Glückwunsch!",
"connection_successful_message": "Gut gemacht! Wir sind verbunden.",
"do_it_later": "Ich mache es später",
"finish_onboarding": "Onboarding abschließen",
"headline": "Lass uns deine App oder Webseite mit Formbricks verbinden",
"i_dont_know_how_to_do_it": "Ich weiß nicht wie",
"headline": "Verbinde deine App oder Website",
"import_formbricks_and_initialize_the_widget_in_your_component": "Importiere Formbricks und initialisiere das Widget in deiner Komponente (z.B. App.tsx):",
"insert_this_code_into_the_head_tag_of_your_website": "Füge diesen Code in den head-Tag deiner Website ein:",
"invite": {
"headline": "Wer ist dein Lieblingsentwickler?",
"invite_message": "Einladungstext",
"invite_message_content": "Ich schaue mir Formbricks an, um gezielte Umfragen durchzuführen. Kannst Du mir helfen, es einzurichten? 🙏",
"invite_sent_successfully": "Einladung erfolgreich gesendet",
"subtitle": "Lade deinen technikaffinen Kollegen ein, dir bei der Einrichtung zu helfen."
},
"subtitle": "Das dauert keine 4 Minuten.",
"waiting_for_your_signal": "Warte auf ein Signal von dir..."
},
@@ -1921,17 +1914,12 @@
"projects": {
"new": {
"channel": {
"app_with_sign_up": "App mit Anmeldung",
"app_with_sign_up_description": "Führe granular zielgerichtete Umfragen durch.",
"app_with_sign_up_icon_text": "Benutzerprofile anreichern",
"channel_select_subtitle": "Führe Umfragen auf öffentlichen Websites, in deiner App oder mit teilbaren Links und E-Mails durch.",
"channel_select_title": "Wo möchtest Du hauptsächlich Leute befragen?",
"channel_select_subtitle": "Teile einen Link oder zeige deine Umfrage in Apps oder auf Websites.",
"channel_select_title": "Welche Art von Umfrage brauchst du?",
"in_product_surveys": "In-Product-Umfragen",
"in_product_surveys_description": "Führe zielgerichtete Micro-Umfragen in deinen Apps durch.",
"link_and_email_surveys": "Link- und E-Mail-Umfragen",
"link_and_email_surveys_description": "Erreiche Menschen überall online.",
"link_and_email_surveys_icon_text": "Überall online",
"public_website": "Öffentliche Website",
"public_website_description": "Führe perfekt abgestimmte Pop-up-Umfragen durch.",
"public_website_icon_text": "Für große Skalen gebaut"
"link_and_email_surveys_description": "Erreiche Menschen überall online."
},
"mode": {
"formbricks_cx": "Formbricks CX",
@@ -1944,14 +1932,14 @@
"app_channel_headline": "Lass uns herausfinden, was deine Nutzer brauchen!",
"brand_color": "Markenfarbe",
"brand_color_description": "Passe die Hauptfarbe der Umfragen an deine Marke an.",
"channel_settings_description": "Erhalte doppelt so viele Antworten mit Umfragen, die zu deiner Marke und UI passen.",
"channel_settings_subtitle": "Wenn Leute deine Marke erkennen, ist es viel wahrscheinlicher, dass sie Umfragen beantworten und abschließen.",
"channel_settings_title": "Passe deine Marke an, erhalte doppelt so viele Antworten.",
"create_new_team": "Neues Team erstellen",
"link_channel_headline": "Du pflegst ein Produkt, wie aufregend!",
"project_creation_failed": "Projekterstellung fehlgeschlagen",
"project_name": "Produktname",
"project_name_description": "Wie heißt dein Produkt?",
"project_settings_subtitle": "Wenn Leute deine Marke erkennen, ist es viel wahrscheinlicher, dass sie Umfragen beantworten und abschließen.",
"project_settings_title": "Lass die Teilnehmenden wissen, dass du es bist",
"team_description": "Wer kann auf dieses Projekt zugreifen?",
"website_channel_headline": "Lass uns Alles aus deinem Website-Traffic herausholen!"
}
}
@@ -2693,18 +2681,17 @@
"picture_selection": "Bilderauswahl",
"picture_selection_description": "Bitte die Befragten, ein oder mehrere Bilder auszuwählen",
"picture_selection_headline": "Welcher Welpe ist der süßeste?",
"preview_survey_ending_card_description": "Mach bitte mit deinem Onboarding weiter.",
"preview_survey_ending_card_headline": "Geschafft!",
"preview_survey_name": "Vorschau",
"preview_survey_question_1_headline": "Das ist eine Vorschau",
"preview_survey_question_1_placeholder": "Tippe deine Antwort hier...",
"preview_survey_question_1_subheader": "Klick Dich durch, um die Darstellung der Umfrage zu testen.",
"preview_survey_question_2_headline": "Wie würdest Du mein Produkt bewerten?",
"preview_survey_question_2_lower_label": "Nicht gut",
"preview_survey_question_2_subheader": "Keine Sorge, sei ehrlich.",
"preview_survey_question_2_upper_label": "Sehr gut",
"preview_survey_question_3_choice_1_label": "Kuchen essen 🍰",
"preview_survey_question_3_choice_2_label": "Kuchen aufbewahren 🎂",
"preview_survey_question_3_headline": "Was machst du?",
"preview_survey_question_3_subheader": "Kann nicht beides machen.",
"preview_survey_question_1_headline": "Wie würdest Du {projectName} bewerten?",
"preview_survey_question_1_lower_label": "Nicht gut",
"preview_survey_question_1_subheader": "Das ist eine Vorschau der Umfrage.",
"preview_survey_question_1_upper_label": "Sehr gut",
"preview_survey_question_2_back_button_label": "Zurück",
"preview_survey_question_2_choice_1_label": "Ja, halte mich auf dem Laufenden.",
"preview_survey_question_2_choice_2_label": "Nein, danke!",
"preview_survey_question_2_headline": "Willst du auf dem Laufenden bleiben?",
"preview_survey_welcome_card_headline": "Willkommen!",
"preview_survey_welcome_card_html": "Danke für dein Feedback - los geht's!",
"prioritize_features_description": "Identifiziere die Funktionen, die deine Nutzer am meisten und am wenigsten brauchen.",

View File

@@ -568,18 +568,11 @@
"connect": {
"congrats": "Congrats!",
"connection_successful_message": "Well done! We're connected.",
"do_it_later": "I'll do it later",
"finish_onboarding": "Finish Onboarding",
"headline": "Let's connect your product with Formbricks",
"i_dont_know_how_to_do_it": "I don't know how to do it",
"headline": "Connect your app or website",
"import_formbricks_and_initialize_the_widget_in_your_component": "Import Formbricks and initialize the widget in your Component (e.g. App.tsx):",
"insert_this_code_into_the_head_tag_of_your_website": "Insert this code into the head tag of your website:",
"invite": {
"headline": "Who is your favorite engineer?",
"invite_message": "Invite Message",
"invite_message_content": "I'm looking into Formbricks to run targeted surveys. Can you help me set it up? 🙏",
"invite_sent_successfully": "Invite sent successfully",
"subtitle": "Invite your tech-savvy co-worker to help with the setup."
},
"subtitle": "It takes less than 4 minutes.",
"waiting_for_your_signal": "Waiting for your signal..."
},
@@ -1921,17 +1914,12 @@
"projects": {
"new": {
"channel": {
"app_with_sign_up": "App with sign up",
"app_with_sign_up_description": "Run highly-targeted micro-surveys.",
"app_with_sign_up_icon_text": "Enrich user profiles",
"channel_select_subtitle": "Run surveys on public websites, in your app, or with shareable links & emails.",
"channel_select_title": "Where do you mainly want to survey people?",
"channel_select_subtitle": "Share a link or display your survey in apps or on websites.",
"channel_select_title": "What type of surveys do you need?",
"in_product_surveys": "In-product surveys",
"in_product_surveys_description": "Embedded in apps or websites.",
"link_and_email_surveys": "Link & email surveys",
"link_and_email_surveys_description": "Reach people anywhere online.",
"link_and_email_surveys_icon_text": "Anywhere online",
"public_website": "Public website",
"public_website_description": "Run well-timed pop-up surveys.",
"public_website_icon_text": "Built for scale"
"link_and_email_surveys_description": "Reach people anywhere online."
},
"mode": {
"formbricks_cx": "Formbricks CX",
@@ -1944,14 +1932,14 @@
"app_channel_headline": "Let's research what your users need!",
"brand_color": "Brand color",
"brand_color_description": "Match the main color of surveys with your brand.",
"channel_settings_description": "Get 2x more responses matching surveys with your brand and UI",
"channel_settings_subtitle": "When people recognize your brand, they are much more likely to start and complete responses.",
"channel_settings_title": "Match your brand, get 2x more responses.",
"create_new_team": "Create new team",
"link_channel_headline": "You maintain a product, how exciting!",
"project_creation_failed": "Project creation failed",
"project_name": "Product name",
"project_name_description": "What is your product called?",
"project_settings_subtitle": "When people recognize your brand, they are much more likely to start and complete responses.",
"project_settings_title": "Let respondents know it's you",
"team_description": "Who all can access this project?",
"website_channel_headline": "Let's get the most out of your website traffic!"
}
}
@@ -2693,18 +2681,17 @@
"picture_selection": "Picture Selection",
"picture_selection_description": "Ask respondents to choose one or more images",
"picture_selection_headline": "Which is the cutest puppy?",
"preview_survey_ending_card_description": "Please continue your onboarding.",
"preview_survey_ending_card_headline": "You did it!",
"preview_survey_name": "New Survey",
"preview_survey_question_1_headline": "This is a preview survey",
"preview_survey_question_1_placeholder": "Type your answer here...",
"preview_survey_question_1_subheader": "Click through it to check the look and feel of the surveying experience.",
"preview_survey_question_2_headline": "How would you rate My Product",
"preview_survey_question_2_lower_label": "Not good",
"preview_survey_question_2_subheader": "Don't worry, be honest.",
"preview_survey_question_2_upper_label": "Very good",
"preview_survey_question_3_choice_1_label": "Eat the cake 🍰",
"preview_survey_question_3_choice_2_label": "Have the cake 🎂",
"preview_survey_question_3_headline": "What do you do?",
"preview_survey_question_3_subheader": "Can't do both.",
"preview_survey_question_1_headline": "How would you rate {projectName}?",
"preview_survey_question_1_lower_label": "Not good",
"preview_survey_question_1_subheader": "This is a survey preview.",
"preview_survey_question_1_upper_label": "Very good",
"preview_survey_question_2_back_button_label": "Back",
"preview_survey_question_2_choice_1_label": "Yes, keep me informed.",
"preview_survey_question_2_choice_2_label": "No, thank you!",
"preview_survey_question_2_headline": "What to stay in the loop?",
"preview_survey_welcome_card_headline": "Welcome!",
"preview_survey_welcome_card_html": "Thanks for providing your feedback - let's go!",
"prioritize_features_description": "Identify features your users need most and least.",

View File

@@ -568,18 +568,11 @@
"connect": {
"congrats": "Félicitations !",
"connection_successful_message": "Bien joué ! Nous sommes connectés.",
"do_it_later": "Je le ferai plus tard",
"finish_onboarding": "Terminer l'intégration",
"headline": "Connectons votre produit avec Formbricks",
"i_dont_know_how_to_do_it": "Je ne sais pas comment le faire.",
"headline": "Connecte ton appli ou site web",
"import_formbricks_and_initialize_the_widget_in_your_component": "Importez Formbricks et initialisez le widget dans votre composant (par exemple, App.tsx) :",
"insert_this_code_into_the_head_tag_of_your_website": "Insérez ce code dans la balise head de votre site web :",
"invite": {
"headline": "Qui est votre ingénieur préféré ?",
"invite_message": "Message d'invitation",
"invite_message_content": "Je m'intéresse à Formbricks pour réaliser des enquêtes ciblées. Peux-tu m'aider à le configurer ? 🙏",
"invite_sent_successfully": "Invitation envoyée avec succès",
"subtitle": "Invitez votre collègue féru de technologie à vous aider avec la configuration."
},
"subtitle": "Ça prend moins de 4 minutes.",
"waiting_for_your_signal": "En attente de votre signal..."
},
@@ -1921,17 +1914,12 @@
"projects": {
"new": {
"channel": {
"app_with_sign_up": "Application avec inscription",
"app_with_sign_up_description": "Réalisez des micro-enquêtes très ciblées.",
"app_with_sign_up_icon_text": "Enrichir les profils utilisateurs",
"channel_select_subtitle": "Réalisez des enquêtes sur des sites web publics, dans votre application, ou avec des liens et des e-mails partageables.",
"channel_select_title": "Où souhaitez-vous principalement interroger des personnes ?",
"channel_select_subtitle": "Partage un lien ou affiche ton sondage dans des applis ou sur des sites web.",
"channel_select_title": "Quel type de sondage te faut-il ?",
"in_product_surveys": "Enquêtes dans les applications",
"in_product_surveys_description": "Réalisez des enquêtes micro-ciblées dans vos applications.",
"link_and_email_surveys": "Liens et enquêtes par e-mail",
"link_and_email_surveys_description": "Atteignez les gens partout en ligne.",
"link_and_email_surveys_icon_text": "",
"public_website": "Site web public",
"public_website_description": "Réalisez des enquêtes pop-up bien chronométrées.",
"public_website_icon_text": ""
"link_and_email_surveys_description": "Atteignez les gens partout en ligne."
},
"mode": {
"formbricks_cx": "Formbricks CX",
@@ -1944,14 +1932,14 @@
"app_channel_headline": "Faisons des recherches sur ce dont vos utilisateurs ont besoin !",
"brand_color": "Couleur de marque",
"brand_color_description": "Faites correspondre la couleur principale des enquêtes avec votre marque.",
"channel_settings_description": "Obtenez 2 fois plus de réponses correspondant aux enquêtes avec votre marque et votre interface utilisateur.",
"channel_settings_subtitle": "Lorsque les gens reconnaissent votre marque, ils sont beaucoup plus susceptibles de commencer et de compléter des réponses.",
"channel_settings_title": "Alignez votre marque, obtenez 2x plus de réponses.",
"create_new_team": "Créer une nouvelle équipe",
"link_channel_headline": "",
"project_creation_failed": "Échec de la création du projet",
"project_name": "Nom du produit",
"project_name_description": "Comment s'appelle votre produit ?",
"project_settings_subtitle": "Lorsque les gens reconnaissent votre marque, ils sont beaucoup plus susceptibles de commencer et de compléter des réponses.",
"project_settings_title": "Fais savoir aux répondants que c'est toi",
"team_description": "Qui peut accéder à ce projet ?",
"website_channel_headline": "Maximisons le potentiel de votre trafic web !"
}
}
@@ -2693,18 +2681,17 @@
"picture_selection": "Sélection d'images",
"picture_selection_description": "Demandez aux répondants de choisir une ou plusieurs images",
"picture_selection_headline": "Quel est le chiot le plus mignon ?",
"preview_survey_ending_card_description": "Continue ton onboarding, s'il te plaît.",
"preview_survey_ending_card_headline": "C'est fait !",
"preview_survey_name": "Nouveau Sondage",
"preview_survey_question_1_headline": "Ceci est une enquête de prévisualisation",
"preview_survey_question_1_placeholder": "Entrez votre réponse ici...",
"preview_survey_question_1_subheader": "Cliquez dessus pour vérifier l'apparence et la convivialité de l'expérience d'enquête.",
"preview_survey_question_2_headline": "Comment évalueriez-vous Mon Produit ?",
"preview_survey_question_2_lower_label": "Pas bon",
"preview_survey_question_2_subheader": "Ne t'inquiète pas, sois honnête.",
"preview_survey_question_2_upper_label": "Très bien",
"preview_survey_question_3_choice_1_label": "Mange le gâteau 🍰",
"preview_survey_question_3_choice_2_label": "Avoir le gâteau 🎂",
"preview_survey_question_3_headline": "Que faites-vous ?",
"preview_survey_question_3_subheader": "Je ne peux pas faire les deux.",
"preview_survey_question_1_headline": "Comment évalueriez-vous {projectName} ?",
"preview_survey_question_1_lower_label": "Pas bon",
"preview_survey_question_1_subheader": "Ceci est un aperçu du sondage.",
"preview_survey_question_1_upper_label": "Très bien",
"preview_survey_question_2_back_button_label": "Retour",
"preview_survey_question_2_choice_1_label": "Oui, tiens-moi au courant.",
"preview_survey_question_2_choice_2_label": "Non, merci !",
"preview_survey_question_2_headline": "Tu veux rester dans la boucle ?",
"preview_survey_welcome_card_headline": "Bienvenue !",
"preview_survey_welcome_card_html": "Merci pour vos retours - allons-y !",
"prioritize_features_description": "Identifiez les fonctionnalités dont vos utilisateurs ont le plus et le moins besoin.",

View File

@@ -256,7 +256,7 @@
"multiple_languages": "Vários idiomas",
"name": "Nome",
"new_survey": "Nova Pesquisa",
"new_version_available": "O Formbricks {versão} chegou. Atualize agora!",
"new_version_available": "O Formbricks {version} chegou. Atualize agora!",
"next": "Próximo",
"no_background_image_found": "Imagem de fundo não encontrada.",
"no_code": "Sem código",
@@ -568,18 +568,11 @@
"connect": {
"congrats": "Parabéns!",
"connection_successful_message": "Mandou bem! Estamos conectados.",
"do_it_later": "Vou fazer isso mais tarde",
"finish_onboarding": "Concluir Integração",
"headline": "Vamos conectar seu produto com o Formbricks",
"i_dont_know_how_to_do_it": "Eu não sei como fazer isso",
"headline": "Conecte seu app ou site",
"import_formbricks_and_initialize_the_widget_in_your_component": "Importe o Formbricks e inicialize o widget no seu Componente (por exemplo, App.tsx):",
"insert_this_code_into_the_head_tag_of_your_website": "Insira esse código na tag head do seu site:",
"invite": {
"headline": "Quem é seu engenheiro favorito?",
"invite_message": "Mensagem de Convite",
"invite_message_content": "Estou dando uma olhada no Formbricks para fazer pesquisas direcionadas. Você pode me ajudar a configurar? 🙏",
"invite_sent_successfully": "Convite enviado com sucesso",
"subtitle": "Chama seu colega que manja de tecnologia pra ajudar na configuração."
},
"subtitle": "Leva menos de 4 minutos.",
"waiting_for_your_signal": "Esperando seu sinal..."
},
@@ -1921,17 +1914,12 @@
"projects": {
"new": {
"channel": {
"app_with_sign_up": "App com cadastro",
"app_with_sign_up_description": "Faça micro-pesquisas super direcionadas.",
"app_with_sign_up_icon_text": "Enriquecer perfis de usuário",
"channel_select_subtitle": "Faça pesquisas em sites públicos, no seu app ou com links e e-mails compartilháveis.",
"channel_select_title": "Onde você quer principalmente pesquisar as pessoas?",
"channel_select_subtitle": "Compartilha um link ou mostra sua pesquisa em apps ou sites.",
"channel_select_title": "Que tipo de pesquisa você quer?",
"in_product_surveys": "Pesquisas em produto",
"in_product_surveys_description": "Fazer pesquisas micro-direcionadas em apps.",
"link_and_email_surveys": "Pesquisas por link e email",
"link_and_email_surveys_description": "Alcance pessoas em qualquer lugar online.",
"link_and_email_surveys_icon_text": "Em qualquer lugar online",
"public_website": "site público",
"public_website_description": "Faça pesquisas pop-up bem cronometradas.",
"public_website_icon_text": "Feito pra crescer"
"link_and_email_surveys_description": "Alcance pessoas em qualquer lugar online."
},
"mode": {
"formbricks_cx": "Formbricks CX",
@@ -1944,14 +1932,14 @@
"app_channel_headline": "Vamos pesquisar o que seus usuários precisam!",
"brand_color": "cor da marca",
"brand_color_description": "Combine a cor principal das pesquisas com a sua marca.",
"channel_settings_description": "Obtenha 2x mais respostas combinando pesquisas com sua marca e interface",
"channel_settings_subtitle": "Quando as pessoas reconhecem sua marca, é muito mais provável que comecem e concluam respostas.",
"channel_settings_title": "Combine sua marca, receba 2x mais respostas.",
"create_new_team": "Criar nova equipe",
"link_channel_headline": "Você mantém um produto, que empolgante!",
"project_creation_failed": "Falha ao criar o projeto",
"project_name": "Nome do produto",
"project_name_description": "Como se chama o seu produto?",
"project_settings_subtitle": "Quando as pessoas reconhecem sua marca, é muito mais provável que comecem e concluam respostas.",
"project_settings_title": "Deixe os respondentes saberem que é você",
"team_description": "Quem pode acessar este projeto?",
"website_channel_headline": "Vamos aproveitar ao máximo o tráfego do seu site!"
}
}
@@ -2693,18 +2681,17 @@
"picture_selection": "Seleção de Imagem",
"picture_selection_description": "Peça aos respondentes para escolherem uma ou mais imagens",
"picture_selection_headline": "Qual é o filhote mais fofo?",
"preview_survey_name": "Nova Pesquisa",
"preview_survey_question_1_headline": "Essa é uma pesquisa de prévia",
"preview_survey_question_1_placeholder": "Digite sua resposta aqui...",
"preview_survey_question_1_subheader": "Clique para conferir o visual e a experiência da pesquisa.",
"preview_survey_question_2_headline": "Como você avaliaria meu produto?",
"preview_survey_question_2_lower_label": "Não tá bom",
"preview_survey_question_2_subheader": "Não se preocupe, seja honesto.",
"preview_survey_question_2_upper_label": "Muito bom",
"preview_survey_question_3_choice_1_label": "Come o bolo 🍰",
"preview_survey_question_3_choice_2_label": "Ficar com o bolo 🎂",
"preview_survey_question_3_headline": "O que você faz?",
"preview_survey_question_3_subheader": "Não dá pra fazer os dois.",
"preview_survey_ending_card_description": "Por favor, continue seu onboarding.",
"preview_survey_ending_card_headline": "Você conseguiu!",
"preview_survey_name": "Nova pesquisa",
"preview_survey_question_1_headline": "Como você avaliaria {projectName}?",
"preview_survey_question_1_lower_label": "Não tá bom",
"preview_survey_question_1_subheader": "Esta é uma prévia da pesquisa.",
"preview_survey_question_1_upper_label": "Muito bom",
"preview_survey_question_2_back_button_label": "Voltar",
"preview_survey_question_2_choice_1_label": "Sim, me mantenha informado.",
"preview_survey_question_2_choice_2_label": "Não, obrigado!",
"preview_survey_question_2_headline": "Quer ficar por dentro?",
"preview_survey_welcome_card_headline": "Bem-vindo!",
"preview_survey_welcome_card_html": "Valeu pelo feedback - bora lá!",
"prioritize_features_description": "Identifique os recursos que seus usuários mais e menos precisam.",

View File

@@ -51,7 +51,7 @@ export const defaultStyling: TProjectStyling = {
},
};
export const getPreviewSurvey = (locale: string) => {
export const getPreviewSurvey = (locale: string, projectName: string) => {
return {
id: "cltxxaa6x0000g8hacxdxejeu",
createdAt: new Date(),
@@ -75,21 +75,6 @@ export const getPreviewSurvey = (locale: string) => {
styling: null,
segment: null,
questions: [
{
id: "tunaz8ricd4regvkz1j0rbf6",
type: "openText",
headline: {
default: translate("preview_survey_question_1_headline", locale),
},
required: true,
inputType: "text",
subheader: {
default: translate("preview_survey_question_1_subheader", locale),
},
placeholder: {
default: translate("preview_survey_question_1_placeholder", locale),
},
},
{
id: "lbdxozwikh838yc6a8vbwuju",
type: "rating",
@@ -97,17 +82,17 @@ export const getPreviewSurvey = (locale: string) => {
scale: "star",
isDraft: true,
headline: {
default: translate("preview_survey_question_2_headline", locale),
default: translate("preview_survey_question_1_headline", locale, { projectName }),
},
required: true,
subheader: {
default: translate("preview_survey_question_2_subheader", locale),
default: translate("preview_survey_question_1_subheader", locale),
},
lowerLabel: {
default: translate("preview_survey_question_2_lower_label", locale),
default: translate("preview_survey_question_1_lower_label", locale),
},
upperLabel: {
default: translate("preview_survey_question_2_upper_label", locale),
default: translate("preview_survey_question_1_upper_label", locale),
},
},
{
@@ -117,24 +102,24 @@ export const getPreviewSurvey = (locale: string) => {
{
id: "x6wty2s72v7vd538aadpurqx",
label: {
default: translate("preview_survey_question_3_choice_1_label", locale),
default: translate("preview_survey_question_2_choice_1_label", locale),
},
},
{
id: "fbcj4530t2n357ymjp2h28d6",
label: {
default: translate("preview_survey_question_3_choice_2_label", locale),
default: translate("preview_survey_question_2_choice_2_label", locale),
},
},
],
isDraft: true,
headline: {
default: translate("preview_survey_question_3_headline", locale),
default: translate("preview_survey_question_2_headline", locale),
},
backButtonLabel: {
default: translate("preview_survey_question_2_back_button_label", locale),
},
required: true,
subheader: {
default: translate("preview_survey_question_3_subheader", locale),
},
shuffleOption: "none",
},
],
@@ -142,10 +127,8 @@ export const getPreviewSurvey = (locale: string) => {
{
id: "cltyqp5ng000108l9dmxw6nde",
type: "endScreen",
headline: { default: translate("default_ending_card_headline", locale) },
subheader: { default: translate("default_ending_card_subheader", locale) },
buttonLabel: { default: translate("default_ending_card_button_label", locale) },
buttonLink: "https://formbricks.com",
headline: { default: translate("preview_survey_ending_card_headline", locale) },
subheader: { default: translate("preview_survey_ending_card_description", locale) },
},
],
hiddenFields: {

View File

@@ -20,9 +20,17 @@ const getMessages = (locale: string) => {
return messageCache[locale];
};
export const translate = (text: string, locale: string) => {
export const translate = (text: string, locale: string, replacements?: Record<string, string>) => {
const messages = getMessages(locale ?? defaultLocale);
return messages.templates[text];
let translatedText = messages.templates[text];
if (replacements) {
Object.entries(replacements).forEach(([key, value]) => {
translatedText = translatedText.replace(new RegExp(`\\{${key}\\}`, "g"), value);
});
}
return translatedText;
};
export const getDefaultEndingCard = (languages: TSurveyLanguage[], locale: string): TSurveyEndScreenCard => {

View File

@@ -104,7 +104,7 @@ export function EndingCard({
responseData,
variablesData
)
: "Respondants will not see this card"
: "Respondents will not see this card"
}
questionId="EndingCard"
/>