mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-19 02:10:33 -05:00
chore: onboarding cleanup (#4479)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
a0d02a843e
commit
5d1224e438
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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/);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -104,7 +104,7 @@ export function EndingCard({
|
||||
responseData,
|
||||
variablesData
|
||||
)
|
||||
: "Respondants will not see this card"
|
||||
: "Respondents will not see this card"
|
||||
}
|
||||
questionId="EndingCard"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user