feat: multi language UI (#3133)

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Dhruwang Jariwala
2024-10-31 13:53:57 +05:30
committed by GitHub
parent 705f55176f
commit b3e6e8d5d0
540 changed files with 17783 additions and 5661 deletions

View File

@@ -1,11 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json",
"access": "public",
"baseBranch": "main",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"ignore": ["@formbricks/demo", "@formbricks/web"],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@formbricks/demo", "@formbricks/web"]
"updateInternalDependencies": "patch"
}

View File

@@ -2,11 +2,6 @@
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/javascript-node-postgres
// Update the VARIANT arg in docker-compose.yml to pick a Node.js version
{
"name": "Node.js & PostgreSQL",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
// Configure tool-specific properties.
"customizations": {
// Configure properties specific to VS Code.
@@ -16,14 +11,18 @@
}
},
"dockerComposeFile": "docker-compose.yml",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or with the host.
"forwardPorts": [3000, 5432, 8025],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "cp .env.example .env && sed -i '/^ENCRYPTION_KEY=/c\\ENCRYPTION_KEY='$(openssl rand -hex 32) .env && sed -i '/^NEXTAUTH_SECRET=/c\\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env && sed -i '/^CRON_SECRET=/c\\CRON_SECRET='$(openssl rand -hex 32) .env && pnpm install && pnpm db:migrate:dev",
"name": "Node.js & PostgreSQL",
"postAttachCommand": "pnpm dev --filter=@formbricks/web... --filter=@formbricks/demo...",
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "cp .env.example .env && sed -i '/^ENCRYPTION_KEY=/c\\ENCRYPTION_KEY='$(openssl rand -hex 32) .env && sed -i '/^NEXTAUTH_SECRET=/c\\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env && sed -i '/^CRON_SECRET=/c\\CRON_SECRET='$(openssl rand -hex 32) .env && pnpm install && pnpm db:migrate:dev",
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "node"
"remoteUser": "node",
"service": "app",
"workspaceFolder": "/workspace"
}

View File

@@ -4,78 +4,78 @@ title: "[BUG]"
labels: bug
assignees: []
body:
- type: textarea
id: issue-summary
attributes:
label: Issue Summary
description: A summary of the issue. This needs to be a clear detailed-rich summary.
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
value: |
1. (for example) Went to ...
2. Clicked on...
3. ...
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: other-information
attributes:
label: Other information
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
validations:
required: false
- type: checkboxes
id: environment
attributes:
label: Environment
options:
- label: Formbricks Cloud (app.formbricks.com)
- label: Self-hosted Formbricks
- type: textarea
id: desktop-version
attributes:
label: Desktop (please complete the following information)
description: |
examples:
- **OS**: [e.g. iOS]
- **Browser**: [e.g. chrome, safari]
- **Version**: [e.g. 22]
value: |
- OS:
- Node:
- npm:
render: markdown
validations:
required: true
- type: markdown
id: nodejs-version
attributes:
value: |
#### Node.JS version
[e.g. v18.15.0]
- type: markdown
id: anything-else
attributes:
value: |
#### Anything else?
- Screen recording, console logs, network requests: You can make a recording with [Loom](https://www.loom.com).
- Anything else that you think could be an issue?
- type: textarea
id: issue-summary
attributes:
label: Issue Summary
description: A summary of the issue. This needs to be a clear detailed-rich summary.
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
value: |
1. (for example) Went to ...
2. Clicked on...
3. ...
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
id: other-information
attributes:
label: Other information
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem.
validations:
required: false
- type: checkboxes
id: environment
attributes:
label: Environment
options:
- label: Formbricks Cloud (app.formbricks.com)
- label: Self-hosted Formbricks
- type: textarea
id: desktop-version
attributes:
label: Desktop (please complete the following information)
description: |
examples:
- **OS**: [e.g. iOS]
- **Browser**: [e.g. chrome, safari]
- **Version**: [e.g. 22]
value: |
- OS:
- Node:
- npm:
render: markdown
validations:
required: true
- type: markdown
id: nodejs-version
attributes:
value: |
#### Node.JS version
[e.g. v18.15.0]
- type: markdown
id: anything-else
attributes:
value: |
#### Anything else?
- Screen recording, console logs, network requests: You can make a recording with [Loom](https://www.loom.com).
- Anything else that you think could be an issue?

View File

@@ -2,5 +2,10 @@ const baseConfig = require("./packages/config-prettier/prettier-preset");
module.exports = {
...baseConfig,
plugins: ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
plugins: [
"@trivago/prettier-plugin-sort-imports",
"prettier-plugin-tailwindcss",
"prettier-plugin-sort-json",
],
jsonRecursiveSort: true,
};

18
.vscode/launch.json vendored
View File

@@ -1,21 +1,21 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch localhost:3002",
"type": "firefox",
"request": "launch",
"reAttach": true,
"request": "launch",
"type": "firefox",
"url": "http://localhost:3002/",
"webRoot": "${workspaceFolder}"
},
{
"name": "Attach",
"type": "firefox",
"request": "attach"
"request": "attach",
"type": "firefox"
}
]
],
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0"
}

View File

@@ -1,4 +1,4 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.importModuleSpecifier": "non-relative"
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@@ -1,32 +1,32 @@
{
"expo": {
"name": "react-native-demo",
"slug": "react-native-demo",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"jsEngine": "hermes",
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"infoPlist": {
"NSCameraUsageDescription": "Take pictures for certain activities.",
"NSPhotoLibraryUsageDescription": "Select pictures for certain activities.",
"NSMicrophoneUsageDescription": "Need microphone access for recording videos."
}
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
"backgroundColor": "#ffffff",
"foregroundImage": "./assets/adaptive-icon.png"
}
},
"assetBundlePatterns": ["**/*"],
"icon": "./assets/icon.png",
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "Take pictures for certain activities.",
"NSMicrophoneUsageDescription": "Need microphone access for recording videos.",
"NSPhotoLibraryUsageDescription": "Select pictures for certain activities."
},
"supportsTablet": true
},
"jsEngine": "hermes",
"name": "react-native-demo",
"orientation": "portrait",
"slug": "react-native-demo",
"splash": {
"backgroundColor": "#ffffff",
"image": "./assets/splash.png",
"resizeMode": "contain"
},
"userInterfaceStyle": "light",
"version": "1.0.0",
"web": {
"favicon": "./assets/favicon.png"
}

View File

@@ -1,6 +1,6 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
},
"extends": "expo/tsconfig.base"
}

View File

@@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
}
};

View File

@@ -1,5 +1,5 @@
{
"exclude": ["node_modules"],
"extends": "@formbricks/config-typescript/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

View File

@@ -11,6 +11,7 @@ Matrix questions allow respondents to select a value for each option presented i
<SurveyEmbed surveyUrl="https://app.formbricks.com/s/obqeey0574jig4lo2gqyv51e" />
## Elements
<MdxImage
src={Matrix}
alt="Overview of Matrix question type"
@@ -19,18 +20,22 @@ Matrix questions allow respondents to select a value for each option presented i
/>
### Title
Add a clear title to inform the respondent what information you are asking for.
### Description
Provide an optional description with further instructions.
### Rows
Define the options shown on the left side of the matrix. These represent the items for which users will select a value.
### Columns
Represent the range of values from 0 to X (right side of the screen). Users can choose any value, including 0, using radio buttons.
### Select ordering
- Keep current order: This will keep the order of options the same for all respondents.
- Randomize all: This will randomize the options for each respondent.
- Randomize all: This will randomize the options for each respondent.

View File

@@ -1,7 +1,4 @@
{
"extends": "@formbricks/config-typescript/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../packages/types/*.d.ts"],
"exclude": ["../../.env", "node_modules"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
@@ -13,5 +10,8 @@
}
],
"strictNullChecks": true
}
},
"exclude": ["../../.env", "node_modules"],
"extends": "@formbricks/config-typescript/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../packages/types/*.d.ts"]
}

View File

@@ -1,35 +1,36 @@
import { Meta } from "@storybook/blocks";
import Github from "./assets/github.svg";
import Discord from "./assets/discord.svg";
import Youtube from "./assets/youtube.svg";
import Tutorials from "./assets/tutorials.svg";
import Styling from "./assets/styling.png";
import Context from "./assets/context.png";
import Assets from "./assets/assets.png";
import Docs from "./assets/docs.png";
import Share from "./assets/share.png";
import FigmaPlugin from "./assets/figma-plugin.png";
import Testing from "./assets/testing.png";
import Accessibility from "./assets/accessibility.png";
import Theming from "./assets/theming.png";
import AddonLibrary from "./assets/addon-library.png";
import Assets from "./assets/assets.png";
import Context from "./assets/context.png";
import Discord from "./assets/discord.svg";
import Docs from "./assets/docs.png";
import FigmaPlugin from "./assets/figma-plugin.png";
import Github from "./assets/github.svg";
import Share from "./assets/share.png";
import Styling from "./assets/styling.png";
import Testing from "./assets/testing.png";
import Theming from "./assets/theming.png";
import Tutorials from "./assets/tutorials.svg";
import Youtube from "./assets/youtube.svg";
export const RightArrow = () => <svg
viewBox="0 0 14 14"
width="8px"
height="14px"
style={{
marginLeft: '4px',
display: 'inline-block',
shapeRendering: 'inherit',
verticalAlign: 'middle',
fill: 'currentColor',
'path fill': 'currentColor'
}}
>
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
</svg>
export const RightArrow = () => (
<svg
viewBox="0 0 14 14"
width="8px"
height="14px"
style={{
marginLeft: "4px",
display: "inline-block",
shapeRendering: "inherit",
verticalAlign: "middle",
fill: "currentColor",
"path fill": "currentColor",
}}>
<path d="m11.1 7.35-5.5 5.5a.5.5 0 0 1-.7-.7L10.04 7 4.9 1.85a.5.5 0 1 1 .7-.7l5.5 5.5c.2.2.2.5 0 .7Z" />
</svg>
);
<Meta title="Configure your project" />
@@ -38,6 +39,7 @@ export const RightArrow = () => <svg
# Configure your project
Because Storybook works separately from your app, you'll need to configure it for your specific stack and setup. Below, explore guides for configuring Storybook with popular frameworks and tools. If you get stuck, learn how you can ask for help from our community.
</div>
<div className="sb-section">
<div className="sb-section-item">
@@ -84,6 +86,7 @@ export const RightArrow = () => <svg
# Do more with Storybook
Now that you know the basics, let's explore other parts of Storybook that will improve your experience. This list is just to get you started. You can customise Storybook in many ways to fit your needs.
</div>
<div className="sb-section">
@@ -203,6 +206,7 @@ export const RightArrow = () => <svg
target="_blank"
>Discover tutorials<RightArrow /></a>
</div>
</div>
<style>

View File

@@ -1,24 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"allowImportingTsExtensions": true,
"isolatedModules": true,
"jsx": "react-jsx",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"skipLibCheck": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
"target": "ES2020",
"useDefineForClassFields": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]

View File

@@ -1,10 +1,10 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
"skipLibCheck": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,6 +1,7 @@
"use client";
import { ArrowRight } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { cn } from "@formbricks/lib/cn";
@@ -22,8 +23,8 @@ export const ConnectWithFormbricks = ({
widgetSetupCompleted,
channel,
}: ConnectWithFormbricksProps) => {
const t = useTranslations();
const router = useRouter();
const handleFinishOnboarding = async () => {
if (!widgetSetupCompleted) {
router.push(`/environments/${environment.id}/connect/invite`);
@@ -64,8 +65,10 @@ export const ConnectWithFormbricks = ({
)}>
{widgetSetupCompleted ? (
<div>
<p className="text-3xl">Congrats!</p>
<p className="pt-4 text-sm font-medium text-slate-600">Well done! We&apos;re connected.</p>
<p className="text-3xl">{t("environments.connect.congrats")}</p>
<p className="pt-4 text-sm font-medium text-slate-600">
{t("environments.connect.connection_successful_message")}
</p>
</div>
) : (
<div className="flex animate-pulse flex-col items-center space-y-4">
@@ -73,7 +76,9 @@ export const ConnectWithFormbricks = ({
<span className="animate-ping-slow absolute inline-flex h-full w-full rounded-full bg-slate-400 opacity-75"></span>
<span className="relative inline-flex h-10 w-10 rounded-full bg-slate-500"></span>
</span>
<p className="pt-4 text-sm font-medium text-slate-600">Waiting for your signal...</p>
<p className="pt-4 text-sm font-medium text-slate-600">
{t("environments.connect.waiting_for_your_signal")}
</p>
</div>
)}
</div>
@@ -83,7 +88,9 @@ export const ConnectWithFormbricks = ({
variant={widgetSetupCompleted ? "primary" : "minimal"}
onClick={handleFinishOnboarding}
EndIcon={ArrowRight}>
{widgetSetupCompleted ? "Finish Onboarding" : "I don't know how to do it"}
{widgetSetupCompleted
? t("environments.connect.finish_onboarding")
: t("environments.connect.i_dont_know_how_to_do_it")}
</Button>
</div>
);

View File

@@ -2,6 +2,7 @@
import { inviteOrganizationMemberAction } from "@/app/(app)/(onboarding)/organizations/actions";
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";
@@ -24,11 +25,11 @@ type TInviteOrganizationMemberDetails = z.infer<typeof ZInviteOrganizationMember
export const InviteOrganizationMember = ({ organization, environmentId }: InviteOrganizationMemberProps) => {
const router = useRouter();
const t = useTranslations();
const form = useForm<TInviteOrganizationMemberDetails>({
defaultValues: {
email: "",
inviteMessage: "I'm looking into Formbricks to run targeted surveys. Can you help me set it up? 🙏",
inviteMessage: t("environments.connect.invite.invite_message_content"),
},
resolver: zodResolver(ZInviteOrganizationMemberDetails),
});
@@ -63,7 +64,7 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
name="email"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<FormLabel>Email</FormLabel>
<FormLabel>{t("common.email")}</FormLabel>
<FormControl>
<div>
<Input
@@ -83,7 +84,7 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
name="inviteMessage"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<FormLabel>Invite Message</FormLabel>
<FormLabel>{t("environments.connect.invite.invite_message")}</FormLabel>
<FormControl>
<div>
<textarea
@@ -108,10 +109,10 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
e.preventDefault();
finishOnboarding();
}}>
Not now
{t("common.not_now")}
</Button>
<Button id="onboarding-inapp-invite-send-invite" type={"submit"} loading={isSubmitting}>
Invite
{t("common.invite")}
</Button>
</div>
</div>

View File

@@ -1,5 +1,6 @@
"use client";
import { useTranslations } from "next-intl";
import "prismjs/themes/prism.css";
import { useState } from "react";
import toast from "react-hot-toast";
@@ -27,6 +28,7 @@ export const OnboardingSetupInstructions = ({
channel,
widgetSetupCompleted,
}: OnboardingSetupInstructionsProps) => {
const t = useTranslations();
const [activeTab, setActiveTab] = useState(tabs[0].id);
const htmlSnippetForAppSurveys = `<!-- START Formbricks Surveys -->
<script type="text/javascript">
@@ -103,12 +105,12 @@ export const OnboardingSetupInstructions = ({
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="sh">
npm install @formbricks/js
</CodeBlock>
<p>or</p>
<p>{t("common.or")}</p>
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="sh">
yarn add @formbricks/js
</CodeBlock>
<p className="text-sm text-slate-700">
Import Formbricks and initialize the widget in your Component (e.g. App.tsx):
{t("environments.connect.import_formbricks_and_initialize_the_widget_in_your_component")}
</p>
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
{channel === "app" ? npmSnippetForAppSurveys : npmSnippetForWebsiteSurveys}
@@ -119,13 +121,13 @@ export const OnboardingSetupInstructions = ({
variant="secondary"
href={`https://formbricks.com/docs/${channel}-surveys/framework-guides`}
target="_blank">
Read docs
{t("common.read_docs")}
</Button>
</div>
) : activeTab === "html" ? (
<div className="prose prose-slate">
<p className="-mb-1 mt-6 text-sm text-slate-700">
Insert this code into the &lt;head&gt; tag of your website:
{t("environments.connect.insert_this_code_into_the_head_tag_of_your_website")}
</p>
<div>
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
@@ -141,16 +143,16 @@ export const OnboardingSetupInstructions = ({
navigator.clipboard.writeText(
channel === "app" ? htmlSnippetForAppSurveys : htmlSnippetForWebsiteSurveys
);
toast.success("Copied to clipboard");
toast.success(t("common.copied_to_clipboard"));
}}>
Copy code
{t("common.copy_code")}
</Button>
<Button
id="onboarding-inapp-connect-step-by-step-manual"
variant="secondary"
href={`https://formbricks.com/docs/${channel}-surveys/framework-guides#html`}
target="_blank">
Step by step manual
{t("common.step_by_step_manual")}
</Button>
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { InviteOrganizationMember } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/InviteOrganizationMember";
import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound, redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
@@ -15,6 +16,7 @@ interface InvitePageProps {
}
const Page = async ({ params }: InvitePageProps) => {
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
@@ -32,8 +34,8 @@ const Page = async ({ params }: InvitePageProps) => {
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center">
<Header
title="Who is your favorite engineer?"
subtitle="Invite your tech-savvy co-worker to help with the setup."
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>

View File

@@ -1,5 +1,6 @@
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
import { XIcon } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
@@ -13,22 +14,23 @@ interface ConnectPageProps {
}
const Page = async ({ params }: ConnectPageProps) => {
const t = await getTranslations();
const environment = await getEnvironment(params.environmentId);
if (!environment) {
throw new Error("Environment not found");
throw new Error(t("common.environment_not_found"));
}
const product = await getProductByEnvironmentId(environment.id);
if (!product) {
throw new Error("Product not found");
throw new Error(t("common.product_not_found"));
}
const channel = product.config.channel || null;
return (
<div className="flex min-h-full flex-col items-center justify-center py-10">
<Header title={`Let's connect your product with Formbricks`} subtitle="It takes less than 4 minutes." />
<Header title={t("environments.connect.headline")} subtitle={t("environments.connect.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>

View File

@@ -1,9 +1,10 @@
"use client";
import { replacePresetPlaceholders } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils";
import { XMTemplates } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates";
import { getXMTemplates } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates";
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { ActivityIcon, ShoppingCartIcon, SmileIcon, StarIcon, ThumbsUpIcon, UsersIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
@@ -22,7 +23,7 @@ interface XMTemplateListProps {
export const XMTemplateList = ({ product, user, environmentId }: XMTemplateListProps) => {
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
const t = useTranslations();
const router = useRouter();
const createSurvey = async (activeTemplate: TXMTemplate) => {
@@ -46,50 +47,50 @@ export const XMTemplateList = ({ product, user, environmentId }: XMTemplateListP
const handleTemplateClick = (templateIdx) => {
setActiveTemplateId(templateIdx);
const template = XMTemplates[templateIdx];
const template = getXMTemplates(user.locale)[templateIdx];
const newTemplate = replacePresetPlaceholders(template, product);
createSurvey(newTemplate);
};
const XMTemplateOptions = [
{
title: "NPS",
description: "Implement proven best practices to understand WHY people buy.",
title: t("environments.xm-templates.nps"),
description: t("environments.xm-templates.nps_description"),
icon: ShoppingCartIcon,
onClick: () => handleTemplateClick(0),
isLoading: activeTemplateId === 0,
},
{
title: "5-Star Rating",
description: "Universal feedback solution to gauge overall satisfaction.",
title: t("environments.xm-templates.five_star_rating"),
description: t("environments.xm-templates.five_star_rating_description"),
icon: StarIcon,
onClick: () => handleTemplateClick(1),
isLoading: activeTemplateId === 1,
},
{
title: "CSAT",
description: "Implement best practices to measure customer satisfaction.",
title: t("environments.xm-templates.csat"),
description: t("environments.xm-templates.csat_description"),
icon: ThumbsUpIcon,
onClick: () => handleTemplateClick(2),
isLoading: activeTemplateId === 2,
},
{
title: "CES",
description: "Leverage every touchpoint to understand ease of customer interaction.",
title: t("environments.xm-templates.ces"),
description: t("environments.xm-templates.ces_description"),
icon: ActivityIcon,
onClick: () => handleTemplateClick(3),
isLoading: activeTemplateId === 3,
},
{
title: "Smileys",
description: "Use visual indicators to capture feedback across customer touchpoints.",
title: t("environments.xm-templates.smileys"),
description: t("environments.xm-templates.smileys_description"),
icon: SmileIcon,
onClick: () => handleTemplateClick(4),
isLoading: activeTemplateId === 4,
},
{
title: "eNPS",
description: "Universal feedback to understand employee engagement and satisfaction.",
title: t("environments.xm-templates.enps"),
description: t("environments.xm-templates.enps_description"),
icon: UsersIcon,
onClick: () => handleTemplateClick(5),
isLoading: activeTemplateId === 5,

View File

@@ -1,42 +1,43 @@
import { createId } from "@paralleldrive/cuid2";
import { getDefaultEndingCard } from "@formbricks/lib/templates";
import { translate } from "@formbricks/lib/templates";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TXMTemplate } from "@formbricks/types/templates";
export const XMSurveyDefault: TXMTemplate = {
export const getXMSurveyDefault = (locale: string): TXMTemplate => ({
name: "",
endings: [getDefaultEndingCard([])],
endings: [getDefaultEndingCard([], locale)],
questions: [],
styling: {
overwriteThemeStyling: true,
},
};
});
const NPSSurvey = (): TXMTemplate => {
const NPSSurvey = (locale: string): TXMTemplate => {
return {
...XMSurveyDefault,
name: "NPS Survey",
...getXMSurveyDefault(locale),
name: translate("nps_survey_name", locale),
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: { default: "How likely are you to recommend {{productName}} to a friend or colleague?" },
headline: { default: translate("nps_survey_question_1_headline", locale) },
required: true,
lowerLabel: { default: "Not at all likely" },
upperLabel: { default: "Extremely likely" },
lowerLabel: { default: translate("nps_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("nps_survey_question_1_upper_label", locale) },
isColorCodingEnabled: true,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "To help us improve, can you describe the reason(s) for your rating?" },
headline: { default: translate("nps_survey_question_2_headline", locale) },
required: false,
inputType: "text",
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Any other comments, feedback, or concerns?" },
headline: { default: translate("nps_survey_question_3_headline", locale) },
required: false,
inputType: "text",
},
@@ -44,12 +45,12 @@ const NPSSurvey = (): TXMTemplate => {
};
};
const StarRatingSurvey = (): TXMTemplate => {
const StarRatingSurvey = (locale: string): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()];
return {
...XMSurveyDefault,
name: "{{productName}}'s Rating Survey",
...getXMSurveyDefault(locale),
name: translate("star_rating_survey_name", locale),
questions: [
{
id: reusableQuestionIds[0],
@@ -86,15 +87,15 @@ const StarRatingSurvey = (): TXMTemplate => {
],
range: 5,
scale: "number",
headline: { default: "How do you like {{productName}}?" },
headline: { default: translate("star_rating_survey_question_1_headline", locale) },
required: true,
lowerLabel: { default: "Extremely dissatisfied" },
upperLabel: { default: "Extremely satisfied" },
lowerLabel: { default: translate("star_rating_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("star_rating_survey_question_1_upper_label", locale) },
isColorCodingEnabled: false,
},
{
id: reusableQuestionIds[1],
html: { default: '<p class="fb-editor-paragraph" dir="ltr"><span>This helps us a lot.</span></p>' },
html: { default: translate("star_rating_survey_question_2_html", locale) },
type: TSurveyQuestionTypeEnum.CTA,
logic: [
{
@@ -117,15 +118,15 @@ const StarRatingSurvey = (): TXMTemplate => {
{
id: createId(),
objective: "jumpToQuestion",
target: XMSurveyDefault.endings[0].id,
target: getXMSurveyDefault(locale).endings[0].id,
},
],
},
],
headline: { default: "Happy to hear 🙏 Please write a review for us!" },
headline: { default: translate("star_rating_survey_question_2_headline", locale) },
required: true,
buttonUrl: "https://formbricks.com/github",
buttonLabel: { default: "Write review" },
buttonLabel: { default: translate("star_rating_survey_question_2_button_label", locale) },
buttonExternal: true,
},
{
@@ -142,12 +143,12 @@ const StarRatingSurvey = (): TXMTemplate => {
};
};
const CSATSurvey = (): TXMTemplate => {
const CSATSurvey = (locale: string): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()];
return {
...XMSurveyDefault,
name: "{{productName}} CSAT",
...getXMSurveyDefault(locale),
name: translate("csat_survey_name", locale),
questions: [
{
id: reusableQuestionIds[0],
@@ -184,10 +185,10 @@ const CSATSurvey = (): TXMTemplate => {
],
range: 5,
scale: "smiley",
headline: { default: "How satisfied are you with your {{productName}} experience?" },
headline: { default: translate("csat_survey_question_1_headline", locale) },
required: true,
lowerLabel: { default: "Extremely dissatisfied" },
upperLabel: { default: "Extremely satisfied" },
lowerLabel: { default: translate("csat_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("csat_survey_question_1_upper_label", locale) },
isColorCodingEnabled: false,
},
{
@@ -214,62 +215,62 @@ const CSATSurvey = (): TXMTemplate => {
{
id: createId(),
objective: "jumpToQuestion",
target: XMSurveyDefault.endings[0].id,
target: getXMSurveyDefault(locale).endings[0].id,
},
],
},
],
headline: { default: "Lovely! Is there anything we can do to improve your experience?" },
headline: { default: translate("csat_survey_question_2_headline", locale) },
required: false,
placeholder: { default: "Type your answer here..." },
placeholder: { default: translate("csat_survey_question_2_placeholder", locale) },
inputType: "text",
},
{
id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Ugh, sorry! Is there anything we can do to improve your experience?" },
headline: { default: translate("csat_survey_question_3_headline", locale) },
required: false,
placeholder: { default: "Type your answer here..." },
placeholder: { default: translate("csat_survey_question_3_placeholder", locale) },
inputType: "text",
},
],
};
};
const CESSurvey = (): TXMTemplate => {
const CESSurvey = (locale: string): TXMTemplate => {
return {
...XMSurveyDefault,
name: "CES Survey",
...getXMSurveyDefault(locale),
name: translate("cess_survey_name", locale),
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.Rating,
range: 5,
scale: "number",
headline: { default: "{{productName}} makes it easy for me to [ADD GOAL]" },
headline: { default: translate("cess_survey_question_1_headline", locale) },
required: true,
lowerLabel: { default: "Disagree strongly" },
upperLabel: { default: "Agree strongly" },
lowerLabel: { default: translate("cess_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("cess_survey_question_1_upper_label", locale) },
isColorCodingEnabled: false,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Thanks! How could we make it easier for you to [ADD GOAL]?" },
headline: { default: translate("cess_survey_question_2_headline", locale) },
required: true,
placeholder: { default: "Type your answer here..." },
placeholder: { default: translate("cess_survey_question_2_placeholder", locale) },
inputType: "text",
},
],
};
};
const SmileysRatingSurvey = (): TXMTemplate => {
const SmileysRatingSurvey = (locale: string): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()];
return {
...XMSurveyDefault,
name: "Smileys Survey",
...getXMSurveyDefault(locale),
name: translate("smileys_survey_name", locale),
questions: [
{
id: reusableQuestionIds[0],
@@ -306,15 +307,15 @@ const SmileysRatingSurvey = (): TXMTemplate => {
],
range: 5,
scale: "smiley",
headline: { default: "How do you like {{productName}}?" },
headline: { default: translate("smileys_survey_question_1_headline", locale) },
required: true,
lowerLabel: { default: "Not good" },
upperLabel: { default: "Very satisfied" },
lowerLabel: { default: translate("smileys_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("smileys_survey_question_1_upper_label", locale) },
isColorCodingEnabled: false,
},
{
id: reusableQuestionIds[1],
html: { default: '<p class="fb-editor-paragraph" dir="ltr"><span>This helps us a lot.</span></p>' },
html: { default: translate("smileys_survey_question_2_html", locale) },
type: TSurveyQuestionTypeEnum.CTA,
logic: [
{
@@ -337,58 +338,58 @@ const SmileysRatingSurvey = (): TXMTemplate => {
{
id: createId(),
objective: "jumpToQuestion",
target: XMSurveyDefault.endings[0].id,
target: getXMSurveyDefault(locale).endings[0].id,
},
],
},
],
headline: { default: "Happy to hear 🙏 Please write a review for us!" },
headline: { default: translate("smileys_survey_question_2_headline", locale) },
required: true,
buttonUrl: "https://formbricks.com/github",
buttonLabel: { default: "Write review" },
buttonLabel: { default: translate("smileys_survey_question_2_button_label", locale) },
buttonExternal: true,
},
{
id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Sorry to hear! What is ONE thing we can do better?" },
headline: { default: translate("smileys_survey_question_3_headline", locale) },
required: true,
subheader: { default: "Help us improve your experience." },
buttonLabel: { default: "Send" },
placeholder: { default: "Type your answer here..." },
subheader: { default: translate("smileys_survey_question_3_subheader", locale) },
buttonLabel: { default: translate("smileys_survey_question_3_button_label", locale) },
placeholder: { default: translate("smileys_survey_question_3_placeholder", locale) },
inputType: "text",
},
],
};
};
const eNPSSurvey = (): TXMTemplate => {
const eNPSSurvey = (locale: string): TXMTemplate => {
return {
...XMSurveyDefault,
name: "eNPS Survey",
...getXMSurveyDefault(locale),
name: translate("enps_survey_name", locale),
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: {
default: "How likely are you to recommend working at this company to a friend or colleague?",
default: translate("enps_survey_question_1_headline", locale),
},
required: false,
lowerLabel: { default: "Not at all likely" },
upperLabel: { default: "Extremely likely" },
lowerLabel: { default: translate("enps_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("enps_survey_question_1_upper_label", locale) },
isColorCodingEnabled: true,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "To help us improve, can you describe the reason(s) for your rating?" },
headline: { default: translate("enps_survey_question_2_headline", locale) },
required: false,
inputType: "text",
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Any other comments, feedback, or concerns?" },
headline: { default: translate("enps_survey_question_3_headline", locale) },
required: false,
inputType: "text",
},
@@ -396,11 +397,11 @@ const eNPSSurvey = (): TXMTemplate => {
};
};
export const XMTemplates: TXMTemplate[] = [
NPSSurvey(),
StarRatingSurvey(),
CSATSurvey(),
CESSurvey(),
SmileysRatingSurvey(),
eNPSSurvey(),
export const getXMTemplates = (locale: string): TXMTemplate[] => [
NPSSurvey(locale),
StarRatingSurvey(locale),
CSATSurvey(locale),
CESSurvey(locale),
SmileysRatingSurvey(locale),
eNPSSurvey(locale),
];

View File

@@ -1,6 +1,7 @@
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
@@ -18,32 +19,31 @@ interface XMTemplatePageProps {
const Page = async ({ params }: XMTemplatePageProps) => {
const session = await getServerSession(authOptions);
const environment = await getEnvironment(params.environmentId);
const t = await getTranslations();
if (!session) {
throw new Error("Session not found");
throw new Error(t("common.session_not_found"));
}
const user = await getUser(session.user.id);
if (!user) {
throw new Error("User not found");
throw new Error(t("common.user_not_found"));
}
if (!environment) {
throw new Error("Environment not found");
throw new Error(t("common.environment_not_found"));
}
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
const product = await getProductByEnvironmentId(environment.id);
if (!product) {
throw new Error("Product not found");
throw new Error(t("common.product_not_found"));
}
const products = await getProducts(organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header title="What kind of feedback would you like to get?" />
<Header title={t("environments.xm-templates.headline")} />
<XMTemplateList product={product} user={user} environmentId={environment.id} />
{products.length >= 2 && (
<Button

View File

@@ -3,10 +3,10 @@ import { TProductConfigChannel } from "@formbricks/types/product";
export const getCustomHeadline = (channel?: TProductConfigChannel) => {
switch (channel) {
case "website":
return "Let's get the most out of your website traffic!";
return "organizations.products.new.settings.website_channel_headline";
case "app":
return "Let's research what your users need!";
return "organizations.products.new.settings.app_channel_headline";
default:
return "You maintain a product, how exciting!";
return "organizations.products.new.settings.link_channel_headline";
}
};

View File

@@ -1,5 +1,6 @@
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound, redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
@@ -10,6 +11,7 @@ import { AuthorizationError } from "@formbricks/types/errors";
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
const ProductOnboardingLayout = async ({ children, params }) => {
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
@@ -17,7 +19,7 @@ const ProductOnboardingLayout = async ({ children, params }) => {
const user = await getUser(session.user.id);
if (!user) {
throw new Error("User not found");
throw new Error(t("common.user_not_found"));
}
const isAuthorized = await canUserAccessOrganization(session.user.id, params.organizationId);
@@ -30,7 +32,7 @@ const ProductOnboardingLayout = async ({ children, params }) => {
const organization = await getOrganization(params.organizationId);
if (!organization) {
throw new Error("Organization not found");
throw new Error(t("common.organization_not_found"));
}
return (

View File

@@ -1,5 +1,6 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { GlobeIcon, GlobeLockIcon, LinkIcon, XIcon } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { getProducts } from "@formbricks/lib/product/service";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
@@ -11,27 +12,28 @@ interface ChannelPageProps {
}
const Page = async ({ params }: ChannelPageProps) => {
const t = await getTranslations();
const channelOptions = [
{
title: "Public website",
description: "Run well-timed pop-up surveys.",
title: t("organizations.products.new.channel.public_website"),
description: t("organizations.products.new.channel.public_website_description"),
icon: GlobeIcon,
iconText: "Built for scale",
iconText: t("organizations.products.new.channel.public_website_icon_text"),
href: `/organizations/${params.organizationId}/products/new/settings?channel=website`,
},
{
title: "App with sign up",
description: "Run highly-targeted micro-surveys.",
title: t("organizations.products.new.channel.app_with_sign_up"),
description: t("organizations.products.new.channel.app_with_sign_up_description"),
icon: GlobeLockIcon,
iconText: "Enrich user profiles",
iconText: t("organizations.products.new.channel.app_with_sign_up_icon_text"),
href: `/organizations/${params.organizationId}/products/new/settings?channel=app`,
},
{
channel: "link",
title: "Link & email surveys",
description: "Reach people anywhere online.",
title: t("organizations.products.new.channel.link_and_email_surveys"),
description: t("organizations.products.new.channel.link_and_email_surveys_description"),
icon: LinkIcon,
iconText: "Anywhere online",
iconText: t("organizations.products.new.channel.link_and_email_surveys_icon_text"),
href: `/organizations/${params.organizationId}/products/new/settings?channel=link`,
},
];
@@ -41,8 +43,8 @@ const Page = async ({ params }: ChannelPageProps) => {
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header
title="Where do you mainly want to survey people?"
subtitle="Run surveys on public websites, in your app, or with shareable links & emails."
title={t("organizations.products.new.channel.channel_select_title")}
subtitle={t("organizations.products.new.channel.channel_select_subtitle")}
/>
<OnboardingOptionsContainer options={channelOptions} />
{products.length >= 1 && (

View File

@@ -1,69 +0,0 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { HeartIcon, MonitorIcon, ShoppingCart, XIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { getProducts } from "@formbricks/lib/product/service";
import { TProductConfigChannel } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
interface IndustryPageProps {
params: {
organizationId: string;
};
searchParams: {
channel?: TProductConfigChannel;
};
}
const Page = async ({ params, searchParams }: IndustryPageProps) => {
const channel = searchParams.channel;
if (!channel) {
return notFound();
}
const products = await getProducts(params.organizationId);
const industryOptions = [
{
title: "E-Commerce",
description: "Understand why people buy.",
icon: ShoppingCart,
iconText: "B2B and B2C",
href: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=eCommerce`,
},
{
title: "SaaS",
description: "Improve product-market fit.",
icon: MonitorIcon,
iconText: "Proven methods",
href: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=saas`,
},
{
title: "Other",
description: "Listen to your customers.",
icon: HeartIcon,
iconText: "Customer insights",
href: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=other`,
},
];
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header
title="Which industry do you work for?"
subtitle="Get started with battle-tested best practices."
/>
<OnboardingOptionsContainer options={industryOptions} />
{products.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
href={"/"}>
<XIcon className="h-7 w-7" strokeWidth={1.5} />
</Button>
)}
</div>
);
};
export default Page;

View File

@@ -1,5 +1,6 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
import { getTranslations } from "next-intl/server";
import { getProducts } from "@formbricks/lib/product/service";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
@@ -11,16 +12,17 @@ interface ModePageProps {
}
const Page = async ({ params }: ModePageProps) => {
const t = await getTranslations();
const channelOptions = [
{
title: "Formbricks Surveys",
description: "Multi-purpose survey platform for web, app and email surveys.",
title: t("organizations.products.new.mode.formbricks_surveys"),
description: t("organizations.products.new.mode.formbricks_surveys_description"),
icon: ListTodoIcon,
href: `/organizations/${params.organizationId}/products/new/channel`,
},
{
title: "Formbricks CX",
description: "Surveys and reports to understand what your customers need.",
title: t("organizations.products.new.mode.formbricks_cx"),
description: t("organizations.products.new.mode.formbricks_cx_description"),
icon: HeartIcon,
href: `/organizations/${params.organizationId}/products/new/settings?mode=cx`,
},
@@ -30,7 +32,7 @@ const Page = async ({ params }: ModePageProps) => {
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header title="What are you here for?" />
<Header title={t("organizations.products.new.mode.what_are_you_here_for")} />
<OnboardingOptionsContainer options={channelOptions} />
{products.length >= 1 && (
<Button

View File

@@ -2,13 +2,14 @@
import { createProductAction } from "@/app/(app)/environments/[environmentId]/actions";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
import { PREVIEW_SURVEY } from "@formbricks/lib/styling/constants";
import { getPreviewSurvey } from "@formbricks/lib/styling/constants";
import {
TProductConfigChannel,
TProductConfigIndustry,
@@ -36,6 +37,7 @@ interface ProductSettingsProps {
channel: TProductConfigChannel;
industry: TProductConfigIndustry;
defaultBrandColor: string;
locale: string;
}
export const ProductSettings = ({
@@ -44,9 +46,10 @@ export const ProductSettings = ({
channel,
industry,
defaultBrandColor,
locale,
}: ProductSettingsProps) => {
const router = useRouter();
const t = useTranslations();
const addProduct = async (data: TProductUpdateInput) => {
try {
const createProductResponse = await createProductAction({
@@ -107,8 +110,10 @@ export const ProductSettings = ({
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<div>
<FormLabel>Brand color</FormLabel>
<FormDescription>Match the main color of surveys with your brand.</FormDescription>
<FormLabel>{t("organizations.products.new.settings.brand_color")}</FormLabel>
<FormDescription>
{t("organizations.products.new.settings.brand_color_description")}
</FormDescription>
</div>
<FormControl>
<div>
@@ -129,8 +134,10 @@ export const ProductSettings = ({
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<div>
<FormLabel>Product name</FormLabel>
<FormDescription>What is your product called?</FormDescription>
<FormLabel>{t("organizations.products.new.settings.product_name")}</FormLabel>
<FormDescription>
{t("organizations.products.new.settings.product_name_description")}
</FormDescription>
</div>
<FormControl>
<div>
@@ -150,7 +157,7 @@ export const ProductSettings = ({
<div className="flex w-full justify-end">
<Button loading={isSubmitting} type="submit">
Next
{t("common.next")}
</Button>
</div>
</form>
@@ -167,10 +174,10 @@ export const ProductSettings = ({
className="absolute left-2 top-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
/>
)}
<p className="text-sm text-slate-400">Preview</p>
<p className="text-sm text-slate-400">{t("common.preview")}</p>
<div className="h-3/4 w-3/4">
<SurveyInline
survey={PREVIEW_SURVEY}
survey={getPreviewSurvey(locale)}
styling={{ brandColor: { light: brandColor } }}
isBrandingEnabled={false}
languageCode="default"

View File

@@ -1,8 +1,12 @@
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
import { ProductSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings";
import { XIcon } from "lucide-react";
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { authOptions } from "@formbricks/lib/authOptions";
import { DEFAULT_BRAND_COLOR, DEFAULT_LOCALE } from "@formbricks/lib/constants";
import { getProducts } from "@formbricks/lib/product/service";
import { getUserLocale } from "@formbricks/lib/user/service";
import { TProductConfigChannel, TProductConfigIndustry, TProductMode } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/components/Button";
import { Header } from "@formbricks/ui/components/Header";
@@ -19,10 +23,12 @@ interface ProductSettingsPageProps {
}
const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
const t = await getTranslations();
const session = await getServerSession(authOptions);
const channel = searchParams.channel || null;
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 products = await getProducts(params.organizationId);
@@ -30,13 +36,13 @@ const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
{channel === "link" || mode === "cx" ? (
<Header
title="Match your brand, get 2x more responses."
subtitle="When people recognize your brand, they are much more likely to start and complete responses."
title={t("organizations.products.new.settings.channel_settings_title")}
subtitle={t("organizations.products.new.settings.channel_settings_subtitle")}
/>
) : (
<Header
title={customHeadline}
subtitle="Get 2x more responses matching surveys with your brand and UI"
title={t(customHeadline)}
subtitle={t("organizations.products.new.settings.channel_settings_description")}
/>
)}
<ProductSettings
@@ -45,6 +51,7 @@ const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
channel={channel}
industry={industry}
defaultBrandColor={DEFAULT_BRAND_COLOR}
locale={locale ?? DEFAULT_LOCALE}
/>
{products.length >= 1 && (
<Button

View File

@@ -46,7 +46,8 @@ export const inviteOrganizationMemberAction = authenticatedActionClient
ctx.user.name ?? "",
"",
true, // is onboarding invite
parsedInput.inviteMessage
parsedInput.inviteMessage,
ctx.user.locale
);
}

View File

@@ -2,6 +2,7 @@ import { FormbricksClient } from "@/app/(app)/components/FormbricksClient";
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
@@ -13,6 +14,7 @@ import { DevEnvironmentBanner } from "@formbricks/ui/components/DevEnvironmentBa
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
const SurveyEditorEnvironmentLayout = async ({ children, params }) => {
const t = await getTranslations();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
@@ -20,23 +22,23 @@ const SurveyEditorEnvironmentLayout = async ({ children, params }) => {
const user = await getUser(session.user.id);
if (!user) {
throw new Error("User not found");
throw new Error(t("common.user_not_found"));
}
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
if (!hasAccess) {
throw new AuthorizationError("Not authorized");
throw new AuthorizationError(t("common.not_authorized"));
}
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!organization) {
throw new Error("Organization not found");
throw new Error(t("common.organization_not_found"));
}
const environment = await getEnvironment(params.environmentId);
if (!environment) {
throw new Error("Environment not found");
throw new Error(t("common.environment_not_found"));
}
return (

View File

@@ -1,5 +1,6 @@
"use client";
import { useTranslations } from "next-intl";
import { TActionClass } from "@formbricks/types/action-classes";
import { TSurvey } from "@formbricks/types/surveys/types";
import { ModalWithTabs } from "@formbricks/ui/components/ModalWithTabs";
@@ -27,9 +28,10 @@ export const AddActionModal = ({
isViewer,
environmentId,
}: AddActionModalProps) => {
const t = useTranslations();
const tabs = [
{
title: "Select saved action",
title: t("environments.surveys.edit.select_saved_action"),
children: (
<SavedActionsTab
actionClasses={actionClasses}
@@ -40,7 +42,7 @@ export const AddActionModal = ({
),
},
{
title: "Capture new action",
title: t("environments.surveys.edit.capture_new_action"),
children: (
<CreateNewActionTab
actionClasses={actionClasses}
@@ -55,8 +57,8 @@ export const AddActionModal = ({
];
return (
<ModalWithTabs
label="Add action"
description="Capture a new action to trigger a survey on."
label={t("common.add_action")}
description={t("environments.surveys.edit.capture_a_new_action_to_trigger_a_survey_on")}
open={open}
setOpen={setOpen}
tabs={tabs}

View File

@@ -1,6 +1,7 @@
"use client";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { TSurvey } from "@formbricks/types/surveys/types";
interface AddEndingCardButtonProps {
@@ -10,6 +11,7 @@ interface AddEndingCardButtonProps {
}
export const AddEndingCardButton = ({ localSurvey, addEndingCard }: AddEndingCardButtonProps) => {
const t = useTranslations();
return (
<div
className="group inline-flex rounded-lg border border-slate-300 bg-slate-50 hover:cursor-pointer hover:bg-white"
@@ -18,7 +20,7 @@ export const AddEndingCardButton = ({ localSurvey, addEndingCard }: AddEndingCar
<PlusIcon className="h-6 w-6 text-white" />
</div>
<div className="px-4 py-3 text-sm">
<p className="font-semibold">Add ending</p>
<p className="font-semibold">{t("environments.surveys.edit.add_ending")}</p>
</div>
</div>
);

View File

@@ -4,12 +4,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import * as Collapsible from "@radix-ui/react-collapsible";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import {
CXQuestionTypes,
getCXQuestionTypes,
getQuestionDefaults,
questionTypes,
getQuestionTypes,
universalQuestionPresets,
} from "@formbricks/lib/utils/questions";
import { TProduct } from "@formbricks/types/product";
@@ -18,15 +19,16 @@ interface AddQuestionButtonProps {
addQuestion: (question: any) => void;
product: TProduct;
isCxMode: boolean;
locale: string;
}
export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestionButtonProps) => {
export const AddQuestionButton = ({ addQuestion, product, isCxMode, locale }: AddQuestionButtonProps) => {
const t = useTranslations();
const [open, setOpen] = useState(false);
const [hoveredQuestionId, setHoveredQuestionId] = useState<string | null>(null);
const availableQuestionTypes = isCxMode ? getCXQuestionTypes(locale) : getQuestionTypes(locale);
const [parent] = useAutoAnimate();
const availableQuestionTypes = isCxMode ? CXQuestionTypes : questionTypes;
return (
<Collapsible.Root
open={open}
@@ -41,8 +43,10 @@ export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestio
<PlusIcon className="h-5 w-5 text-white" />
</div>
<div className="px-4 py-3">
<p className="text-sm font-semibold">Add question</p>
<p className="mt-1 text-xs text-slate-500">Add a new question to your survey</p>
<p className="text-sm font-semibold">{t("environments.surveys.edit.add_question")}</p>
<p className="mt-1 text-xs text-slate-500">
{t("environments.surveys.edit.add_a_new_question_to_your_survey")}
</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
@@ -56,7 +60,7 @@ export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestio
onClick={() => {
addQuestion({
...universalQuestionPresets,
...getQuestionDefaults(questionType.id, product),
...getQuestionDefaults(questionType.id, product, locale),
id: createId(),
type: questionType.id,
});

View File

@@ -2,10 +2,12 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Button } from "@formbricks/ui/components/Button";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { QuestionToggleTable } from "@formbricks/ui/components/QuestionToggleTable";
@@ -20,6 +22,7 @@ interface AddressQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const AddressQuestionForm = ({
@@ -31,38 +34,39 @@ export const AddressQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: AddressQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
const t = useTranslations();
const fields = [
{
id: "addressLine1",
label: "Address Line 1",
label: t("environments.surveys.edit.address_line_1"),
...question.addressLine1,
},
{
id: "addressLine2",
label: "Address Line 2",
label: t("environments.surveys.edit.address_line_2"),
...question.addressLine2,
},
{
id: "city",
label: "City",
label: t("environments.surveys.edit.city"),
...question.city,
},
{
id: "state",
label: "State",
label: t("environments.surveys.edit.state"),
...question.state,
},
{
id: "zip",
label: "Zip",
label: t("environments.surveys.edit.zip"),
...question.zip,
},
{
id: "country",
label: "Country",
label: t("environments.surveys.edit.country"),
...question.country,
},
];
@@ -98,7 +102,7 @@ export const AddressQuestionForm = ({
<QuestionFormInput
id="headline"
value={question.headline}
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -106,6 +110,7 @@ export const AddressQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
@@ -115,7 +120,7 @@ export const AddressQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={"Description"}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -123,6 +128,7 @@ export const AddressQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -139,7 +145,7 @@ export const AddressQuestionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
{t("environments.surveys.edit.add_description")}
</Button>
)}

View File

@@ -3,6 +3,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { TProductStyling } from "@formbricks/types/product";
@@ -33,6 +34,7 @@ export const BackgroundStylingCard = ({
isUnsplashConfigured,
form,
}: BackgroundStylingCardProps) => {
const t = useTranslations();
const [parent] = useAutoAnimate();
return (
@@ -65,12 +67,12 @@ export const BackgroundStylingCard = ({
<div className="flex flex-col">
<div className="flex items-center gap-2">
<p className={cn("font-semibold text-slate-800", isSettingsPage ? "text-sm" : "text-base")}>
Background Styling
{t("environments.surveys.edit.background_styling")}
</p>
{isSettingsPage && <Badge text="Link Surveys" type="gray" size="normal" />}
{isSettingsPage && <Badge text={t("common.link_surveys")} type="gray" size="normal" />}
</div>
<p className={cn("mt-1 text-slate-500", isSettingsPage ? "text-xs" : "text-sm")}>
Change the background to a color, image or animation.
{t("environments.surveys.edit.change_the_background_to_a_color_image_or_animation")}
</p>
</div>
</div>
@@ -84,8 +86,10 @@ export const BackgroundStylingCard = ({
render={({ field }) => (
<FormItem>
<div>
<FormLabel>Change background</FormLabel>
<FormDescription>Pick a background from our library or upload your own.</FormDescription>
<FormLabel>{t("environments.surveys.edit.change_background")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.pick_a_background_from_our_library_or_upload_your_own")}
</FormDescription>
</div>
<FormControl>
@@ -118,8 +122,10 @@ export const BackgroundStylingCard = ({
render={({ field }) => (
<FormItem>
<div>
<FormLabel>Brightness</FormLabel>
<FormDescription>Darken or lighten background of your choice.</FormDescription>
<FormLabel>{t("environments.surveys.edit.brightness")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.darken_or_lighten_background_of_your_choice")}
</FormDescription>
</div>
<FormControl>

View File

@@ -1,10 +1,12 @@
"use client";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
@@ -13,9 +15,9 @@ import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
const options = [
{
value: "internal",
label: "Button to continue in survey",
label: "environments.surveys.edit.button_to_continue_in_survey",
},
{ value: "external", label: "Button to link to external URL" },
{ value: "external", label: "environments.surveys.edit.button_to_link_to_external_url" },
];
interface CTAQuestionFormProps {
@@ -28,6 +30,7 @@ interface CTAQuestionFormProps {
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const CTAQuestionForm = ({
@@ -40,15 +43,17 @@ export const CTAQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: CTAQuestionFormProps): JSX.Element => {
const [firstRender, setFirstRender] = useState(true);
const t = useTranslations();
const [parent] = useAutoAnimate();
return (
<form ref={parent}>
<QuestionFormInput
id="headline"
value={question.headline}
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -56,10 +61,11 @@ export const CTAQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div className="mt-3">
<Label htmlFor="subheader">Description</Label>
<Label htmlFor="subheader">{t("common.description")}</Label>
<div className="mt-2">
<LocalizedEditor
id="subheader"
@@ -72,6 +78,7 @@ export const CTAQuestionForm = ({
firstRender={firstRender}
setFirstRender={setFirstRender}
questionIdx={questionIdx}
locale={locale}
/>
</div>
</div>
@@ -88,23 +95,24 @@ export const CTAQuestionForm = ({
<QuestionFormInput
id="buttonLabel"
value={question.buttonLabel}
label={`"Next" Button Label`}
label={t("environments.surveys.edit.next_button_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={lastQuestion ? "Finish" : "Next"}
placeholder={lastQuestion ? t("common.finish") : t("common.next")}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
{questionIdx !== 0 && (
<QuestionFormInput
id="backButtonLabel"
value={question.backButtonLabel}
label={`"Back" Button Label`}
label={t("environments.surveys.edit.back_button_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
@@ -114,6 +122,7 @@ export const CTAQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
)}
</div>
@@ -121,7 +130,7 @@ export const CTAQuestionForm = ({
{question.buttonExternal && (
<div className="mt-3 flex-1">
<Label htmlFor="buttonLabel">Button URL</Label>
<Label htmlFor="buttonLabel">{t("environments.surveys.edit.button_url")}</Label>
<div className="mt-2">
<Input
id="buttonUrl"
@@ -139,7 +148,7 @@ export const CTAQuestionForm = ({
<QuestionFormInput
id="dismissButtonLabel"
value={question.dismissButtonLabel}
label={"Skip Button Label"}
label={t("environments.surveys.edit.skip_button_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
placeholder={"skip"}
@@ -148,6 +157,7 @@ export const CTAQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
)}

View File

@@ -1,8 +1,10 @@
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { AdvancedOptionToggle } from "@formbricks/ui/components/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/components/Button";
import { Input } from "@formbricks/ui/components/Input";
@@ -19,6 +21,7 @@ interface CalQuestionFormProps {
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const CalQuestionForm = ({
@@ -30,10 +33,11 @@ export const CalQuestionForm = ({
setSelectedLanguageCode,
isInvalid,
attributeClasses,
locale,
}: CalQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const [isCalHostEnabled, setIsCalHostEnabled] = useState(!!question.calHost);
const t = useTranslations();
useEffect(() => {
if (!isCalHostEnabled) {
updateQuestion(questionIdx, { calHost: undefined });
@@ -49,7 +53,7 @@ export const CalQuestionForm = ({
<QuestionFormInput
id="headline"
value={question.headline}
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -57,6 +61,7 @@ export const CalQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div>
{question.subheader !== undefined && (
@@ -65,7 +70,7 @@ export const CalQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={"Description"}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -73,6 +78,7 @@ export const CalQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -90,12 +96,12 @@ export const CalQuestionForm = ({
}}>
{" "}
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
{t("environments.surveys.edit.add_description")}
</Button>
)}
<div className="mt-3 flex flex-col gap-6">
<div className="flex flex-col gap-3">
<Label htmlFor="calUserName">Cal.com username or username/event</Label>
<Label htmlFor="calUserName">{t("environments.surveys.edit.cal_username")}</Label>
<div>
<Input
id="calUserName"
@@ -110,13 +116,13 @@ export const CalQuestionForm = ({
isChecked={isCalHostEnabled}
onToggle={(checked: boolean) => setIsCalHostEnabled(checked)}
htmlId="calHost"
description="Needed for a self-hosted Cal.com instance"
description={t("environments.surveys.edit.needed_for_self_hosted_cal_com_instance")}
childBorder
title="Custom hostname"
title={t("environments.surveys.edit.custom_hostname")}
customContainerClass="p-0">
<div className="p-4">
<div className="flex items-center gap-2">
<Label htmlFor="calHost">Hostname</Label>
<Label htmlFor="calHost">{t("environments.surveys.edit.hostname")}</Label>
<Input
id="calHost"
name="calHost"

View File

@@ -3,6 +3,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import React from "react";
import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
@@ -35,6 +36,7 @@ export const CardStylingSettings = ({
setOpen,
form,
}: CardStylingSettingsProps) => {
const t = useTranslations();
const isAppSurvey = surveyType === "app";
const surveyTypeDerived = isAppSurvey ? "App" : "Link";
const isLogoVisible = !!product.logo?.url;
@@ -71,10 +73,10 @@ export const CardStylingSettings = ({
<div>
<p className={cn("font-semibold text-slate-800", isSettingsPage ? "text-sm" : "text-base")}>
Card Styling
{t("environments.surveys.edit.card_styling")}
</p>
<p className={cn("mt-1 text-slate-500", isSettingsPage ? "text-xs" : "text-sm")}>
Style the survey card.
{t("environments.surveys.edit.style_the_survey_card")}
</p>
</div>
</div>
@@ -91,8 +93,10 @@ export const CardStylingSettings = ({
render={() => (
<FormItem>
<div>
<FormLabel>Roundness</FormLabel>
<FormDescription>Change the border radius of the card and the inputs.</FormDescription>
<FormLabel>{t("environments.surveys.edit.roundness")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_border_radius_of_the_card_and_the_inputs")}
</FormDescription>
</div>
<FormControl>
@@ -117,8 +121,10 @@ export const CardStylingSettings = ({
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>Card background color</FormLabel>
<FormDescription>Change the background color of the card.</FormDescription>
<FormLabel>{t("environments.surveys.edit.card_background_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_background_color_of_the_card")}
</FormDescription>
</div>
<FormControl>
@@ -138,8 +144,10 @@ export const CardStylingSettings = ({
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>Card border color</FormLabel>
<FormDescription>Change the border color of the card.</FormDescription>
<FormLabel>{t("environments.surveys.edit.card_border_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_border_color_of_the_card")}
</FormDescription>
</div>
<FormControl>
@@ -159,8 +167,10 @@ export const CardStylingSettings = ({
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>Card shadow color</FormLabel>
<FormDescription>Change the shadow color of the card.</FormDescription>
<FormLabel>{t("environments.surveys.edit.card_shadow_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_shadow_color_of_the_card")}
</FormDescription>
</div>
<FormControl>
@@ -180,10 +190,18 @@ export const CardStylingSettings = ({
render={() => (
<FormItem>
<div>
<FormLabel>Card Arrangement for {surveyTypeDerived} Surveys</FormLabel>
<FormLabel>
{t("environments.surveys.edit.card_arrangement_for_survey_type_derived", {
surveyTypeDerived: surveyTypeDerived,
})}
</FormLabel>
<FormDescription>
How funky do you want your cards in {surveyTypeDerived} Surveys
{t(
"environments.surveys.edit.how_funky_do_you_want_your_cards_in_survey_type_derived_surveys",
{
surveyTypeDerived: surveyTypeDerived,
}
)}
</FormDescription>
</div>
<FormControl>
@@ -216,8 +234,10 @@ export const CardStylingSettings = ({
</FormControl>
<div>
<FormLabel>Hide progress bar</FormLabel>
<FormDescription>Disable the visibility of survey progress.</FormDescription>
<FormLabel>{t("environments.surveys.edit.hide_progress_bar")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.disable_the_visibility_of_survey_progress")}
</FormDescription>
</div>
</FormItem>
)}
@@ -241,10 +261,12 @@ export const CardStylingSettings = ({
<div>
<FormLabel>
Hide logo
<Badge text="Link Surveys" type="gray" size="normal" />
{t("environments.surveys.edit.hide_logo")}
<Badge text={t("common.link_surveys")} type="gray" size="normal" />
</FormLabel>
<FormDescription>Hides the logo in this specific survey</FormDescription>
<FormDescription>
{t("environments.surveys.edit.hide_the_logo_in_this_specific_survey")}
</FormDescription>
</div>
</FormItem>
)}
@@ -279,9 +301,9 @@ export const CardStylingSettings = ({
</FormControl>
<div>
<FormLabel>Add highlight border</FormLabel>
<FormLabel>{t("environments.surveys.edit.add_highlight_border")}</FormLabel>
<FormDescription className="text-xs font-normal text-slate-500">
Add an outer border to your survey card.
{t("environments.surveys.edit.add_highlight_border_description")}
</FormDescription>
</div>
</div>

View File

@@ -14,6 +14,7 @@ import {
SplitIcon,
TrashIcon,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { duplicateLogicItem } from "@formbricks/lib/surveyLogic/utils";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
@@ -43,6 +44,7 @@ export function ConditionalLogic({
questionIdx,
updateQuestion,
}: ConditionalLogicProps) {
const t = useTranslations();
const transformedSurvey = useMemo(() => {
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default", attributeClasses);
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default", attributeClasses);
@@ -117,7 +119,7 @@ export function ConditionalLogic({
return (
<div className="mt-4" ref={parent}>
<Label className="flex gap-2">
Conditional Logic
{t("environments.surveys.edit.conditional_logic")}
<SplitIcon className="h-4 w-4 rotate-90" />
</Label>
@@ -147,7 +149,7 @@ export function ConditionalLogic({
duplicateLogic(logicItemIdx);
}}
icon={<CopyIcon className="h-4 w-4" />}>
Duplicate
{t("common.duplicate")}
</DropdownMenuItem>
<DropdownMenuItem
disabled={logicItemIdx === 0}
@@ -155,7 +157,7 @@ export function ConditionalLogic({
moveLogic(logicItemIdx, logicItemIdx - 1);
}}
icon={<ArrowUpIcon className="h-4 w-4" />}>
Move up
{t("common.move_up")}
</DropdownMenuItem>
<DropdownMenuItem
disabled={logicItemIdx === (question.logic ?? []).length - 1}
@@ -163,14 +165,14 @@ export function ConditionalLogic({
moveLogic(logicItemIdx, logicItemIdx + 1);
}}
icon={<ArrowDownIcon className="h-4 w-4" />}>
Move down
{t("common.move_down")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
handleRemoveLogic(logicItemIdx);
}}
icon={<TrashIcon className="h-4 w-4" />}>
Remove
{t("common.remove")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -189,7 +191,7 @@ export function ConditionalLogic({
variant="secondary"
EndIcon={PlusIcon}
onClick={addLogic}>
Add logic
{t("environments.surveys.edit.add_logic")}
</Button>
</div>
</div>

View File

@@ -1,9 +1,11 @@
"use client";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
@@ -16,6 +18,7 @@ interface ConsentQuestionFormProps {
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const ConsentQuestionForm = ({
@@ -27,14 +30,15 @@ export const ConsentQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: ConsentQuestionFormProps): JSX.Element => {
const [firstRender, setFirstRender] = useState(true);
const t = useTranslations();
return (
<form>
<QuestionFormInput
id="headline"
label="Question*"
label={t("environments.surveys.edit.question") + "*"}
value={question.headline}
localSurvey={localSurvey}
questionIdx={questionIdx}
@@ -43,10 +47,11 @@ export const ConsentQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div className="mt-3">
<Label htmlFor="subheader">Description</Label>
<Label htmlFor="subheader">{t("common.description")}</Label>
<div className="mt-2">
<LocalizedEditor
id="subheader"
@@ -59,13 +64,14 @@ export const ConsentQuestionForm = ({
firstRender={firstRender}
setFirstRender={setFirstRender}
questionIdx={questionIdx}
locale={locale}
/>
</div>
</div>
<QuestionFormInput
id="label"
label="Checkbox Label*"
label={t("environments.surveys.edit.checkbox_label") + "*"}
placeholder="I agree to the terms and conditions"
value={question.label}
localSurvey={localSurvey}
@@ -75,6 +81,7 @@ export const ConsentQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</form>
);

View File

@@ -2,10 +2,12 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyContactInfoQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Button } from "@formbricks/ui/components/Button";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { QuestionToggleTable } from "@formbricks/ui/components/QuestionToggleTable";
@@ -20,6 +22,7 @@ interface ContactInfoQuestionFormProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (language: string) => void;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const ContactInfoQuestionForm = ({
@@ -31,33 +34,35 @@ export const ContactInfoQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: ContactInfoQuestionFormProps): JSX.Element => {
const t = useTranslations();
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
const fields = [
{
id: "firstName",
label: "First Name",
label: t("environments.surveys.edit.first_name"),
...question.firstName,
},
{
id: "lastName",
label: "Last Name",
label: t("environments.surveys.edit.last_name"),
...question.lastName,
},
{
id: "email",
label: "Email",
label: t("common.email"),
...question.email,
},
{
id: "phone",
label: "Phone",
label: t("common.phone"),
...question.phone,
},
{
id: "company",
label: "Company",
label: t("environments.surveys.edit.company"),
...question.company,
},
];
@@ -87,7 +92,7 @@ export const ContactInfoQuestionForm = ({
<QuestionFormInput
id="headline"
value={question.headline}
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -95,6 +100,7 @@ export const ContactInfoQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
@@ -104,7 +110,7 @@ export const ContactInfoQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={"Description"}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -112,6 +118,7 @@ export const ContactInfoQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -128,7 +135,7 @@ export const ContactInfoQuestionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
{t("environments.surveys.edit.add_description")}
</Button>
)}

View File

@@ -1,5 +1,6 @@
import { isValidCssSelector } from "@/app/lib/actionClass/actionClass";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { FormProvider, useForm } from "react-hook-form";
import toast from "react-hot-toast";
@@ -37,6 +38,7 @@ export const CreateNewActionTab = ({
setLocalSurvey,
environmentId,
}: CreateNewActionTabProps) => {
const t = useTranslations();
const actionClassNames = useMemo(
() => actionClasses.map((actionClass) => actionClass.name),
[actionClasses]
@@ -63,7 +65,7 @@ export const CreateNewActionTab = ({
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["name"],
message: `Action with name ${data.name} already exists`,
message: t("environments.actions.action_with_name_already_exists", { name: data.name }),
});
}
})
@@ -86,15 +88,15 @@ export const CreateNewActionTab = ({
const { type } = data;
try {
if (isViewer) {
throw new Error("You are not authorised to perform this action.");
throw new Error(t("common.you_are_not_authorised_to_perform_this_action"));
}
if (data.name && actionClassNames.includes(data.name)) {
throw new Error(`Action with name ${data.name} already exist`);
throw new Error(t("environments.actions.action_with_name_already_exists", { name: data.name }));
}
if (type === "code" && data.key && actionClassKeys.includes(data.key)) {
throw new Error(`Action with key ${data.key} already exist`);
throw new Error(t("environments.actions.action_with_key_already_exists", { key: data.key }));
}
if (
@@ -156,7 +158,7 @@ export const CreateNewActionTab = ({
reset();
resetAllStates();
toast.success("Action created successfully");
toast.success(t("environments.actions.action_created_successfully"));
} catch (e: any) {
toast.error(e.message);
}
@@ -178,12 +180,12 @@ export const CreateNewActionTab = ({
control={control}
render={({ field }) => (
<div>
<Label className="font-semibold">Action Type</Label>
<Label className="font-semibold">{t("environments.actions.action_type")}</Label>
<TabToggle
id="type"
options={[
{ value: "noCode", label: "No code" },
{ value: "code", label: "Code" },
{ value: "noCode", label: t("common.no_code") },
{ value: "code", label: t("common.code") },
]}
{...field}
defaultSelected={field.value}
@@ -200,14 +202,16 @@ export const CreateNewActionTab = ({
name="name"
render={({ field, fieldState: { error } }) => (
<FormItem>
<FormLabel htmlFor="actionNameInput">What did your user do?</FormLabel>
<FormLabel htmlFor="actionNameInput">
{t("environments.actions.what_did_your_user_do")}
</FormLabel>
<FormControl>
<Input
type="text"
id="actionNameInput"
{...field}
placeholder="E.g. Clicked Download"
placeholder={t("environments.actions.eg_clicked_download")}
isInvalid={!!error?.message}
/>
</FormControl>
@@ -223,14 +227,14 @@ export const CreateNewActionTab = ({
name="description"
render={({ field }) => (
<FormItem>
<FormLabel htmlFor="actionDescriptionInput">Description</FormLabel>
<FormLabel htmlFor="actionDescriptionInput">{t("common.description")}</FormLabel>
<FormControl>
<Input
type="text"
id="actionDescriptionInput"
{...field}
placeholder="User clicked Download Button"
placeholder={t("environments.actions.eg_user_clicked_download_button")}
value={field.value ?? ""}
/>
</FormControl>
@@ -251,10 +255,10 @@ export const CreateNewActionTab = ({
<div className="flex justify-end pt-6">
<div className="flex space-x-2">
<Button type="button" variant="minimal" onClick={resetAllStates}>
Cancel
{t("common.cancel")}
</Button>
<Button type="submit" loading={isSubmitting}>
Create action
{t("environments.actions.create_action")}
</Button>
</div>
</div>

View File

@@ -1,8 +1,10 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label";
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
@@ -18,6 +20,7 @@ interface IDateQuestionFormProps {
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
const dateOptions = [
@@ -44,15 +47,17 @@ export const DateQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: IDateQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const t = useTranslations();
const [parent] = useAutoAnimate();
return (
<form>
<QuestionFormInput
id="headline"
value={question.headline}
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -60,6 +65,7 @@ export const DateQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
{question.subheader !== undefined && (
@@ -68,7 +74,7 @@ export const DateQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={"Description"}
label={t("environments.surveys.edit.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -76,6 +82,7 @@ export const DateQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -93,13 +100,13 @@ export const DateQuestionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
{t("environments.surveys.edit.add_description")}
</Button>
)}
</div>
<div className="mt-3">
<Label htmlFor="questionType">Date Format</Label>
<Label htmlFor="questionType">{t("environments.surveys.edit.date_format")}</Label>
<div className="mt-2 flex items-center">
<OptionsSwitch
options={dateOptions}

View File

@@ -9,6 +9,7 @@ import { CSS } from "@dnd-kit/utilities";
import { createId } from "@paralleldrive/cuid2";
import * as Collapsible from "@radix-ui/react-collapsible";
import { GripIcon, Handshake, Undo2 } from "lucide-react";
import { useTranslations } from "next-intl";
import { cn } from "@formbricks/lib/cn";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
@@ -19,6 +20,7 @@ import {
TSurveyQuestionId,
TSurveyRedirectUrlCard,
} from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
import { TooltipRenderer } from "@formbricks/ui/components/Tooltip";
@@ -35,6 +37,7 @@ interface EditEndingCardProps {
plan: TOrganizationBillingPlan;
addEndingCard: (index: number) => void;
isFormbricksCloud: boolean;
locale: TUserLocale;
}
export const EditEndingCard = ({
@@ -50,16 +53,21 @@ export const EditEndingCard = ({
plan,
addEndingCard,
isFormbricksCloud,
locale,
}: EditEndingCardProps) => {
const endingCard = localSurvey.endings[endingCardIndex];
const t = useTranslations();
const isRedirectToUrlDisabled = isFormbricksCloud
? plan === "free" && endingCard.type !== "redirectToUrl"
: false;
const endingCardTypes = [
{ value: "endScreen", label: "Ending card" },
{ value: "redirectToUrl", label: "Redirect to Url", disabled: isRedirectToUrlDisabled },
{ value: "endScreen", label: t("environments.surveys.edit.ending_card") },
{
value: "redirectToUrl",
label: t("environments.surveys.edit.redirect_to_url"),
disabled: isRedirectToUrlDisabled,
},
];
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
@@ -176,12 +184,15 @@ export const EditEndingCard = ({
attributeClasses
)[selectedLanguageCode]
)
: "Ending card")}
{endingCard.type === "redirectToUrl" && (endingCard.label || "Redirect to Url")}
: t("environments.surveys.edit.ending_card"))}
{endingCard.type === "redirectToUrl" &&
(endingCard.label || t("environments.surveys.edit.redirect_to_url"))}
</p>
{!open && (
<p className="mt-1 truncate text-xs text-slate-500">
{endingCard.type === "endScreen" ? "Ending card" : "Redirect to Url"}
{endingCard.type === "endScreen"
? t("environments.surveys.edit.ending_card")
: t("environments.surveys.edit.redirect_to_url")}
</p>
)}
</div>
@@ -199,6 +210,7 @@ export const EditEndingCard = ({
updateCard={() => {}}
addCard={addEndingCard}
cardType="ending"
locale={locale}
/>
</div>
</div>
@@ -206,7 +218,7 @@ export const EditEndingCard = ({
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "mt-3 pb-6"}`}>
<TooltipRenderer
shouldRender={endingCard.type === "endScreen" && isRedirectToUrlDisabled}
tooltipContent={"Redirect To Url is not available on free plan"}
tooltipContent={t("environments.surveys.edit.redirect_to_url_not_available_on_free_plan")}
triggerClass="w-full">
<OptionsSwitch
options={endingCardTypes}
@@ -233,6 +245,7 @@ export const EditEndingCard = ({
attributeClasses={attributeClasses}
updateSurvey={updateSurvey}
endingCard={endingCard}
locale={locale}
/>
)}
{endingCard.type === "redirectToUrl" && (

View File

@@ -2,12 +2,14 @@
import * as Collapsible from "@radix-ui/react-collapsible";
import { Hand } from "lucide-react";
import { useTranslations } from "next-intl";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
import { cn } from "@formbricks/lib/cn";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyQuestionId, TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { FileInput } from "@formbricks/ui/components/FileInput";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
@@ -22,6 +24,7 @@ interface EditWelcomeCardProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (languageCode: string) => void;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const EditWelcomeCard = ({
@@ -33,7 +36,9 @@ export const EditWelcomeCard = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: EditWelcomeCardProps) => {
const t = useTranslations();
const [firstRender, setFirstRender] = useState(true);
const path = usePathname();
const environmentId = path?.split("/environments/")[1]?.split("/")[0];
@@ -79,17 +84,19 @@ export const EditWelcomeCard = ({
<div>
<div className="inline-flex">
<div>
<p className="text-sm font-semibold">Welcome card</p>
<p className="text-sm font-semibold">{t("common.welcome_card")}</p>
{!open && (
<p className="mt-1 truncate text-xs text-slate-500">
{localSurvey?.welcomeCard?.enabled ? "Shown" : "Hidden"}
{localSurvey?.welcomeCard?.enabled ? t("common.shown") : t("common.hidden")}
</p>
)}
</div>
</div>
<div className="flex items-center space-x-2">
<Label htmlFor="welcome-toggle">{localSurvey?.welcomeCard?.enabled ? "On" : "Off"}</Label>
<Label htmlFor="welcome-toggle">
{localSurvey?.welcomeCard?.enabled ? t("common.on") : t("common.off")}
</Label>
<Switch
id="welcome-toggle"
@@ -105,7 +112,7 @@ export const EditWelcomeCard = ({
<Collapsible.CollapsibleContent className={`flex flex-col px-4 ${open && "pb-6"}`}>
<form>
<div className="mt-2">
<Label htmlFor="companyLogo">Company Logo</Label>
<Label htmlFor="companyLogo">{t("environments.surveys.edit.company_logo")}</Label>
</div>
<div className="mt-3 flex w-full items-center justify-center">
<FileInput
@@ -122,7 +129,7 @@ export const EditWelcomeCard = ({
<QuestionFormInput
id="headline"
value={localSurvey.welcomeCard.headline}
label="Note*"
label={t("common.note") + "*"}
localSurvey={localSurvey}
questionIdx={-1}
isInvalid={isInvalid}
@@ -130,10 +137,11 @@ export const EditWelcomeCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
<div className="mt-3">
<Label htmlFor="subheader">Welcome Message</Label>
<Label htmlFor="subheader">{t("environments.surveys.edit.welcome_message")}</Label>
<div className="mt-2">
<LocalizedEditor
id="html"
@@ -146,6 +154,7 @@ export const EditWelcomeCard = ({
firstRender={firstRender}
setFirstRender={setFirstRender}
questionIdx={-1}
locale={locale}
/>
</div>
</div>
@@ -159,13 +168,14 @@ export const EditWelcomeCard = ({
localSurvey={localSurvey}
questionIdx={-1}
maxLength={48}
placeholder={"Next"}
placeholder={t("common.next")}
isInvalid={isInvalid}
updateSurvey={updateSurvey}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
label={`"Next" Button Label`}
label={t("environments.surveys.edit.next_button_label")}
locale={locale}
/>
</div>
</div>
@@ -182,9 +192,9 @@ export const EditWelcomeCard = ({
/>
</div>
<div className="flex-column">
<Label htmlFor="timeToFinish">Time to Finish</Label>
<Label htmlFor="timeToFinish">{t("common.time_to_finish")}</Label>
<div className="text-sm text-slate-500 dark:text-slate-400">
Display an estimate of completion time for survey
{t("environments.surveys.edit.display_an_estimate_of_completion_time_for_survey")}
</div>
</div>
</div>
@@ -201,9 +211,9 @@ export const EditWelcomeCard = ({
/>
</div>
<div className="flex-column">
<Label htmlFor="showResponseCount">Show Response Count</Label>
<Label htmlFor="showResponseCount">{t("common.show_response_count")}</Label>
<div className="text-sm text-slate-500 dark:text-slate-400">
Display number of responses for survey
{t("environments.surveys.edit.display_number_of_responses_for_survey")}
</div>
</div>
</div>

View File

@@ -2,13 +2,14 @@
import { createId } from "@paralleldrive/cuid2";
import { ArrowDownIcon, ArrowUpIcon, CopyIcon, EllipsisIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import {
CX_QUESTIONS_NAME_MAP,
QUESTIONS_ICON_MAP,
QUESTIONS_NAME_MAP,
getCXQuestionNameMap,
getQuestionDefaults,
getQuestionNameMap,
} from "@formbricks/lib/utils/questions";
import { TProduct } from "@formbricks/types/product";
import {
@@ -42,6 +43,7 @@ interface EditorCardMenuProps {
cardType: "question" | "ending";
product?: TProduct;
isCxMode?: boolean;
locale: string;
}
export const EditorCardMenu = ({
@@ -57,7 +59,9 @@ export const EditorCardMenu = ({
addCard,
cardType,
isCxMode = false,
locale,
}: EditorCardMenuProps) => {
const t = useTranslations();
const [logicWarningModal, setLogicWarningModal] = useState(false);
const [changeToType, setChangeToType] = useState(() => {
if (card.type !== "endScreen" && card.type !== "redirectToUrl") {
@@ -71,7 +75,7 @@ export const EditorCardMenu = ({
? survey.questions.length === 1
: survey.type === "link" && survey.endings.length === 1;
const availableQuestionTypes = isCxMode ? CX_QUESTIONS_NAME_MAP : QUESTIONS_NAME_MAP;
const availableQuestionTypes = isCxMode ? getCXQuestionNameMap(locale) : getQuestionNameMap(locale);
const changeQuestionType = (type?: TSurveyQuestionTypeEnum) => {
if (!type) return;
@@ -79,7 +83,7 @@ export const EditorCardMenu = ({
const { headline, required, subheader, imageUrl, videoUrl, buttonLabel, backButtonLabel } =
card as TSurveyQuestion;
const questionDefaults = getQuestionDefaults(type, product);
const questionDefaults = getQuestionDefaults(type, product, locale);
if (
(type === TSurveyQuestionTypeEnum.MultipleChoiceSingle &&
@@ -111,7 +115,7 @@ export const EditorCardMenu = ({
};
const addQuestionCardBelow = (type: TSurveyQuestionTypeEnum) => {
const questionDefaults = getQuestionDefaults(type, product);
const questionDefaults = getQuestionDefaults(type, product, locale);
addCard(
{
@@ -169,7 +173,7 @@ export const EditorCardMenu = ({
<DropdownMenuSubTrigger
className="cursor-pointer text-sm text-slate-600 hover:text-slate-700"
onClick={(e) => e.preventDefault()}>
Change question type
{t("environments.surveys.edit.change_question_type")}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="ml-2">
@@ -202,14 +206,14 @@ export const EditorCardMenu = ({
e.preventDefault();
addEndingCardBelow();
}}>
<span className="text-sm">Add ending below</span>
<span className="text-sm">{t("environments.surveys.edit.add_ending_below")}</span>
</DropdownMenuItem>
)}
{cardType === "question" && (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="cursor-pointer" onClick={(e) => e.preventDefault()}>
Add question below
{t("environments.surveys.edit.add_question_below")}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="ml-2">
@@ -241,7 +245,7 @@ export const EditorCardMenu = ({
}}
icon={<ArrowUpIcon className="h-4 w-4" />}
disabled={cardIdx === 0}>
<span>Move up</span>
<span>{t("common.move_up")}</span>
</DropdownMenuItem>
<DropdownMenuItem
@@ -253,7 +257,7 @@ export const EditorCardMenu = ({
}}
icon={<ArrowDownIcon className="h-4 w-4" />}
disabled={lastCard}>
<span>Move down</span>
<span>{t("common.move_down")}</span>
</DropdownMenuItem>
</div>
</DropdownMenuContent>
@@ -262,9 +266,9 @@ export const EditorCardMenu = ({
<ConfirmationModal
open={logicWarningModal}
setOpen={setLogicWarningModal}
title="Changing will cause logic errors"
text="Changing the question type will remove the logic conditions from this question"
buttonText="Change anyway"
title={t("environments.surveys.edit.logic_error_warning")}
text={t("environments.surveys.edit.logic_error_warning_text")}
buttonText={t("environments.surveys.edit.change_anyway")}
onConfirm={onConfirm}
/>
</div>

View File

@@ -1,9 +1,11 @@
"use client";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyEndScreenCard } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
@@ -18,6 +20,7 @@ interface EndScreenFormProps {
attributeClasses: TAttributeClass[];
updateSurvey: (input: Partial<TSurveyEndScreenCard>) => void;
endingCard: TSurveyEndScreenCard;
locale: TUserLocale;
}
export const EndScreenForm = ({
@@ -29,7 +32,9 @@ export const EndScreenForm = ({
attributeClasses,
updateSurvey,
endingCard,
locale,
}: EndScreenFormProps) => {
const t = useTranslations();
const [showEndingCardCTA, setshowEndingCardCTA] = useState<boolean>(
endingCard.type === "endScreen" &&
(!!getLocalizedValue(endingCard.buttonLabel, selectedLanguageCode) || !!endingCard.buttonLink)
@@ -38,7 +43,7 @@ export const EndScreenForm = ({
<form>
<QuestionFormInput
id="headline"
label="Note*"
label={t("common.note") + "*"}
value={endingCard.headline}
localSurvey={localSurvey}
questionIdx={localSurvey.questions.length + endingCardIndex}
@@ -47,12 +52,13 @@ export const EndScreenForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<QuestionFormInput
id="subheader"
value={endingCard.subheader}
label={"Description"}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={localSurvey.questions.length + endingCardIndex}
isInvalid={isInvalid}
@@ -60,6 +66,7 @@ export const EndScreenForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div className="mt-4">
<div className="flex items-center space-x-1">
@@ -71,7 +78,7 @@ export const EndScreenForm = ({
updateSurvey({ buttonLabel: undefined, buttonLink: undefined });
} else {
updateSurvey({
buttonLabel: { default: "Create your own Survey" },
buttonLabel: { default: t("environments.surveys.edit.create_your_own_survey") },
buttonLink: "https://formbricks.com",
});
}
@@ -80,9 +87,11 @@ export const EndScreenForm = ({
/>
<Label htmlFor="showButton" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Show Button</h3>
<h3 className="text-sm font-semibold text-slate-700">
{t("environments.surveys.edit.show_button")}
</h3>
<p className="text-xs font-normal text-slate-500">
Send your respondents to a page of your choice.
{t("environments.surveys.edit.send_your_respondents_to_a_page_of_your_choice")}
</p>
</div>
</Label>
@@ -92,8 +101,8 @@ export const EndScreenForm = ({
<div className="space-y-2">
<QuestionFormInput
id="buttonLabel"
label="Button Label"
placeholder="Create your own Survey"
label={t("environments.surveys.edit.button_label")}
placeholder={t("environments.surveys.edit.create_your_own_survey")}
className="bg-white"
value={endingCard.buttonLabel}
localSurvey={localSurvey}
@@ -103,10 +112,11 @@ export const EndScreenForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
<div className="space-y-2">
<Label>Button Link</Label>
<Label>{t("environments.surveys.edit.button_url")}</Label>
<Input
id="buttonLink"
name="buttonLink"

View File

@@ -2,6 +2,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon, XCircleIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useMemo, useState } from "react";
import { toast } from "react-hot-toast";
@@ -12,6 +13,7 @@ import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
import { TProduct } from "@formbricks/types/product";
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { AdvancedOptionToggle } from "@formbricks/ui/components/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/components/Button";
import { Input } from "@formbricks/ui/components/Input";
@@ -29,6 +31,7 @@ interface FileUploadFormProps {
isInvalid: boolean;
attributeClasses: TAttributeClass[];
isFormbricksCloud: boolean;
locale: TUserLocale;
}
export const FileUploadQuestionForm = ({
@@ -42,8 +45,10 @@ export const FileUploadQuestionForm = ({
setSelectedLanguageCode,
attributeClasses,
isFormbricksCloud,
locale,
}: FileUploadFormProps): JSX.Element => {
const [extension, setExtension] = useState("");
const t = useTranslations();
const [isMaxSizeError, setMaxSizeError] = useState(false);
const {
billingInfo,
@@ -68,14 +73,14 @@ export const FileUploadQuestionForm = ({
}
if (!modifiedExtension) {
toast.error("Please enter a file extension.");
toast.error(t("environments.surveys.edit.please_enter_a_file_extension"));
return;
}
const parsedExtensionResult = ZAllowedFileExtension.safeParse(modifiedExtension);
if (!parsedExtensionResult.success) {
toast.error("This file type is not supported.");
toast.error(t("environments.surveys.edit.this_file_type_is_not_supported"));
return;
}
@@ -86,7 +91,7 @@ export const FileUploadQuestionForm = ({
});
setExtension("");
} else {
toast.error("This extension is already added.");
toast.error(t("environments.surveys.edit.this_extension_is_already_added"));
}
} else {
updateQuestion(questionIdx, { allowedFileExtensions: [modifiedExtension] });
@@ -129,7 +134,7 @@ export const FileUploadQuestionForm = ({
<QuestionFormInput
id="headline"
value={question.headline}
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -137,6 +142,7 @@ export const FileUploadQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
{question.subheader !== undefined && (
@@ -145,7 +151,7 @@ export const FileUploadQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={"Description"}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -153,6 +159,7 @@ export const FileUploadQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -169,7 +176,7 @@ export const FileUploadQuestionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
{t("environments.surveys.edit.add_description")}
</Button>
)}
</div>
@@ -178,8 +185,8 @@ export const FileUploadQuestionForm = ({
isChecked={question.allowMultipleFiles}
onToggle={() => updateQuestion(questionIdx, { allowMultipleFiles: !question.allowMultipleFiles })}
htmlId="allowMultipleFile"
title="Allow Multiple Files"
description="Let people upload up to 25 files at the same time."
title={t("environments.surveys.edit.allow_multiple_files")}
description={t("environments.surveys.edit.let_people_upload_up_to_25_files_at_the_same_time")}
childBorder
customContainerClass="p-0"></AdvancedOptionToggle>
@@ -187,13 +194,13 @@ export const FileUploadQuestionForm = ({
isChecked={!!question.maxSizeInMB}
onToggle={handleMaxSizeInMBToggle}
htmlId="maxFileSize"
title="Max file size"
description="Limit the maximum file size."
title={t("environments.surveys.edit.max_file_size")}
description={t("environments.surveys.edit.limit_the_maximum_file_size")}
childBorder
customContainerClass="p-0">
<label htmlFor="autoCompleteResponses" className="cursor-pointer bg-slate-50 p-4">
<p className="text-sm font-semibold text-slate-700">
Limit upload file size to
{t("environments.surveys.edit.limit_upload_file_size_to")}
<Input
autoFocus
type="number"
@@ -203,7 +210,9 @@ export const FileUploadQuestionForm = ({
const parsedValue = parseInt(e.target.value, 10);
if (isFormbricksCloud && parsedValue > maxSizeInMBLimit) {
toast.error(`Max file size limit is ${maxSizeInMBLimit} MB`);
toast.error(
`${t("environments.surveys.edit.max_file_size_limit_is")} ${maxSizeInMBLimit} MB`
);
setMaxSizeError(true);
updateQuestion(questionIdx, { maxSizeInMB: maxSizeInMBLimit });
return;
@@ -217,12 +226,13 @@ export const FileUploadQuestionForm = ({
</p>
{isMaxSizeError && (
<p className="text-xs text-red-500">
Max file size limit is {maxSizeInMBLimit} MB. If you need more, please{" "}
{t("environments.surveys.edit.max_file_size_limit_is")} {maxSizeInMBLimit} MB.{" "}
{t("environments.surveys.edit.if_you_need_more_please")}
<Link
className="underline"
target="_blank"
href={`/environments/${localSurvey.environmentId}/settings/billing`}>
upgrade your plan.
{t("environments.surveys.edit.upgrade_your_plan")}
</Link>
</p>
)}
@@ -235,8 +245,8 @@ export const FileUploadQuestionForm = ({
updateQuestion(questionIdx, { allowedFileExtensions: checked ? [] : undefined })
}
htmlId="limitFileType"
title="Limit file types"
description="Control which file types can be uploaded."
title={t("environments.surveys.edit.limit_file_types")}
description={t("environments.surveys.edit.control_which_file_types_can_be_uploaded")}
childBorder
customContainerClass="p-0">
<div className="p-4">
@@ -264,7 +274,7 @@ export const FileUploadQuestionForm = ({
type="text"
/>
<Button size="sm" variant="secondary" onClick={(e) => addExtension(e)}>
Allow file type
{t("environments.surveys.edit.allow_file_type")}
</Button>
</div>
</div>

View File

@@ -3,6 +3,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { CheckIcon, SparklesIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import React from "react";
import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
@@ -29,6 +30,7 @@ export const FormStylingSettings = ({
setOpen,
form,
}: FormStylingSettingsProps) => {
const t = useTranslations();
const brandColor = form.watch("brandColor.light") || COLOR_DEFAULTS.brandColor;
const background = form.watch("background");
const highlightBorderColor = form.watch("highlightBorderColor");
@@ -97,10 +99,10 @@ export const FormStylingSettings = ({
<div>
<p className={cn("font-semibold text-slate-800", isSettingsPage ? "text-sm" : "text-base")}>
Form Styling
{t("environments.surveys.edit.form_styling")}
</p>
<p className={cn("mt-1 text-slate-500", isSettingsPage ? "text-xs" : "text-sm")}>
Style the question texts, descriptions and input fields.
{t("environments.surveys.edit.style_the_question_texts_descriptions_and_input_fields")}
</p>
</div>
</div>
@@ -117,8 +119,10 @@ export const FormStylingSettings = ({
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>Brand color</FormLabel>
<FormDescription>Change the brand color of the survey.</FormDescription>
<FormLabel>{t("environments.surveys.edit.brand_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_brand_color_of_the_survey")}
</FormDescription>
</div>
<FormControl>
@@ -139,7 +143,7 @@ export const FormStylingSettings = ({
EndIcon={SparklesIcon}
className="w-fit"
onClick={() => suggestColors()}>
Suggest colors
{t("environments.surveys.edit.suggest_colors")}
</Button>
</div>
@@ -149,8 +153,10 @@ export const FormStylingSettings = ({
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>Question color</FormLabel>
<FormDescription>Change the question color of the survey.</FormDescription>
<FormLabel>{t("environments.surveys.edit.question_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_question_color_of_the_survey")}
</FormDescription>
</div>
<FormControl>
@@ -170,8 +176,10 @@ export const FormStylingSettings = ({
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>Input color</FormLabel>
<FormDescription>Change the background color of the input fields.</FormDescription>
<FormLabel>{t("environments.surveys.edit.input_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_background_color_of_the_input_fields")}
</FormDescription>
</div>
<FormControl>
@@ -191,8 +199,10 @@ export const FormStylingSettings = ({
render={({ field }) => (
<FormItem className="space-y-4">
<div>
<FormLabel>Input border color</FormLabel>
<FormDescription>Change the border color of the input fields.</FormDescription>
<FormLabel>{t("environments.surveys.edit.input_border_color")}</FormLabel>
<FormDescription>
{t("environments.surveys.edit.change_the_border_color_of_the_input_fields")}
</FormDescription>
</div>
<FormControl>

View File

@@ -4,6 +4,7 @@ import { findHiddenFieldUsedInLogic } from "@/app/(app)/(survey-editor)/environm
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { EyeOff } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
@@ -31,7 +32,7 @@ export const HiddenFieldsCard = ({
}: HiddenFieldsCardProps) => {
const open = activeQuestionId == "hidden";
const [hiddenField, setHiddenField] = useState<string>("");
const t = useTranslations();
const setOpen = (open: boolean) => {
if (open) {
setActiveQuestionId("hidden");
@@ -72,7 +73,13 @@ export const HiddenFieldsCard = ({
if (quesIdx !== -1) {
toast.error(
`${fieldId} is used in logic of question ${quesIdx + 1}. Please remove it from logic first.`
t(
"environments.surveys.edit.fieldId_is_used_in_logic_of_question_please_remove_it_from_logic_first",
{
fieldId,
questionIndex: quesIdx + 1,
}
)
);
return;
}
@@ -108,13 +115,13 @@ export const HiddenFieldsCard = ({
<div>
<div className="inline-flex">
<div>
<p className="text-sm font-semibold">Hidden fields</p>
<p className="text-sm font-semibold">{t("common.hidden_fields")}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Label htmlFor="hidden-fields-toggle">
{localSurvey?.hiddenFields?.enabled ? "On" : "Off"}
{localSurvey?.hiddenFields?.enabled ? t("common.on") : t("common.off")}
</Label>
<Switch
@@ -143,7 +150,7 @@ export const HiddenFieldsCard = ({
})
) : (
<p className="mt-2 text-sm italic text-slate-500">
No hidden fields yet. Add the first one below.
{t("environments.surveys.edit.no_hidden_fields_yet_add_first_one_below")}
</p>
)}
</div>
@@ -171,10 +178,10 @@ export const HiddenFieldsCard = ({
fieldIds: [...(localSurvey.hiddenFields?.fieldIds || []), hiddenField],
enabled: true,
});
toast.success("Hidden field added successfully");
toast.success(t("environments.surveys.edit.hidden_field_added_successfully"));
setHiddenField("");
}}>
<Label htmlFor="headline">Hidden Field</Label>
<Label htmlFor="headline">{t("common.hidden_field")}</Label>
<div className="mt-2 flex gap-2">
<Input
autoFocus
@@ -182,10 +189,10 @@ export const HiddenFieldsCard = ({
name="headline"
value={hiddenField}
onChange={(e) => setHiddenField(e.target.value.trim())}
placeholder="Type field id..."
placeholder={t("environments.surveys.edit.type_field_id") + "..."}
/>
<Button variant="secondary" type="submit" size="sm" className="whitespace-nowrap">
Add hidden field ID
{t("environments.surveys.edit.add_hidden_field_id")}
</Button>
</div>
</form>

View File

@@ -3,6 +3,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { AlertCircleIcon, CheckIcon, LinkIcon, MonitorIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
@@ -18,12 +19,13 @@ interface HowToSendCardProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey | ((TSurvey: TSurvey) => TSurvey)) => void;
environment: TEnvironment;
locale: string;
}
export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowToSendCardProps) => {
export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, locale }: HowToSendCardProps) => {
const [open, setOpen] = useState(false);
const [appSetupCompleted, setAppSetupCompleted] = useState(false);
const t = useTranslations();
useEffect(() => {
if (environment) {
setAppSetupCompleted(environment.appSetupCompleted);
@@ -33,7 +35,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
const setSurveyType = (type: TSurveyType) => {
const endingsTemp = localSurvey.endings;
if (type === "link" && localSurvey.endings.length === 0) {
endingsTemp.push(getDefaultEndingCard(localSurvey.languages));
endingsTemp.push(getDefaultEndingCard(localSurvey.languages, locale));
}
setLocalSurvey((prevSurvey) => ({
...prevSurvey,
@@ -73,18 +75,18 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
const options = [
{
id: "link",
name: "Link survey",
name: t("common.link_survey"),
icon: LinkIcon,
description: "Share a link to a survey page or embed it in a web page or email.",
description: t("environments.surveys.edit.link_survey_description"),
comingSoon: false,
alert: false,
hide: false,
},
{
id: "app",
name: "Website & App Survey",
name: t("common.website_app_survey"),
icon: MonitorIcon,
description: "Embed a survey in your web app or website to collect responses.",
description: t("environments.surveys.edit.app_survey_description"),
comingSoon: false,
alert: !appSetupCompleted,
},
@@ -112,8 +114,10 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
/>
</div>
<div>
<p className="font-semibold text-slate-800">Survey Type</p>
<p className="mt-1 text-sm text-slate-500">Choose where to run the survey.</p>
<p className="font-semibold text-slate-800">{t("common.survey_type")}</p>
<p className="mt-1 text-sm text-slate-500">
{t("environments.surveys.edit.choose_where_to_run_the_survey")}
</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
@@ -166,15 +170,17 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
<div className="mt-2 flex items-center space-x-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-2">
<AlertCircleIcon className="h-5 w-5 text-amber-500" />
<div className="text-amber-800">
<p className="text-xs font-semibold">Formbricks SDK is not connected</p>
<p className="text-xs font-semibold">
{t("environments.surveys.edit.formbricks_sdk_is_not_connected")}
</p>
<p className="text-xs font-normal">
<Link
href={`/environments/${environment.id}/product/${option.id}-connection`}
className="underline hover:text-amber-900"
target="_blank">
Connect Formbricks
{t("common.connect_formbricks")}
</Link>{" "}
and launch surveys in your website or app.
{t("environments.surveys.edit.and_launch_surveys_in_your_website_or_app")}
</p>
</div>
</div>

View File

@@ -1,6 +1,7 @@
import { LogicEditorActions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions";
import { LogicEditorConditions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions";
import { ArrowRightIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
interface LogicEditorProps {
@@ -22,6 +23,7 @@ export function LogicEditor({
logicIdx,
isLast,
}: LogicEditorProps) {
const t = useTranslations();
return (
<div className="flex w-full grow flex-col gap-4 overflow-x-auto pb-2 text-sm">
<LogicEditorConditions
@@ -43,7 +45,9 @@ export function LogicEditor({
{isLast ? (
<div className="flex flex-wrap items-center space-x-2">
<ArrowRightIcon className="h-4 w-4" />
<p className="text-slate-700">All other answers will continue to the next question</p>
<p className="text-slate-700">
{t("environments.surveys.edit.all_other_answers_will_continue_to_the_next_question")}
</p>
</div>
) : null}
</div>

View File

@@ -7,6 +7,7 @@ import {
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { createId } from "@paralleldrive/cuid2";
import { CopyIcon, CornerDownRightIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { getUpdatedActionBody } from "@formbricks/lib/surveyLogic/utils";
import {
TActionNumberVariableCalculateOperator,
@@ -44,7 +45,7 @@ export function LogicEditorActions({
questionIdx,
}: LogicEditorActions) {
const actions = logicItem.actions;
const t = useTranslations();
const handleActionsChange = (
operation: "remove" | "addBelow" | "duplicate" | "update",
actionIdx: number,
@@ -93,7 +94,9 @@ export function LogicEditorActions({
<div className="flex grow flex-col gap-y-2">
{actions?.map((action, idx) => (
<div key={action.id} className="flex grow items-center justify-between gap-x-2">
<div className="block w-9 shrink-0">{idx === 0 ? "Then" : "and"}</div>
<div className="block w-9 shrink-0">
{idx === 0 ? t("environments.surveys.edit.then") : t("common.and")}
</div>
<div className="flex grow items-center gap-x-2">
<InputCombobox
id={`action-${idx}-objective`}
@@ -111,7 +114,7 @@ export function LogicEditorActions({
id={`action-${idx}-target`}
key={`target-${action.id}`}
showSearch={false}
options={getActionTargetOptions(action, localSurvey, questionIdx)}
options={getActionTargetOptions(action, localSurvey, questionIdx, t)}
value={action.target}
onChangeValue={(val: string) => {
handleValuesChange(idx, {
@@ -139,13 +142,14 @@ export function LogicEditorActions({
});
}}
comboboxClasses="grow"
emptyDropdownText="Add a variable to calculate"
emptyDropdownText={t("environments.surveys.edit.add_a_variable_to_calculate")}
/>
<InputCombobox
id={`action-${idx}-operator`}
key={`operator-${action.id}`}
showSearch={false}
options={getActionOperatorOptions(
t,
localSurvey.variables.find((v) => v.id === action.variableId)?.type
)}
value={action.operator}
@@ -168,7 +172,7 @@ export function LogicEditorActions({
placeholder: "Value",
type: localSurvey.variables.find((v) => v.id === action.variableId)?.type || "text",
}}
groupedOptions={getActionValueOptions(action.variableId, localSurvey)}
groupedOptions={getActionValueOptions(action.variableId, localSurvey, t)}
onChangeValue={(val, option, fromInput) => {
const fieldType = option?.meta?.type as TActionVariableValueType;
@@ -204,7 +208,7 @@ export function LogicEditorActions({
handleActionsChange("addBelow", idx);
}}
icon={<PlusIcon className="h-4 w-4" />}>
Add action below
{t("environments.surveys.edit.add_action_below")}
</DropdownMenuItem>
<DropdownMenuItem
@@ -213,7 +217,7 @@ export function LogicEditorActions({
handleActionsChange("remove", idx);
}}
icon={<TrashIcon className="h-4 w-4" />}>
Remove
{t("common.remove")}
</DropdownMenuItem>
<DropdownMenuItem
@@ -221,7 +225,7 @@ export function LogicEditorActions({
handleActionsChange("duplicate", idx);
}}
icon={<CopyIcon className="h-4 w-4" />}>
Duplicate
{t("common.duplicate")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -7,6 +7,7 @@ import {
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { CopyIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon, WorkflowIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { cn } from "@formbricks/lib/cn";
import {
addConditionBelow,
@@ -53,6 +54,7 @@ export function LogicEditorConditions({
updateQuestion,
depth = 0,
}: LogicEditorConditionsProps) {
const t = useTranslations();
const [parent] = useAutoAnimate();
const handleAddConditionBelow = (resourceId: string) => {
@@ -190,7 +192,7 @@ export function LogicEditorConditions({
return (
<div key={condition.id} className="flex items-start justify-between gap-4">
{index === 0 ? (
<div>When</div>
<div>{t("environments.surveys.edit.when")}</div>
) : (
<div
className={cn("w-14", index === 1 && "cursor-pointer underline")}
@@ -223,13 +225,13 @@ export function LogicEditorConditions({
handleAddConditionBelow(condition.id);
}}
icon={<PlusIcon className="h-4 w-4" />}>
Add condition below
{t("environments.surveys.edit.add_condition_below")}
</DropdownMenuItem>
<DropdownMenuItem
disabled={depth === 0 && conditions.conditions.length === 1}
onClick={() => handleRemoveCondition(condition.id)}
icon={<TrashIcon className="h-4 w-4" />}>
Remove
{t("common.remove")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -238,16 +240,16 @@ export function LogicEditorConditions({
);
}
const conditionValueOptions = getConditionValueOptions(localSurvey, questionIdx);
const conditionValueOptions = getConditionValueOptions(localSurvey, questionIdx, t);
const conditionOperatorOptions = getConditionOperatorOptions(condition, localSurvey);
const { show, options, showInput = false, inputType } = getMatchValueProps(condition, localSurvey);
const { show, options, showInput = false, inputType } = getMatchValueProps(condition, localSurvey, t);
const allowMultiSelect = ["equalsOneOf", "includesAllOf", "includesOneOf"].includes(condition.operator);
return (
<div key={condition.id} className="flex items-center gap-x-2">
<div className="w-10 shrink-0">
{index === 0 ? (
"When"
t("environments.surveys.edit.when")
) : (
<div
className={cn("w-14", index === 1 && "cursor-pointer underline")}
@@ -312,23 +314,23 @@ export function LogicEditorConditions({
handleAddConditionBelow(condition.id);
}}
icon={<PlusIcon className="h-4 w-4" />}>
Add condition below
{t("environments.surveys.edit.add_condition_below")}
</DropdownMenuItem>
<DropdownMenuItem
disabled={depth === 0 && conditions.conditions.length === 1}
onClick={() => handleRemoveCondition(condition.id)}
icon={<TrashIcon className="h-4 w-4" />}>
Remove
{t("common.remove")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleDuplicateCondition(condition.id)}
icon={<CopyIcon className="h-4 w-4" />}>
Duplicate
{t("common.duplicate")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleCreateGroup(condition.id)}
icon={<WorkflowIcon className="h-4 w-4" />}>
Create group
{t("environments.surveys.edit.create_group")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -2,9 +2,11 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
@@ -21,6 +23,7 @@ interface MatrixQuestionFormProps {
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const MatrixQuestionForm = ({
@@ -32,8 +35,10 @@ export const MatrixQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: MatrixQuestionFormProps): JSX.Element => {
const languageCodes = extractLanguageCodes(localSurvey.languages);
const t = useTranslations();
// Function to add a new Label input field
const handleAddLabel = (type: "row" | "column") => {
if (type === "row") {
@@ -83,17 +88,17 @@ export const MatrixQuestionForm = ({
const shuffleOptionsTypes = {
none: {
id: "none",
label: "Keep current order",
label: t("environments.surveys.edit.keep_current_order"),
show: true,
},
all: {
id: "all",
label: "Randomize all",
label: t("environments.surveys.edit.randomize_all"),
show: true,
},
exceptLast: {
id: "exceptLast",
label: "Randomize all except last option",
label: t("environments.surveys.edit.randomize_all_except_last"),
show: true,
},
};
@@ -104,7 +109,7 @@ export const MatrixQuestionForm = ({
<QuestionFormInput
id="headline"
value={question.headline}
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -112,6 +117,7 @@ export const MatrixQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
{question.subheader !== undefined && (
@@ -120,7 +126,7 @@ export const MatrixQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={"Description"}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -128,6 +134,7 @@ export const MatrixQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -144,14 +151,14 @@ export const MatrixQuestionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
{t("environments.surveys.edit.add_description")}
</Button>
)}
</div>
<div className="mt-3 grid grid-cols-2 gap-4">
<div>
{/* Rows section */}
<Label htmlFor="rows">Rows</Label>
<Label htmlFor="rows">{t("environments.surveys.edit.rows")}</Label>
<div ref={parent}>
{question.rows.map((_, index) => (
<div className="flex items-center" onKeyDown={(e) => handleKeyDown(e, "row")}>
@@ -169,6 +176,7 @@ export const MatrixQuestionForm = ({
isInvalid && !isLabelValidForAllLanguages(question.rows[index], localSurvey.languages)
}
attributeClasses={attributeClasses}
locale={locale}
/>
{question.rows.length > 2 && (
<TrashIcon
@@ -187,13 +195,13 @@ export const MatrixQuestionForm = ({
e.preventDefault();
handleAddLabel("row");
}}>
<span>Add row</span>
<span>{t("environments.surveys.edit.add_row")}</span>
</Button>
</div>
</div>
<div>
{/* Columns section */}
<Label htmlFor="columns">Columns</Label>
<Label htmlFor="columns">{t("environments.surveys.edit.columns")}</Label>
<div ref={parent}>
{question.columns.map((_, index) => (
<div className="flex items-center" onKeyDown={(e) => handleKeyDown(e, "column")}>
@@ -211,6 +219,7 @@ export const MatrixQuestionForm = ({
isInvalid && !isLabelValidForAllLanguages(question.columns[index], localSurvey.languages)
}
attributeClasses={attributeClasses}
locale={locale}
/>
{question.columns.length > 2 && (
<TrashIcon
@@ -229,7 +238,7 @@ export const MatrixQuestionForm = ({
e.preventDefault();
handleAddLabel("column");
}}>
<span>Add column</span>
<span>{t("environments.surveys.edit.add_column")}</span>
</Button>
</div>
<div className="mt-3 flex flex-1 items-center justify-end gap-2">

View File

@@ -6,6 +6,7 @@ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
@@ -17,13 +18,14 @@ import {
TSurveyMultipleChoiceQuestion,
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { ShuffleOptionSelect } from "@formbricks/ui/components/ShuffleOptionSelect";
import { QuestionOptionChoice } from "./QuestionOptionChoice";
interface OpenQuestionFormProps {
interface MultipleChoiceQuestionFormProps {
localSurvey: TSurvey;
question: TSurveyMultipleChoiceQuestion;
questionIdx: number;
@@ -33,6 +35,7 @@ interface OpenQuestionFormProps {
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const MultipleChoiceQuestionForm = ({
@@ -44,7 +47,9 @@ export const MultipleChoiceQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
}: OpenQuestionFormProps): JSX.Element => {
locale,
}: MultipleChoiceQuestionFormProps): JSX.Element => {
const t = useTranslations();
const lastChoiceRef = useRef<HTMLInputElement>(null);
const [isNew, setIsNew] = useState(true);
const [isInvalidValue, setisInvalidValue] = useState<string | null>(null);
@@ -55,17 +60,17 @@ export const MultipleChoiceQuestionForm = ({
const shuffleOptionsTypes = {
none: {
id: "none",
label: "Keep current order",
label: t("common.none"),
show: true,
},
all: {
id: "all",
label: "Randomize all",
label: t("environments.surveys.edit.randomize_all"),
show: question.choices.filter((c) => c.id === "other").length === 0,
},
exceptLast: {
id: "exceptLast",
label: "Randomize all except last option",
label: t("environments.surveys.edit.randomize_all_except_last"),
show: true,
},
};
@@ -129,7 +134,9 @@ export const MultipleChoiceQuestionForm = ({
const questionIdx = findOptionUsedInLogic(localSurvey, question.id, choiceToDelete);
if (questionIdx !== -1) {
toast.error(
`This option is used in logic for question ${questionIdx + 1}. Please fix the logic first before deleting.`
t("environments.surveys.edit.option_used_in_logic_error", {
questionIndex: questionIdx + 1,
})
);
return;
}
@@ -166,7 +173,7 @@ export const MultipleChoiceQuestionForm = ({
<QuestionFormInput
id="headline"
value={question.headline}
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -174,6 +181,7 @@ export const MultipleChoiceQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
@@ -183,7 +191,7 @@ export const MultipleChoiceQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={"Description"}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -191,6 +199,7 @@ export const MultipleChoiceQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -207,7 +216,7 @@ export const MultipleChoiceQuestionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
{t("environments.surveys.edit.add_description")}
</Button>
)}
</div>
@@ -259,6 +268,7 @@ export const MultipleChoiceQuestionForm = ({
updateQuestion={updateQuestion}
surveyLanguageCodes={surveyLanguageCodes}
attributeClasses={attributeClasses}
locale={locale}
/>
))}
</div>
@@ -267,7 +277,7 @@ export const MultipleChoiceQuestionForm = ({
<div className="mt-2 flex items-center justify-between space-x-2">
{question.choices.filter((c) => c.id === "other").length === 0 && (
<Button size="sm" variant="minimal" type="button" onClick={() => addOther()}>
Add &quot;Other&quot;
{t("environments.surveys.edit.add_other")}
</Button>
)}
<Button
@@ -282,8 +292,9 @@ export const MultipleChoiceQuestionForm = ({
: TSurveyQuestionTypeEnum.MultipleChoiceMulti,
});
}}>
Convert to{" "}
{question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ? "Multiple" : "Single"} Select
{question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle
? t("environments.surveys.edit.convert_to_multiple_choice")
: t("environments.surveys.edit.convert_to_single_choice")}
</Button>
<div className="flex flex-1 items-center justify-end gap-2">

View File

@@ -2,9 +2,11 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { AdvancedOptionToggle } from "@formbricks/ui/components/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/components/Button";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
@@ -19,6 +21,7 @@ interface NPSQuestionFormProps {
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const NPSQuestionForm = ({
@@ -31,7 +34,9 @@ export const NPSQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: NPSQuestionFormProps): JSX.Element => {
const t = useTranslations();
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
// Auto animate
const [parent] = useAutoAnimate();
@@ -41,7 +46,7 @@ export const NPSQuestionForm = ({
<QuestionFormInput
id="headline"
value={question.headline}
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -49,6 +54,7 @@ export const NPSQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
@@ -58,7 +64,7 @@ export const NPSQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={"Description"}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -66,6 +72,7 @@ export const NPSQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -83,7 +90,7 @@ export const NPSQuestionForm = ({
}}>
{" "}
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
{t("environments.surveys.edit.add_description")}
</Button>
)}
</div>
@@ -93,7 +100,7 @@ export const NPSQuestionForm = ({
<QuestionFormInput
id="lowerLabel"
value={question.lowerLabel}
label={"Lower Label"}
label={t("environments.surveys.edit.lower_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -101,13 +108,14 @@ export const NPSQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
<div className="w-full">
<QuestionFormInput
id="upperLabel"
value={question.upperLabel}
label={"Upper Label"}
label={t("environments.surveys.edit.upper_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -115,6 +123,7 @@ export const NPSQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -124,16 +133,17 @@ export const NPSQuestionForm = ({
<QuestionFormInput
id="buttonLabel"
value={question.buttonLabel}
label={`"Next" Button Label`}
label={t("environments.surveys.edit.next_button_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={lastQuestion ? "Finish" : "Next"}
placeholder={lastQuestion ? t("common.finish") : t("common.next")}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
)}
@@ -142,8 +152,8 @@ export const NPSQuestionForm = ({
isChecked={question.isColorCodingEnabled}
onToggle={() => updateQuestion(questionIdx, { isColorCodingEnabled: !question.isColorCodingEnabled })}
htmlId="isColorCodingEnabled"
title="Add color coding"
description="Add red, orange and green color codes to the options."
title={t("environments.surveys.edit.add_color_coding")}
description={t("environments.surveys.edit.add_color_coding_description")}
childBorder
customContainerClass="p-0 mt-4"
/>

View File

@@ -2,6 +2,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { HashIcon, LinkIcon, MailIcon, MessageSquareTextIcon, PhoneIcon, PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import {
@@ -9,17 +10,18 @@ import {
TSurveyOpenTextQuestion,
TSurveyOpenTextQuestionInputType,
} from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label";
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
const questionTypes = [
{ value: "text", label: "Text", icon: <MessageSquareTextIcon className="h-4 w-4" /> },
{ value: "email", label: "Email", icon: <MailIcon className="h-4 w-4" /> },
{ value: "url", label: "URL", icon: <LinkIcon className="h-4 w-4" /> },
{ value: "number", label: "Number", icon: <HashIcon className="h-4 w-4" /> },
{ value: "phone", label: "Phone", icon: <PhoneIcon className="h-4 w-4" /> },
{ value: "text", label: "common.text", icon: <MessageSquareTextIcon className="h-4 w-4" /> },
{ value: "email", label: "common.email", icon: <MailIcon className="h-4 w-4" /> },
{ value: "url", label: "common.url", icon: <LinkIcon className="h-4 w-4" /> },
{ value: "number", label: "common.number", icon: <HashIcon className="h-4 w-4" /> },
{ value: "phone", label: "common.phone", icon: <PhoneIcon className="h-4 w-4" /> },
];
interface OpenQuestionFormProps {
@@ -32,6 +34,7 @@ interface OpenQuestionFormProps {
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const OpenQuestionForm = ({
@@ -43,7 +46,9 @@ export const OpenQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: OpenQuestionFormProps): JSX.Element => {
const t = useTranslations();
const defaultPlaceholder = getPlaceholderByInputType(question.inputType ?? "text");
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
const handleInputChange = (inputType: TSurveyOpenTextQuestionInputType) => {
@@ -69,7 +74,8 @@ export const OpenQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
locale={locale}
/>
<div ref={parent}>
@@ -86,7 +92,8 @@ export const OpenQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
label={"Description"}
label={t("common.description")}
locale={locale}
/>
</div>
</div>
@@ -103,7 +110,7 @@ export const OpenQuestionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
{t("environments.surveys.edit.add_description")}
</Button>
)}
</div>
@@ -122,13 +129,14 @@ export const OpenQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
label={"Placeholder"}
label={t("common.placeholder")}
locale={locale}
/>
</div>
{/* Add a dropdown to select the question type */}
<div className="mt-3">
<Label htmlFor="questionType">Input Type</Label>
<Label htmlFor="questionType">{t("common.input_type")}</Label>
<div className="mt-2 flex items-center">
<OptionsSwitch
options={questionTypes}

View File

@@ -1,10 +1,12 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { cn } from "@formbricks/lib/cn";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyPictureSelectionQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Button } from "@formbricks/ui/components/Button";
import { FileInput } from "@formbricks/ui/components/FileInput";
import { Label } from "@formbricks/ui/components/Label";
@@ -21,6 +23,7 @@ interface PictureSelectionFormProps {
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const PictureSelectionForm = ({
@@ -32,10 +35,11 @@ export const PictureSelectionForm = ({
setSelectedLanguageCode,
isInvalid,
attributeClasses,
locale,
}: PictureSelectionFormProps): JSX.Element => {
const environmentId = localSurvey.environmentId;
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const t = useTranslations();
const handleChoiceDeletion = (choiceValue: string) => {
// Filter out the deleted choice from the choices array
const newChoices = question.choices?.filter((choice) => choice.id !== choiceValue) || [];
@@ -71,7 +75,7 @@ export const PictureSelectionForm = ({
<form>
<QuestionFormInput
id="headline"
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
value={question.headline}
localSurvey={localSurvey}
questionIdx={questionIdx}
@@ -80,6 +84,7 @@ export const PictureSelectionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
{question.subheader !== undefined && (
@@ -88,7 +93,7 @@ export const PictureSelectionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={"Description"}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -96,6 +101,7 @@ export const PictureSelectionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -112,18 +118,18 @@ export const PictureSelectionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
{t("environments.surveys.edit.add_description")}
</Button>
)}
</div>
<div className="mt-2">
<Label htmlFor="Images">
Images{" "}
{t("common.images")}{" "}
<span
className={cn("text-slate-400", {
"text-red-600": isInvalid && question.choices?.length < 2,
})}>
(Upload at least 2 images)
({t("environments.surveys.edit.upload_at_least_2_images")})
</span>
</Label>
<div className="mt-3 flex w-full items-center justify-center">
@@ -149,8 +155,12 @@ export const PictureSelectionForm = ({
/>
<Label htmlFor="multi-select-toggle" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Allow Multi Select</h3>
<p className="text-xs font-normal text-slate-500">Allow users to select more than one image.</p>
<h3 className="text-sm font-semibold text-slate-700">
{t("environments.surveys.edit.allow_multi_select")}
</h3>
<p className="text-xs font-normal text-slate-500">
{t("environments.surveys.edit.allow_users_to_select_more_than_one_image")}
</p>
</div>
</Label>
</div>

View File

@@ -1,5 +1,6 @@
"use client";
import { useTranslations } from "next-intl";
import { cn } from "@formbricks/lib/cn";
import { TPlacement } from "@formbricks/types/common";
import { Label } from "@formbricks/ui/components/Label";
@@ -7,21 +8,21 @@ import { getPlacementStyle } from "@formbricks/ui/components/PreviewSurvey/lib/u
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/components/RadioGroup";
const placements = [
{ name: "Bottom Right", value: "bottomRight", disabled: false },
{ name: "Top Right", value: "topRight", disabled: false },
{ name: "Top Left", value: "topLeft", disabled: false },
{ name: "Bottom Left", value: "bottomLeft", disabled: false },
{ name: "Centered Modal", value: "center", disabled: false },
{ name: "common.bottom_right", value: "bottomRight", disabled: false },
{ name: "common.top_right", value: "topRight", disabled: false },
{ name: "common.top_left", value: "topLeft", disabled: false },
{ name: "common.bottom_left", value: "bottomLeft", disabled: false },
{ name: "common.centered_modal", value: "center", disabled: false },
];
type TPlacementProps = {
interface TPlacementProps {
currentPlacement: TPlacement;
setCurrentPlacement: (placement: TPlacement) => void;
setOverlay: (overlay: string) => void;
overlay: string;
setClickOutsideClose: (clickOutside: boolean) => void;
clickOutsideClose: boolean;
};
}
export const Placement = ({
setCurrentPlacement,
@@ -31,6 +32,7 @@ export const Placement = ({
setClickOutsideClose,
clickOutsideClose,
}: TPlacementProps) => {
const t = useTranslations();
const overlayStyle =
currentPlacement === "center" && overlay === "dark" ? "bg-gray-700/80" : "bg-slate-200";
return (
@@ -41,7 +43,7 @@ export const Placement = ({
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id={placement.value} value={placement.value} disabled={placement.disabled} />
<Label htmlFor={placement.value} className="text-slate-900">
{placement.name}
{t(placement.name)}
</Label>
</div>
))}
@@ -62,7 +64,9 @@ export const Placement = ({
{currentPlacement === "center" && (
<>
<div className="mt-6 space-y-2">
<Label className="font-semibold">Centered modal overlay color</Label>
<Label className="font-semibold">
{t("environments.surveys.edit.centered_modal_overlay_color")}
</Label>
<RadioGroup
onValueChange={(overlay) => setOverlay(overlay)}
value={overlay}
@@ -70,19 +74,21 @@ export const Placement = ({
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="lightOverlay" value="light" />
<Label htmlFor="lightOverlay" className="text-slate-900">
Light Overlay
{t("common.light_overlay")}
</Label>
</div>
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="darkOverlay" value="dark" />
<Label htmlFor="darkOverlay" className="text-slate-900">
Dark Overlay
{t("common.dark_overlay")}
</Label>
</div>
</RadioGroup>
</div>
<div className="mt-6 space-y-2">
<Label className="font-semibold">Allow users to exit by clicking outside the study</Label>
<Label className="font-semibold">
{t("common.allow_users_to_exit_by_clicking_outside_the_survey")}
</Label>
<RadioGroup
onValueChange={(value) => setClickOutsideClose(value === "allow")}
value={clickOutsideClose ? "allow" : "disallow"}
@@ -90,13 +96,13 @@ export const Placement = ({
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="disallow" value="disallow" />
<Label htmlFor="disallow" className="text-slate-900">
Don&apos;t Allow
{t("common.disallow")}
</Label>
</div>
<div className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id="allow" value="allow" />
<Label htmlFor="allow" className="text-slate-900">
Allow
{t("common.allow")}
</Label>
</div>
</RadioGroup>

View File

@@ -8,6 +8,7 @@ import { CSS } from "@dnd-kit/utilities";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { ChevronDownIcon, ChevronRightIcon, GripIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { QUESTIONS_ICON_MAP, getTSurveyQuestionTypeEnumName } from "@formbricks/lib/utils/questions";
@@ -21,6 +22,7 @@ import {
TSurveyQuestionId,
TSurveyQuestionTypeEnum,
} from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { Switch } from "@formbricks/ui/components/Switch";
@@ -58,6 +60,7 @@ interface QuestionCardProps {
addQuestion: (question: any, index?: number) => void;
isFormbricksCloud: boolean;
isCxMode: boolean;
locale: TUserLocale;
}
export const QuestionCard = ({
@@ -79,11 +82,12 @@ export const QuestionCard = ({
addQuestion,
isFormbricksCloud,
isCxMode,
locale,
}: QuestionCardProps) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: question.id,
});
const t = useTranslations();
const open = activeQuestionId === question.id;
const [openAdvanced, setOpenAdvanced] = useState(question.logic && question.logic.length > 0);
const [parent] = useAutoAnimate();
@@ -203,11 +207,13 @@ export const QuestionCard = ({
attributeClasses
)[selectedLanguageCode] ?? ""
)
: getTSurveyQuestionTypeEnumName(question.type)}
: getTSurveyQuestionTypeEnumName(question.type, locale)}
</p>
{!open && (
<p className="mt-1 truncate text-xs text-slate-500">
{question?.required ? "Required" : "Optional"}
{question?.required
? t("environments.surveys.edit.required")
: t("environments.surveys.edit.optional")}
</p>
)}
</div>
@@ -227,6 +233,7 @@ export const QuestionCard = ({
addCard={addQuestion}
cardType="question"
isCxMode={isCxMode}
locale={locale}
/>
</div>
</div>
@@ -243,6 +250,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceSingle ? (
<MultipleChoiceQuestionForm
@@ -255,6 +263,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.MultipleChoiceMulti ? (
<MultipleChoiceQuestionForm
@@ -267,6 +276,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.NPS ? (
<NPSQuestionForm
@@ -279,6 +289,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.CTA ? (
<CTAQuestionForm
@@ -291,6 +302,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Rating ? (
<RatingQuestionForm
@@ -303,6 +315,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Consent ? (
<ConsentQuestionForm
@@ -314,6 +327,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Date ? (
<DateQuestionForm
@@ -326,6 +340,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.PictureSelection ? (
<PictureSelectionForm
@@ -338,6 +353,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.FileUpload ? (
<FileUploadQuestionForm
@@ -352,6 +368,7 @@ export const QuestionCard = ({
isInvalid={isInvalid}
attributeClasses={attributeClasses}
isFormbricksCloud={isFormbricksCloud}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Cal ? (
<CalQuestionForm
@@ -364,6 +381,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Matrix ? (
<MatrixQuestionForm
@@ -376,6 +394,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Address ? (
<AddressQuestionForm
@@ -388,6 +407,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.Ranking ? (
<RankingQuestionForm
@@ -400,6 +420,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : question.type === TSurveyQuestionTypeEnum.ContactInfo ? (
<ContactInfoQuestionForm
@@ -412,6 +433,7 @@ export const QuestionCard = ({
setSelectedLanguageCode={setSelectedLanguageCode}
isInvalid={isInvalid}
attributeClasses={attributeClasses}
locale={locale}
/>
) : null}
<div className="mt-4">
@@ -422,7 +444,9 @@ export const QuestionCard = ({
) : (
<ChevronRightIcon className="mr-2 h-4 w-3" />
)}
{openAdvanced ? "Hide Advanced Settings" : "Show Advanced Settings"}
{openAdvanced
? t("environments.surveys.edit.hide_advanced_settings")
: t("environments.surveys.edit.show_advanced_settings")}
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="flex flex-col gap-4" ref={parent}>
@@ -434,11 +458,11 @@ export const QuestionCard = ({
<QuestionFormInput
id="buttonLabel"
value={question.buttonLabel}
label={`"Next" Button Label`}
label={t("environments.surveys.edit.next_button_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={lastQuestion ? "Finish" : "Next"}
placeholder={lastQuestion ? t("common.finish") : t("common.next")}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
@@ -458,22 +482,24 @@ export const QuestionCard = ({
);
}}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
{questionIdx !== 0 && (
<QuestionFormInput
id="backButtonLabel"
value={question.backButtonLabel}
label={`"Back" Button Label`}
label={t("environments.surveys.edit.back_button_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
maxLength={48}
placeholder={"Back"}
placeholder={t("common.back")}
isInvalid={isInvalid}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
onBlur={(e) => {
if (!question.backButtonLabel) return;
let translatedBackButtonLabel = {
@@ -503,6 +529,7 @@ export const QuestionCard = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
)}
@@ -523,7 +550,7 @@ export const QuestionCard = ({
<div className="mx-4 flex justify-end space-x-6 border-t border-slate-200">
{question.type === "openText" && (
<div className="my-4 flex items-center justify-end space-x-2">
<Label htmlFor="longAnswer">Long Answer</Label>
<Label htmlFor="longAnswer">{t("environments.surveys.edit.long_answer")}</Label>
<Switch
id="longAnswer"
disabled={question.inputType !== "text"}
@@ -539,7 +566,7 @@ export const QuestionCard = ({
)}
{
<div className="my-4 flex items-center justify-end space-x-2">
<Label htmlFor="required-toggle">Required</Label>
<Label htmlFor="required-toggle">{t("environments.surveys.edit.required")}</Label>
<Switch
id="required-toggle"
checked={question.required}

View File

@@ -1,6 +1,7 @@
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { GripVerticalIcon, PlusIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { cn } from "@formbricks/lib/cn";
import { createI18nString } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
@@ -12,6 +13,7 @@ import {
TSurveyQuestionChoice,
TSurveyRankingQuestion,
} from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
import { isLabelValidForAllLanguages } from "../lib/validation";
@@ -34,6 +36,7 @@ interface ChoiceProps {
) => void;
surveyLanguageCodes: string[];
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const QuestionOptionChoice = ({
@@ -52,7 +55,9 @@ export const QuestionOptionChoice = ({
surveyLanguageCodes,
updateQuestion,
attributeClasses,
locale,
}: ChoiceProps) => {
const t = useTranslations();
const isDragDisabled = choice.id === "other";
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({
id: choice.id,
@@ -75,7 +80,11 @@ export const QuestionOptionChoice = ({
<QuestionFormInput
key={choice.id}
id={`choice-${choiceIdx}`}
placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`}
placeholder={
choice.id === "other"
? t("common.other")
: t("environments.surveys.edit.option_idx", { choiceIndex: choiceIdx + 1 })
}
label={""}
localSurvey={localSurvey}
questionIdx={questionIdx}
@@ -88,18 +97,19 @@ export const QuestionOptionChoice = ({
}
className={`${choice.id === "other" ? "border border-dashed" : ""} mt-0`}
attributeClasses={attributeClasses}
locale={locale}
/>
{choice.id === "other" && (
<QuestionFormInput
id="otherOptionPlaceholder"
localSurvey={localSurvey}
placeholder={"Please specify"}
placeholder={t("environments.surveys.edit.please_specify")}
label={""}
questionIdx={questionIdx}
value={
question.otherOptionPlaceholder
? question.otherOptionPlaceholder
: createI18nString("Please specify", surveyLanguageCodes)
: createI18nString(t("environments.surveys.edit.please_specify"), surveyLanguageCodes)
}
updateQuestion={updateQuestion}
selectedLanguageCode={selectedLanguageCode}
@@ -109,6 +119,7 @@ export const QuestionOptionChoice = ({
}
className="border border-dashed"
attributeClasses={attributeClasses}
locale={locale}
/>
)}
</div>

View File

@@ -3,6 +3,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TProduct } from "@formbricks/types/product";
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { QuestionCard } from "./QuestionCard";
interface QuestionsDraggableProps {
@@ -22,6 +23,7 @@ interface QuestionsDraggableProps {
addQuestion: (question: any, index?: number) => void;
isFormbricksCloud: boolean;
isCxMode: boolean;
locale: TUserLocale;
}
export const QuestionsDroppable = ({
@@ -41,6 +43,7 @@ export const QuestionsDroppable = ({
addQuestion,
isFormbricksCloud,
isCxMode,
locale,
}: QuestionsDraggableProps) => {
const [parent] = useAutoAnimate();
@@ -68,6 +71,7 @@ export const QuestionsDroppable = ({
addQuestion={addQuestion}
isFormbricksCloud={isFormbricksCloud}
isCxMode={isCxMode}
locale={locale}
/>
))}
</SortableContext>

View File

@@ -1,4 +1,5 @@
import { PaintbrushIcon, Rows3Icon, SettingsIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { cn } from "@formbricks/lib/cn";
import { TSurveyEditorTabs } from "@formbricks/types/surveys/types";
@@ -12,17 +13,17 @@ interface Tab {
const tabs: Tab[] = [
{
id: "questions",
label: "Questions",
label: "common.questions",
icon: <Rows3Icon className="h-5 w-5" />,
},
{
id: "styling",
label: "Styling",
label: "common.styling",
icon: <PaintbrushIcon className="h-5 w-5" />,
},
{
id: "settings",
label: "Settings",
label: "common.settings",
icon: <SettingsIcon className="h-5 w-5" />,
},
];
@@ -40,6 +41,7 @@ export const QuestionsAudienceTabs = ({
isStylingTabVisible,
isCxMode,
}: QuestionsAudienceTabsProps) => {
const t = useTranslations();
const tabsComputed = useMemo(() => {
if (isStylingTabVisible) {
return tabs;
@@ -66,7 +68,7 @@ export const QuestionsAudienceTabs = ({
)}
aria-current={tab.id === activeId ? "page" : undefined}>
{tab.icon && <div className="mr-2 h-5 w-5">{tab.icon}</div>}
{tab.label}
{t(tab.label)}
</button>
))}
</nav>

View File

@@ -14,6 +14,7 @@ import {
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { useTranslations } from "next-intl";
import React, { SetStateAction, useEffect, useMemo } from "react";
import toast from "react-hot-toast";
import { MultiLanguageCard } from "@formbricks/ee/multi-language/components/multi-language-card";
@@ -34,6 +35,7 @@ import {
} from "@formbricks/types/surveys/types";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
import { findQuestionsWithCyclicLogic } from "@formbricks/types/surveys/validation";
import { TUserLocale } from "@formbricks/types/user";
import {
isEndingCardValid,
isWelcomeCardValid,
@@ -61,6 +63,7 @@ interface QuestionsViewProps {
attributeClasses: TAttributeClass[];
plan: TOrganizationBillingPlan;
isCxMode: boolean;
locale: TUserLocale;
}
export const QuestionsView = ({
@@ -78,7 +81,9 @@ export const QuestionsView = ({
attributeClasses,
plan,
isCxMode,
locale,
}: QuestionsViewProps) => {
const t = useTranslations();
const internalQuestionIdMap = useMemo(() => {
return localSurvey.questions.reduce((acc, question) => {
acc[question.id] = createId();
@@ -262,7 +267,7 @@ export const QuestionsView = ({
const quesIdx = findQuestionUsedInLogic(localSurvey, questionId);
if (quesIdx !== -1) {
toast.error(`This question is used in logic of question ${quesIdx + 1}.`);
toast.error(t("environments.surveys.edit.question_used_in_logic", { questionIndex: quesIdx + 1 }));
return;
}
@@ -289,7 +294,7 @@ export const QuestionsView = ({
setActiveQuestionId(firstEndingCard.id);
}
}
toast.success("Question deleted.");
toast.success(t("environments.surveys.edit.question_deleted"));
};
const duplicateQuestion = (questionIdx: number) => {
@@ -311,7 +316,7 @@ export const QuestionsView = ({
setActiveQuestionId(newQuestionId);
internalQuestionIdMap[newQuestionId] = createId();
toast.success("Question duplicated.");
toast.success(t("environments.surveys.edit.question_duplicated"));
};
const addQuestion = (question: TSurveyQuestion, index?: number) => {
@@ -333,7 +338,7 @@ export const QuestionsView = ({
const addEndingCard = (index: number) => {
const updatedSurvey = structuredClone(localSurvey);
const newEndingCard = getDefaultEndingCard(localSurvey.languages);
const newEndingCard = getDefaultEndingCard(localSurvey.languages, locale);
updatedSurvey.endings.splice(index, 0, newEndingCard);
@@ -374,7 +379,7 @@ export const QuestionsView = ({
if (questionWithEmptyFallback) {
setActiveQuestionId(questionWithEmptyFallback.id);
if (activeQuestionId === questionWithEmptyFallback.id) {
toast.error("Fallback missing");
toast.error(t("environments.surveys.edit.fallback_missing"));
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -427,6 +432,7 @@ export const QuestionsView = ({
setSelectedLanguageCode={setSelectedLanguageCode}
selectedLanguageCode={selectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
)}
@@ -453,10 +459,11 @@ export const QuestionsView = ({
addQuestion={addQuestion}
isFormbricksCloud={isFormbricksCloud}
isCxMode={isCxMode}
locale={locale}
/>
</DndContext>
<AddQuestionButton addQuestion={addQuestion} product={product} isCxMode={isCxMode} />
<AddQuestionButton addQuestion={addQuestion} product={product} isCxMode={isCxMode} locale={locale} />
<div className="mt-5 flex flex-col gap-5" ref={parent}>
<hr className="border-t border-dashed" />
<DndContext
@@ -481,6 +488,7 @@ export const QuestionsView = ({
plan={plan}
addEndingCard={addEndingCard}
isFormbricksCloud={isFormbricksCloud}
locale={locale}
/>
);
})}
@@ -519,6 +527,7 @@ export const QuestionsView = ({
isMultiLanguageAllowed={isMultiLanguageAllowed}
isFormbricksCloud={isFormbricksCloud}
setSelectedLanguageCode={setSelectedLanguageCode}
locale={locale}
/>
</>
)}

View File

@@ -5,10 +5,12 @@ import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TI18nString, TSurvey, TSurveyRankingQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label";
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
@@ -25,6 +27,7 @@ interface RankingQuestionFormProps {
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const RankingQuestionForm = ({
@@ -36,7 +39,9 @@ export const RankingQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: RankingQuestionFormProps): JSX.Element => {
const t = useTranslations();
const lastChoiceRef = useRef<HTMLInputElement>(null);
const [isInvalidValue, setIsInvalidValue] = useState<string | null>(null);
@@ -92,12 +97,12 @@ export const RankingQuestionForm = ({
const shuffleOptionsTypes = {
none: {
id: "none",
label: "Keep current order",
label: t("environments.surveys.edit.keep_current_order"),
show: true,
},
all: {
id: "all",
label: "Randomize all",
label: t("environments.surveys.edit.randomize_all"),
show: question.choices.length > 0,
},
};
@@ -115,7 +120,7 @@ export const RankingQuestionForm = ({
<QuestionFormInput
id="headline"
value={question.headline}
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -123,6 +128,7 @@ export const RankingQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
@@ -132,7 +138,7 @@ export const RankingQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={"Description"}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -140,6 +146,7 @@ export const RankingQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -156,13 +163,13 @@ export const RankingQuestionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
{t("environments.surveys.edit.add_description")}
</Button>
)}
</div>
<div className="mt-3">
<Label htmlFor="choices">Options*</Label>
<Label htmlFor="choices">{t("environments.surveys.edit.options")}*</Label>
<div className="mt-2" id="choices">
<DndContext
id="ranking-choices"
@@ -204,6 +211,7 @@ export const RankingQuestionForm = ({
updateQuestion={updateQuestion}
surveyLanguageCodes={surveyLanguageCodes}
attributeClasses={attributeClasses}
locale={locale}
/>
))}
</div>
@@ -217,7 +225,7 @@ export const RankingQuestionForm = ({
EndIcon={PlusIcon}
type="button"
onClick={() => addOption()}>
Add option
{t("environments.surveys.edit.add_option")}
</Button>
<ShuffleOptionSelect
shuffleOptionsTypes={shuffleOptionsTypes}

View File

@@ -1,8 +1,10 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { HashIcon, PlusIcon, SmileIcon, StarIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TSurvey, TSurveyRatingQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { AdvancedOptionToggle } from "@formbricks/ui/components/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/components/Button";
import { Label } from "@formbricks/ui/components/Label";
@@ -19,6 +21,7 @@ interface RatingQuestionFormProps {
setSelectedLanguageCode: (language: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const RatingQuestionForm = ({
@@ -30,7 +33,9 @@ export const RatingQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
locale,
}: RatingQuestionFormProps) => {
const t = useTranslations();
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const [parent] = useAutoAnimate();
return (
@@ -38,7 +43,7 @@ export const RatingQuestionForm = ({
<QuestionFormInput
id="headline"
value={question.headline}
label={"Question*"}
label={t("environments.surveys.edit.question") + "*"}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -46,6 +51,7 @@ export const RatingQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
<div ref={parent}>
@@ -55,7 +61,7 @@ export const RatingQuestionForm = ({
<QuestionFormInput
id="subheader"
value={question.subheader}
label={"Description"}
label={t("common.description")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -63,6 +69,7 @@ export const RatingQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -79,20 +86,20 @@ export const RatingQuestionForm = ({
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
{t("environments.surveys.edit.add_description")}
</Button>
)}
</div>
<div className="mt-3 flex justify-between gap-8">
<div className="flex-1">
<Label htmlFor="subheader">Scale</Label>
<Label htmlFor="subheader">{t("environments.surveys.edit.scale")}</Label>
<div className="mt-2">
<Dropdown
options={[
{ label: "Number", value: "number", icon: HashIcon },
{ label: "Star", value: "star", icon: StarIcon },
{ label: "Smiley", value: "smiley", icon: SmileIcon },
{ label: t("environments.surveys.edit.number"), value: "number", icon: HashIcon },
{ label: t("environments.surveys.edit.star"), value: "star", icon: StarIcon },
{ label: t("environments.surveys.edit.smiley"), value: "smiley", icon: SmileIcon },
]}
defaultValue={question.scale || "number"}
onSelect={(option) => {
@@ -106,15 +113,15 @@ export const RatingQuestionForm = ({
</div>
</div>
<div className="flex-1">
<Label htmlFor="subheader">Range</Label>
<Label htmlFor="subheader">{t("environments.surveys.edit.range")}</Label>
<div className="mt-2">
<Dropdown
options={[
{ label: "5 points (recommended)", value: 5 },
{ label: "3 points", value: 3 },
{ label: "4 points", value: 4 },
{ label: "7 points", value: 7 },
{ label: "10 points", value: 10 },
{ label: t("environments.surveys.edit.five_points_recommended"), value: 5 },
{ label: t("environments.surveys.edit.three_points"), value: 3 },
{ label: t("environments.surveys.edit.four_points"), value: 4 },
{ label: t("environments.surveys.edit.seven_points"), value: 7 },
{ label: t("environments.surveys.edit.ten_points"), value: 10 },
]}
/* disabled={survey.status !== "draft"} */
defaultValue={question.range || 5}
@@ -130,7 +137,7 @@ export const RatingQuestionForm = ({
id="lowerLabel"
placeholder="Not good"
value={question.lowerLabel}
label={"Lower Label"}
label={t("environments.surveys.edit.lower_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -138,6 +145,7 @@ export const RatingQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
<div className="flex-1">
@@ -145,7 +153,7 @@ export const RatingQuestionForm = ({
id="upperLabel"
placeholder="Very satisfied"
value={question.upperLabel}
label={"Upper Label"}
label={t("environments.surveys.edit.upper_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
isInvalid={isInvalid}
@@ -153,6 +161,7 @@ export const RatingQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
</div>
@@ -163,7 +172,7 @@ export const RatingQuestionForm = ({
<QuestionFormInput
id="buttonLabel"
value={question.buttonLabel}
label={`"Next" Button Label`}
label={t("environments.surveys.edit.next_button_label")}
localSurvey={localSurvey}
questionIdx={questionIdx}
placeholder={"skip"}
@@ -172,6 +181,7 @@ export const RatingQuestionForm = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
attributeClasses={attributeClasses}
locale={locale}
/>
</div>
)}
@@ -184,8 +194,8 @@ export const RatingQuestionForm = ({
updateQuestion(questionIdx, { isColorCodingEnabled: !question.isColorCodingEnabled })
}
htmlId="isColorCodingEnabled"
title="Add color coding"
description="Add red, orange and green color codes to the options."
title={t("environments.surveys.edit.add_color_coding")}
description={t("environments.surveys.edit.add_color_coding_description")}
childBorder
customContainerClass="p-0 mt-4"
/>

View File

@@ -3,6 +3,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useEffect, useState } from "react";
import { TSurvey } from "@formbricks/types/surveys/types";
@@ -20,23 +21,23 @@ interface DisplayOption {
const displayOptions: DisplayOption[] = [
{
id: "displayOnce",
name: "Show only once",
description: "The survey will be shown once, even if person doesn't respond.",
name: "environments.surveys.edit.show_only_once",
description: "environments.surveys.edit.the_survey_will_be_shown_once_even_if_person_doesnt_respond",
},
{
id: "displaySome",
name: "Show multiple times",
description: "The survey will be shown multiple times until they respond",
name: "environments.surveys.edit.show_multiple_times",
description: "environments.surveys.edit.the_survey_will_be_shown_multiple_times_until_they_respond",
},
{
id: "displayMultiple",
name: "Until they submit a response",
description: "If you really want that answer, ask until you get it.",
name: "environments.surveys.edit.until_they_submit_a_response",
description: "environments.surveys.edit.if_you_really_want_that_answer_ask_until_you_get_it",
},
{
id: "respondMultiple",
name: "Keep showing while conditions match",
description: "Even after they submitted a response (e.g. Feedback Box)",
name: "environments.surveys.edit.keep_showing_while_conditions_match",
description: "environments.surveys.edit.even_after_they_submitted_a_response_e_g_feedback_box",
},
];
@@ -51,6 +52,7 @@ export const RecontactOptionsCard = ({
setLocalSurvey,
environmentId,
}: RecontactOptionsCardProps) => {
const t = useTranslations();
const [open, setOpen] = useState(false);
const ignoreWaiting = localSurvey.recontactDays !== null;
const [inputDays, setInputDays] = useState(
@@ -118,8 +120,10 @@ export const RecontactOptionsCard = ({
/>
</div>
<div>
<p className="font-semibold text-slate-800">Recontact Options</p>
<p className="mt-1 text-sm text-slate-500">Decide how often people can answer this survey.</p>
<p className="font-semibold text-slate-800">{t("environments.surveys.edit.recontact_options")}</p>
<p className="mt-1 text-sm text-slate-500">
{t("environments.surveys.edit.decide_how_often_people_can_answer_this_survey")}
</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
@@ -154,15 +158,15 @@ export const RecontactOptionsCard = ({
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
/>
<div>
<p className="font-semibold text-slate-700">{option.name}</p>
<p className="font-semibold text-slate-700">{t(option.name)}</p>
<p className="mt-2 text-xs font-normal text-slate-600">{option.description}</p>
<p className="mt-2 text-xs font-normal text-slate-600">{t(option.description)}</p>
</div>
</Label>
{option.id === "displaySome" && localSurvey.displayOption === "displaySome" && (
<label htmlFor="displayLimit" className="cursor-pointer p-4">
<p className="text-sm font-semibold text-slate-700">
Show survey maximum of
{t("environments.surveys.edit.show_survey_maximum_of")}
<Input
type="number"
min="1"
@@ -171,7 +175,7 @@ export const RecontactOptionsCard = ({
onChange={(e) => handleRecontactSessionDaysChange(e)}
className="mx-2 inline w-16 bg-white text-center text-sm"
/>
times.
{t("environments.surveys.edit.times")}.
</p>
</label>
)}
@@ -184,18 +188,18 @@ export const RecontactOptionsCard = ({
htmlId="recontactDays"
isChecked={ignoreWaiting}
onToggle={handleCheckMark}
title="Ignore waiting time between surveys"
title={t("environments.surveys.edit.ignore_waiting_time_between_surveys")}
childBorder={false}
description={
<>
This setting overwrites your{" "}
{t("environments.surveys.edit.this_setting_overwrites_your")}{" "}
<Link
className="decoration-brand-dark underline"
href={`/environments/${environmentId}/product/general`}
target="_blank">
waiting period
{t("environments.surveys.edit.waiting_period")}
</Link>
. Use with caution.
. {t("environments.surveys.edit.use_with_caution")}
</>
}>
{localSurvey.recontactDays !== null && (
@@ -215,10 +219,14 @@ export const RecontactOptionsCard = ({
className="aria-checked:border-brand-dark mx-4 text-sm disabled:border-slate-400 aria-checked:border-2"
/>
<div>
<p className="font-semibold text-slate-700">Always show survey</p>
<p className="font-semibold text-slate-700">
{t("environments.surveys.edit.always_show_survey")}
</p>
<p className="mt-2 text-xs font-normal text-slate-600">
When conditions match, waiting time will be ignored and survey shown.
{t(
"environments.surveys.edit.when_conditions_match_waiting_time_will_be_ignored_and_survey_shown"
)}
</p>
</div>
</Label>
@@ -233,7 +241,7 @@ export const RecontactOptionsCard = ({
/>
<div>
<p className="text-sm font-semibold text-slate-700">
Wait
{t("environments.surveys.edit.wait")}
<Input
type="number"
min="1"
@@ -242,11 +250,13 @@ export const RecontactOptionsCard = ({
onChange={handleRecontactDaysChange}
className="ml-2 mr-2 inline w-16 bg-white text-center text-sm"
/>
days before showing this survey again.
{t("environments.surveys.edit.days_before_showing_this_survey_again")}.
</p>
<p className="mt-2 text-xs font-normal text-slate-600">
Overwrites waiting period between surveys to {inputDays === 0 ? 1 : inputDays} day(s).
{t("environments.surveys.edit.overwrites_waiting_period_between_surveys_to_x_days", {
days: inputDays === 0 ? 1 : inputDays,
})}
</p>
</div>
</label>

View File

@@ -1,3 +1,4 @@
import { useTranslations } from "next-intl";
import { TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
import { Input } from "@formbricks/ui/components/Input";
import { Label } from "@formbricks/ui/components/Label";
@@ -8,10 +9,11 @@ interface RedirectUrlFormProps {
}
export const RedirectUrlForm = ({ endingCard, updateSurvey }: RedirectUrlFormProps) => {
const t = useTranslations();
return (
<form className="mt-3 space-y-3">
<div className="space-y-2">
<Label>URL</Label>
<Label>{t("common.url")}</Label>
<Input
id="redirectUrl"
name="redirectUrl"
@@ -22,7 +24,7 @@ export const RedirectUrlForm = ({ endingCard, updateSurvey }: RedirectUrlFormPro
/>
</div>
<div className="space-y-2">
<Label>Label</Label>
<Label>{t("common.label")}</Label>
<Input
id="redirectUrlLabel"
name="redirectUrlLabel"

View File

@@ -3,6 +3,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { ArrowUpRight, CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { KeyboardEventHandler, useEffect, useState } from "react";
import toast from "react-hot-toast";
@@ -25,6 +26,7 @@ export const ResponseOptionsCard = ({
setLocalSurvey,
responseCount,
}: ResponseOptionsCardProps) => {
const t = useTranslations();
const [open, setOpen] = useState(localSurvey.type === "link" ? true : false);
const autoComplete = localSurvey.autoComplete !== null;
const [runOnDateToggle, setRunOnDateToggle] = useState(false);
@@ -37,13 +39,13 @@ export const ResponseOptionsCard = ({
);
const [surveyClosedMessage, setSurveyClosedMessage] = useState({
heading: "Survey Completed",
subheading: "This free & open-source survey has been closed",
heading: t("environments.surveys.edit.survey_completed_heading"),
subheading: t("environments.surveys.edit.survey_completed_subheading"),
});
const [singleUseMessage, setSingleUseMessage] = useState({
heading: "The survey has already been answered.",
subheading: "You can only use this link once.",
heading: t("environments.surveys.edit.survey_already_answered_heading"),
subheading: t("environments.surveys.edit.survey_already_answered_subheading"),
});
const [singleUseEncryption, setSingleUseEncryption] = useState(true);
@@ -86,7 +88,7 @@ export const ResponseOptionsCard = ({
//check if pin only contains numbers
const validation = /^\d+$/;
const isValidPin = validation.test(pin);
if (!isValidPin) return toast.error("PIN can only contain numbers");
if (!isValidPin) return toast.error(t("environments.surveys.edit.pin_can_only_contain_numbers"));
setLocalSurvey({ ...localSurvey, pin });
};
@@ -96,7 +98,8 @@ export const ResponseOptionsCard = ({
const regexPattern = /^\d{4}$/;
const isValidPin = regexPattern.test(`${localSurvey.pin}`);
if (!isValidPin) return setVerifyProtectWithPinError("PIN must be a four digit number.");
if (!isValidPin)
return setVerifyProtectWithPinError(t("environments.surveys.edit.pin_must_be_a_four_digit_number"));
setVerifyProtectWithPinError(null);
};
@@ -264,12 +267,16 @@ export const ResponseOptionsCard = ({
const handleInputResponseBlur = (e) => {
if (parseInt(e.target.value) === 0) {
toast.error("Response limit can't be set to 0");
toast.error(t("environments.surveys.edit.response_limit_can_t_be_set_to_0"));
return;
}
if (parseInt(e.target.value) <= responseCount) {
toast.error(`Response limit needs to exceed number of received responses (${responseCount}).`);
toast.error(
t("environments.surveys.edit.response_limit_needs_to_exceed_number_of_received_responses", {
responseCount,
})
);
return;
}
};
@@ -292,8 +299,10 @@ export const ResponseOptionsCard = ({
/>{" "}
</div>
<div>
<p className="font-semibold text-slate-800">Response Options</p>
<p className="mt-1 text-sm text-slate-500">Response limits, redirections and more.</p>
<p className="font-semibold text-slate-800">{t("environments.surveys.edit.response_options")}</p>
<p className="mt-1 text-sm text-slate-500">
{t("environments.surveys.edit.response_limits_redirections_and_more")}
</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
@@ -305,12 +314,14 @@ export const ResponseOptionsCard = ({
htmlId="closeOnNumberOfResponse"
isChecked={autoComplete}
onToggle={toggleAutocomplete}
title="Close survey on response limit"
description="Automatically close the survey after a certain number of responses."
title={t("environments.surveys.edit.close_survey_on_response_limit")}
description={t(
"environments.surveys.edit.automatically_close_the_survey_after_a_certain_number_of_responses"
)}
childBorder={true}>
<label htmlFor="autoCompleteResponses" className="cursor-pointer bg-slate-50 p-4">
<p className="text-sm font-semibold text-slate-700">
Automatically mark the survey as complete after
{t("environments.surveys.edit.automatically_mark_the_survey_as_complete_after")}
<Input
autoFocus
type="number"
@@ -321,7 +332,7 @@ export const ResponseOptionsCard = ({
onBlur={handleInputResponseBlur}
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
/>
completed responses.
{t("environments.surveys.edit.completed_responses")}
</p>
</label>
</AdvancedOptionToggle>
@@ -330,8 +341,10 @@ export const ResponseOptionsCard = ({
htmlId="runOnDate"
isChecked={runOnDateToggle}
onToggle={handleRunOnDateToggle}
title="Release survey on date"
description="Automatically release the survey at the beginning of the day (UTC)."
title={t("environments.surveys.edit.release_survey_on_date")}
description={t(
"environments.surveys.edit.automatically_release_the_survey_at_the_beginning_of_the_day_utc"
)}
childBorder={true}>
<div className="p-4">
<DatePicker date={runOnDate} updateSurveyDate={handleRunOnDateChange} />
@@ -342,8 +355,10 @@ export const ResponseOptionsCard = ({
htmlId="closeOnDate"
isChecked={closeOnDateToggle}
onToggle={handleCloseOnDateToggle}
title="Close survey on date"
description="Automatically closes the survey at the beginning of the day (UTC)."
title={t("environments.surveys.edit.close_survey_on_date")}
description={t(
"environments.surveys.edit.automatically_closes_the_survey_at_the_beginning_of_the_day_utc"
)}
childBorder={true}>
<div className="p-4">
<DatePicker date={closeOnDate} updateSurveyDate={handleCloseOnDateChange} />
@@ -357,12 +372,12 @@ export const ResponseOptionsCard = ({
htmlId="adjustSurveyClosedMessage"
isChecked={surveyClosedMessageToggle}
onToggle={handleCloseSurveyMessageToggle}
title="Adjust 'Survey Closed' message"
description="Change the message visitors see when the survey is closed."
title={t("environments.surveys.edit.adjust_survey_closed_message")}
description={t("environments.surveys.edit.adjust_survey_closed_message_description")}
childBorder={true}>
<div className="flex w-full items-center space-x-1 p-4 pb-4">
<div className="w-full cursor-pointer items-center bg-slate-50">
<Label htmlFor="headline">Heading</Label>
<Label htmlFor="headline">{t("environments.surveys.edit.heading")}</Label>
<Input
autoFocus
id="heading"
@@ -372,7 +387,7 @@ export const ResponseOptionsCard = ({
onChange={(e) => handleClosedSurveyMessageChange({ heading: e.target.value })}
/>
<Label htmlFor="headline">Subheading</Label>
<Label htmlFor="headline">{t("environments.surveys.edit.subheading")}</Label>
<Input
className="mt-2 bg-white"
id="subheading"
@@ -389,31 +404,35 @@ export const ResponseOptionsCard = ({
htmlId="singleUserSurveyOptions"
isChecked={!!localSurvey.singleUse?.enabled}
onToggle={handleSingleUseSurveyToggle}
title="Single-use survey links"
description="Allow only 1 response per survey link."
title={t("environments.surveys.edit.single_use_survey_links")}
description={t("environments.surveys.edit.single_use_survey_links_description")}
childBorder={true}>
<div className="flex w-full items-center space-x-1 p-4 pb-4">
<div className="w-full cursor-pointer items-center bg-slate-50">
<div className="row mb-2 flex cursor-default items-center space-x-2">
<Label htmlFor="howItWorks">How it works</Label>
<Label htmlFor="howItWorks">{t("environments.surveys.edit.how_it_works")}</Label>
</div>
<ul className="mb-3 ml-4 cursor-default list-inside list-disc space-y-1">
<li className="text-sm text-slate-600">
Blocks survey if the survey URL has no Single Use Id (suId).
{t(
"environments.surveys.edit.blocks_survey_if_the_survey_url_has_no_single_use_id_suid"
)}
</li>
<li className="text-sm text-slate-600">
Blocks survey if a submission with the Single Use Id (suId) exists already.
{t(
"environments.surveys.edit.blocks_survey_if_a_submission_with_the_single_use_id_suid_exists_already"
)}
</li>
<li className="text-sm text-slate-600">
<Link
href="https://formbricks.com/docs/link-surveys/single-use-links"
target="_blank"
className="underline">
Docs <ArrowUpRight className="inline" size={16} />
{t("common.read_docs")} <ArrowUpRight className="inline" size={16} />
</Link>
</li>
</ul>
<Label htmlFor="headline">&lsquo;Link Used&rsquo; Message</Label>
<Label htmlFor="headline">{t("environments.surveys.edit.link_used_message")}</Label>
<Input
autoFocus
id="heading"
@@ -423,7 +442,7 @@ export const ResponseOptionsCard = ({
onChange={(e) => handleSingleUseSurveyMessageChange({ heading: e.target.value })}
/>
<Label htmlFor="headline">Subheading</Label>
<Label htmlFor="headline">{t("environments.surveys.edit.subheading")}</Label>
<Input
className="mb-4 mt-2 bg-white"
id="subheading"
@@ -431,7 +450,7 @@ export const ResponseOptionsCard = ({
defaultValue={singleUseMessage.subheading}
onChange={(e) => handleSingleUseSurveyMessageChange({ subheading: e.target.value })}
/>
<Label htmlFor="headline">URL Encryption</Label>
<Label htmlFor="headline">{t("environments.surveys.edit.url_encryption")}</Label>
<div>
<div className="mt-2 flex items-center space-x-1">
<Switch
@@ -442,7 +461,9 @@ export const ResponseOptionsCard = ({
<Label htmlFor="encryption-label">
<div className="ml-2">
<p className="text-sm font-normal text-slate-600">
Enable encryption of Single Use Id (suId) in survey URL.
{t(
"environments.surveys.edit.enable_encryption_of_single_use_id_suid_in_survey_url"
)}
</p>
</div>
</Label>
@@ -457,16 +478,16 @@ export const ResponseOptionsCard = ({
htmlId="verifyEmailBeforeSubmission"
isChecked={verifyEmailToggle}
onToggle={handleVerifyEmailToogle}
title="Verify email before submission"
description="Only let people with a real email respond."
title={t("environments.surveys.edit.verify_email_before_submission")}
description={t("environments.surveys.edit.verify_email_before_submission_description")}
childBorder={true}>
<div className="m-1">
<AdvancedOptionToggle
htmlId="preventDoubleSubmission"
isChecked={isSingleResponsePerEmailEnabledToggle}
onToggle={handleSingleResponsePerEmailToggle}
title="Prevent double submission"
description={"Only allow 1 response per email address"}
title={t("environments.surveys.edit.prevent_double_submission")}
description={t("environments.surveys.edit.prevent_double_submission_description")}
/>
</div>
</AdvancedOptionToggle>
@@ -474,12 +495,12 @@ export const ResponseOptionsCard = ({
htmlId="protectSurveyWithPin"
isChecked={isPinProtectionEnabled}
onToggle={handleProtectSurveyWithPinToggle}
title="Protect survey with a PIN"
description="Only users who have the PIN can access the survey."
title={t("environments.surveys.edit.protect_survey_with_pin")}
description={t("environments.surveys.edit.protect_survey_with_pin_description")}
childBorder={true}>
<div className="p-4">
<Label htmlFor="headline" className="sr-only">
Add PIN:
{t("environments.surveys.edit.add_pin")}
</Label>
<Input
autoFocus
@@ -487,7 +508,7 @@ export const ResponseOptionsCard = ({
isInvalid={Boolean(verifyProtectWithPinError)}
className="bg-white"
name="pin"
placeholder="Add a four digit PIN"
placeholder={t("environments.surveys.edit.add_a_four_digit_pin")}
onBlur={handleProtectSurveyPinBlurEvent}
defaultValue={localSurvey.pin ? localSurvey.pin : undefined}
onKeyDown={handleSurveyPinInputKeyDown}

View File

@@ -1,4 +1,5 @@
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { TActionClass } from "@formbricks/types/action-classes";
import { TSurvey } from "@formbricks/types/surveys/types";
@@ -17,6 +18,7 @@ export const SavedActionsTab = ({
setLocalSurvey,
setOpen,
}: SavedActionsTabProps) => {
const t = useTranslations();
const availableActions = actionClasses.filter(
(actionClass) => !localSurvey.triggers.some((trigger) => trigger.actionClass.id === actionClass.id)
);
@@ -55,7 +57,7 @@ export const SavedActionsTab = ({
actions.length > 0 && (
<div key={i} className="me-4">
<h2 className="mb-2 mt-4 font-semibold">
{i === 0 ? "Automatic" : i === 1 ? "No code" : "Code"}
{i === 0 ? t("common.automatic") : i === 1 ? t("common.no_code") : t("common.code")}
</h2>
<div className="flex flex-col gap-2">
{actions.map((action) => (

View File

@@ -23,6 +23,7 @@ interface SettingsViewProps {
membershipRole?: TMembershipRole;
isUserTargetingAllowed?: boolean;
isFormbricksCloud: boolean;
locale: string;
}
export const SettingsView = ({
@@ -36,12 +37,18 @@ export const SettingsView = ({
membershipRole,
isUserTargetingAllowed = false,
isFormbricksCloud,
locale,
}: SettingsViewProps) => {
const isAppSurvey = localSurvey.type === "app";
return (
<div className="mt-12 space-y-3 p-5">
<HowToSendCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} environment={environment} />
<HowToSendCard
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
environment={environment}
locale={locale}
/>
{localSurvey.type === "app" ? (
<div>

View File

@@ -1,4 +1,5 @@
import { RotateCcwIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import React, { useEffect, useMemo, useState } from "react";
import { UseFormReturn, useForm, useWatch } from "react-hook-form";
@@ -23,7 +24,7 @@ import { BackgroundStylingCard } from "./BackgroundStylingCard";
import { CardStylingSettings } from "./CardStylingSettings";
import { FormStylingSettings } from "./FormStylingSettings";
type StylingViewProps = {
interface StylingViewProps {
environment: TEnvironment;
product: TProduct;
localSurvey: TSurvey;
@@ -35,7 +36,7 @@ type StylingViewProps = {
setLocalStylingChanges: React.Dispatch<React.SetStateAction<TSurveyStyling | null>>;
isUnsplashConfigured: boolean;
isCxMode: boolean;
};
}
export const StylingView = ({
colors,
@@ -50,6 +51,8 @@ export const StylingView = ({
isUnsplashConfigured,
isCxMode,
}: StylingViewProps) => {
const t = useTranslations();
const stylingDefaults: TBaseStyling = useMemo(() => {
let stylingDefaults: TBaseStyling;
const isOverwriteEnabled = localSurvey.styling?.overwriteThemeStyling ?? false;
@@ -119,7 +122,7 @@ export const StylingView = ({
});
setConfirmResetStylingModalOpen(false);
toast.success("Styling set to theme styles");
toast.success(t("environments.surveys.edit.styling_set_to_theme_styles"));
};
useEffect(() => {
@@ -212,10 +215,10 @@ export const StylingView = ({
<div>
<FormLabel className="text-base font-semibold text-slate-900">
Add custom styles
{t("environments.surveys.edit.add_custom_styles")}
</FormLabel>
<FormDescription className="text-sm text-slate-800">
Override the theme with individual styles for this survey.
{t("environments.surveys.edit.override_theme_with_individual_styles_for_this_survey")}
</FormDescription>
</div>
</FormItem>
@@ -261,29 +264,29 @@ export const StylingView = ({
variant="minimal"
className="flex items-center gap-2"
onClick={() => setConfirmResetStylingModalOpen(true)}>
Reset to theme styles
{t("environments.surveys.edit.reset_to_theme_styles")}
<RotateCcwIcon className="h-4 w-4" />
</Button>
)}
</div>
<p className="text-sm text-slate-500">
Adjust the theme in the{" "}
{t("environments.surveys.edit.adjust_the_theme_in_the")}{" "}
<Link
href={`/environments/${environment.id}/product/look`}
target="_blank"
className="font-semibold underline">
Look & Feel
{t("common.look_and_feel")}
</Link>{" "}
settings
{t("common.settings")}
</p>
</div>
)}
<AlertDialog
open={confirmResetStylingModalOpen}
setOpen={setConfirmResetStylingModalOpen}
headerText="Reset to theme styles"
mainText="Are you sure you want to reset the styling to the theme styles? This will remove all custom styling."
confirmBtnLabel="Confirm"
headerText={t("environments.surveys.edit.reset_to_theme_styles")}
mainText={t("environments.surveys.edit.reset_to_theme_styles_main_text")}
confirmBtnLabel={t("common.confirm")}
onDecline={() => setConfirmResetStylingModalOpen(false)}
onConfirm={onResetThemeStyling}
/>

View File

@@ -1,4 +1,5 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { TabBar } from "@formbricks/ui/components/TabBar";
import { AnimatedSurveyBg } from "./AnimatedSurveyBg";
@@ -15,13 +16,6 @@ interface SurveyBgSelectorTabProps {
bg: string;
}
const tabs = [
{ id: "color", label: "Color" },
{ id: "animation", label: "Animation" },
{ id: "upload", label: "Upload" },
{ id: "image", label: "Image" },
];
export const SurveyBgSelectorTab = ({
handleBgChange,
colors,
@@ -31,11 +25,19 @@ export const SurveyBgSelectorTab = ({
isUnsplashConfigured,
}: SurveyBgSelectorTabProps) => {
const [activeTab, setActiveTab] = useState(bgType || "color");
const t = useTranslations();
const [parent] = useAutoAnimate();
const [colorBackground, setColorBackground] = useState(bg);
const [animationBackground, setAnimationBackground] = useState(bg);
const [uploadBackground, setUploadBackground] = useState(bg);
const tabs = [
{ id: "color", label: t("environments.surveys.edit.color") },
{ id: "animation", label: t("environments.surveys.edit.animation") },
{ id: "upload", label: t("environments.surveys.edit.upload") },
{ id: "image", label: t("environments.surveys.edit.image") },
];
useEffect(() => {
if (bgType === "color") {
setColorBackground(bg);

View File

@@ -12,6 +12,7 @@ import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
import { TProduct } from "@formbricks/types/product";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyEditorTabs, TSurveyStyling } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user";
import { PreviewSurvey } from "@formbricks/ui/components/PreviewSurvey";
import { refetchProductAction } from "../actions";
import { LoadingSkeleton } from "./LoadingSkeleton";
@@ -37,6 +38,7 @@ interface SurveyEditorProps {
isUnsplashConfigured: boolean;
plan: TOrganizationBillingPlan;
isCxMode: boolean;
locale: TUserLocale;
}
export const SurveyEditor = ({
@@ -55,6 +57,7 @@ export const SurveyEditor = ({
isUnsplashConfigured,
plan,
isCxMode = false,
locale,
}: SurveyEditorProps) => {
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("questions");
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
@@ -144,6 +147,7 @@ export const SurveyEditor = ({
selectedLanguageCode={selectedLanguageCode}
setSelectedLanguageCode={setSelectedLanguageCode}
isCxMode={isCxMode}
locale={locale}
/>
<div className="relative z-0 flex flex-1 overflow-hidden">
<main
@@ -172,6 +176,7 @@ export const SurveyEditor = ({
attributeClasses={attributeClasses}
plan={plan}
isCxMode={isCxMode}
locale={locale}
/>
)}
@@ -203,6 +208,7 @@ export const SurveyEditor = ({
membershipRole={membershipRole}
isUserTargetingAllowed={isUserTargetingAllowed}
isFormbricksCloud={isFormbricksCloud}
locale={locale}
/>
)}
</main>

View File

@@ -3,6 +3,7 @@
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
import { isEqual } from "lodash";
import { AlertTriangleIcon, ArrowLeftIcon, SettingsIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
@@ -40,6 +41,7 @@ interface SurveyMenuBarProps {
selectedLanguageCode: string;
setSelectedLanguageCode: (selectedLanguage: string) => void;
isCxMode: boolean;
locale: string;
}
export const SurveyMenuBar = ({
@@ -54,14 +56,16 @@ export const SurveyMenuBar = ({
responseCount,
selectedLanguageCode,
isCxMode,
locale,
}: SurveyMenuBarProps) => {
const t = useTranslations();
const router = useRouter();
const [audiencePrompt, setAudiencePrompt] = useState(true);
const [isLinkSurvey, setIsLinkSurvey] = useState(true);
const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [isSurveyPublishing, setIsSurveyPublishing] = useState(false);
const [isSurveySaving, setIsSurveySaving] = useState(false);
const cautionText = "Changes will lead to inconsistencies.";
const cautionText = t("environments.surveys.edit.caution_text");
useEffect(() => {
if (audiencePrompt && activeId === "settings") {
@@ -74,7 +78,7 @@ export const SurveyMenuBar = ({
}, [localSurvey.type]);
useEffect(() => {
const warningText = "You have unsaved changes - are you sure you wish to leave this page?";
const warningText = t("environments.surveys.edit.unsaved_changes_warning");
const handleWindowClose = (e: BeforeUnloadEvent) => {
if (!isEqual(localSurvey, survey)) {
e.preventDefault();
@@ -185,7 +189,7 @@ export const SurveyMenuBar = ({
const params = currentError.params ?? ({} as { invalidLanguageCodes: string[] });
if (params.invalidLanguageCodes && params.invalidLanguageCodes.length) {
const invalidLanguageLabels = params.invalidLanguageCodes.map(
(invalidLanguage: string) => getLanguageLabel(invalidLanguage) ?? invalidLanguage
(invalidLanguage: string) => getLanguageLabel(invalidLanguage, locale) ?? invalidLanguage
);
const messageSplit = currentError.message.split("-fLang-")[0];
@@ -218,7 +222,7 @@ export const SurveyMenuBar = ({
}
try {
const isSurveyValidResult = isSurveyValid(localSurvey, selectedLanguageCode);
const isSurveyValidResult = isSurveyValid(localSurvey, selectedLanguageCode, t);
if (!isSurveyValidResult) {
setIsSurveySaving(false);
return false;
@@ -238,7 +242,7 @@ export const SurveyMenuBar = ({
});
if (localSurvey.type !== "link" && !localSurvey.triggers?.length) {
toast.error("Please set a survey trigger");
toast.error(t("environments.surveys.edit.please_set_a_survey_trigger"));
setIsSurveySaving(false);
return false;
}
@@ -250,7 +254,7 @@ export const SurveyMenuBar = ({
setIsSurveySaving(false);
if (updatedSurveyResponse?.data) {
setLocalSurvey(updatedSurveyResponse.data);
toast.success("Changes saved.");
toast.success(t("environments.surveys.edit.changes_saved"));
} else {
const errorMessage = getFormattedErrorMessage(updatedSurveyResponse);
toast.error(errorMessage);
@@ -260,7 +264,7 @@ export const SurveyMenuBar = ({
} catch (e) {
console.error(e);
setIsSurveySaving(false);
toast.error(`Error saving changes`);
toast.error(t("environments.surveys.edit.error_saving_changes"));
return false;
}
};
@@ -283,7 +287,7 @@ export const SurveyMenuBar = ({
}
try {
const isSurveyValidResult = isSurveyValid(localSurvey, selectedLanguageCode);
const isSurveyValidResult = isSurveyValid(localSurvey, selectedLanguageCode, t);
if (!isSurveyValidResult) {
setIsSurveyPublishing(false);
return;
@@ -300,7 +304,8 @@ export const SurveyMenuBar = ({
setIsSurveyPublishing(false);
router.push(`/environments/${environment.id}/surveys/${localSurvey.id}/summary?success=true`);
} catch (error) {
toast.error("An error occured while publishing the survey.");
console.error(error);
toast.error(t("environments.surveys.edit.error_publishing_survey"));
setIsSurveyPublishing(false);
}
};
@@ -318,7 +323,7 @@ export const SurveyMenuBar = ({
onClick={() => {
handleBack();
}}>
Back
{t("common.back")}
</Button>
)}
<p className="hidden pl-4 font-semibold md:block">{product.name} / </p>
@@ -339,7 +344,9 @@ export const SurveyMenuBar = ({
<AlertTriangleIcon className="h-5 w-5 text-amber-400" />
</TooltipTrigger>
<TooltipContent side={"top"} className="lg:hidden">
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400">{cautionText}</p>
<p className="py-2 text-center text-xs text-slate-500 dark:text-slate-400">
{t(cautionText)}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -365,7 +372,7 @@ export const SurveyMenuBar = ({
loading={isSurveySaving}
onClick={() => handleSurveySave()}
type="submit">
Save
{t("common.save")}
</Button>
)}
@@ -376,7 +383,7 @@ export const SurveyMenuBar = ({
size="sm"
loading={isSurveySaving}
onClick={() => handleSaveAndGoBack()}>
Save & Close
{t("environments.surveys.edit.save_and_close")}
</Button>
)}
{localSurvey.status === "draft" && audiencePrompt && !isLinkSurvey && (
@@ -387,7 +394,7 @@ export const SurveyMenuBar = ({
setActiveId("settings");
}}
EndIcon={SettingsIcon}>
Continue to Settings
{t("environments.surveys.edit.continue_to_settings")}
</Button>
)}
{/* Always display Publish button for link surveys for better CR */}
@@ -397,17 +404,19 @@ export const SurveyMenuBar = ({
disabled={isSurveySaving || containsEmptyTriggers}
loading={isSurveyPublishing}
onClick={handleSurveyPublish}>
{isCxMode ? "Save & Close" : "Publish"}
{isCxMode
? t("environments.surveys.edit.save_and_close")
: t("environments.surveys.edit.publish")}
</Button>
)}
</div>
<AlertDialog
headerText="Confirm Survey Changes"
headerText={t("environments.surveys.edit.confirm_survey_changes")}
open={isConfirmDialogOpen}
setOpen={setConfirmDialogOpen}
mainText="You have unsaved changes in your survey. Would you like to save them before leaving?"
confirmBtnLabel="Save"
declineBtnLabel="Discard"
mainText={t("environments.surveys.edit.unsaved_changes_warning")}
confirmBtnLabel={t("common.save")}
declineBtnLabel={t("common.discard")}
declineBtnVariant="warn"
onDecline={() => {
setConfirmDialogOpen(false);

View File

@@ -3,6 +3,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useState } from "react";
import { TPlacement } from "@formbricks/types/common";
@@ -22,6 +23,7 @@ export const SurveyPlacementCard = ({
setLocalSurvey,
environmentId,
}: SurveyPlacementCardProps) => {
const t = useTranslations();
const [open, setOpen] = useState(false);
const { productOverwrites } = localSurvey ?? {};
@@ -93,8 +95,10 @@ export const SurveyPlacementCard = ({
/>
</div>
<div>
<p className="font-semibold text-slate-800">Survey Placement</p>
<p className="mt-1 text-sm text-slate-500">Overwrite the global placement of the survey</p>
<p className="font-semibold text-slate-800">{t("environments.surveys.edit.survey_placement")}</p>
<p className="mt-1 text-sm text-slate-500">
{t("environments.surveys.edit.overwrite_the_global_placement_of_the_survey")}
</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
@@ -107,9 +111,13 @@ export const SurveyPlacementCard = ({
<Label htmlFor="surveyDeadline" className="cursor-pointer">
<div className="ml-2">
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold text-slate-700">Overwrite Placement</h3>
<h3 className="text-sm font-semibold text-slate-700">
{t("environments.surveys.edit.overwrite_placement")}
</h3>
</div>
<p className="text-xs font-normal text-slate-500">Change the placement of this survey.</p>
<p className="text-xs font-normal text-slate-500">
{t("environments.surveys.edit.change_the_placement_of_this_survey")}
</p>
</div>
</Label>
</div>
@@ -132,9 +140,11 @@ export const SurveyPlacementCard = ({
<div>
<p className="text-xs text-slate-500">
To keep the placement over all surveys consistent, you can{" "}
{t("environments.surveys.edit.to_keep_the_placement_over_all_surveys_consistent_you_can")}{" "}
<Link href={`/environments/${environmentId}/product/look`} target="_blank">
<span className="underline">set the global placement in the Look & Feel settings.</span>
<span className="underline">
{t("environments.surveys.edit.set_the_global_placement_in_the_look_feel_settings")}
</span>
</Link>
</p>
</div>

View File

@@ -3,6 +3,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { FileDigitIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { cn } from "@formbricks/lib/cn";
import { TSurvey, TSurveyQuestionId } from "@formbricks/types/surveys/types";
import { SurveyVariablesCardItem } from "./SurveyVariablesCardItem";
@@ -23,6 +24,7 @@ export const SurveyVariablesCard = ({
setActiveQuestionId,
}: SurveyVariablesCardProps) => {
const open = activeQuestionId === variablesCardId;
const t = useTranslations();
const [parent] = useAutoAnimate();
const setOpenState = (state: boolean) => {
@@ -54,7 +56,7 @@ export const SurveyVariablesCard = ({
<div>
<div className="inline-flex">
<div>
<p className="text-sm font-semibold">Variables</p>
<p className="text-sm font-semibold">{t("common.variables")}</p>
</div>
</div>
</div>
@@ -72,7 +74,9 @@ export const SurveyVariablesCard = ({
/>
))
) : (
<p className="mt-2 text-sm italic text-slate-500">No variables yet. Add the first one below.</p>
<p className="mt-2 text-sm italic text-slate-500">
{t("environments.surveys.edit.no_variables_yet_add_first_one_below")}
</p>
)}
</div>

View File

@@ -3,6 +3,7 @@
import { findVariableUsedInLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
import { createId } from "@paralleldrive/cuid2";
import { TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import React, { useCallback, useEffect } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
@@ -33,6 +34,7 @@ export const SurveyVariablesCardItem = ({
setLocalSurvey,
mode,
}: SurveyVariablesCardItemProps) => {
const t = useTranslations();
const form = useForm<TSurveyVariable>({
defaultValues: variable ?? {
id: createId(),
@@ -87,7 +89,13 @@ export const SurveyVariablesCardItem = ({
if (quesIdx !== -1) {
toast.error(
`${variable.name} is used in logic of question ${quesIdx + 1}. Please remove it from logic first.`
t(
"environments.surveys.edit.variable_is_used_in_logic_of_question_please_remove_it_from_logic_first",
{
variable: variable.name,
questionIndex: quesIdx + 1,
}
)
);
return;
}
@@ -126,7 +134,9 @@ export const SurveyVariablesCardItem = ({
editSurveyVariable(data);
}
})}>
{mode === "create" && <Label htmlFor="headline">Add variable</Label>}
{mode === "create" && (
<Label htmlFor="headline">{t("environments.surveys.edit.add_variable")}</Label>
)}
<div className="mt-2 flex w-full items-center gap-2">
<FormField
@@ -135,7 +145,9 @@ export const SurveyVariablesCardItem = ({
rules={{
pattern: {
value: /^[a-z0-9_]+$/,
message: "Only lower case letters, numbers, and underscores are allowed.",
message: t(
"environments.surveys.edit.only_lower_case_letters_numbers_and_underscores_are_allowed"
),
},
validate: (value) => {
// if the variable name is already taken
@@ -143,18 +155,22 @@ export const SurveyVariablesCardItem = ({
mode === "create" &&
localSurvey.variables.find((variable) => variable.name === value)
) {
return "Variable name is already taken, please choose another.";
return t(
"environments.surveys.edit.variable_name_is_already_taken_please_choose_another"
);
}
if (mode === "edit" && variable && variable.name !== value) {
if (localSurvey.variables.find((variable) => variable.name === value)) {
return "Variable name is already taken, please choose another.";
return t(
"environments.surveys.edit.variable_name_is_already_taken_please_choose_another"
);
}
}
// if it does not start with a letter
if (!/^[a-z]/.test(value)) {
return "Variable name must start with a letter.";
return t("environments.surveys.edit.variable_name_must_start_with_a_letter");
}
},
}}
@@ -165,7 +181,7 @@ export const SurveyVariablesCardItem = ({
{...field}
isInvalid={isNameError}
type="text"
placeholder="Field name e.g, score, price"
placeholder={t("environments.surveys.edit.field_name_eg_score_price")}
/>
</FormControl>
</FormItem>
@@ -183,11 +199,14 @@ export const SurveyVariablesCardItem = ({
field.onChange(value);
}}>
<SelectTrigger className="w-24">
<SelectValue placeholder="Select type" className="text-sm" />
<SelectValue
placeholder={t("environments.surveys.edit.select_type")}
className="text-sm"
/>
</SelectTrigger>
<SelectContent>
<SelectItem value={"number"}>Number</SelectItem>
<SelectItem value={"text"}>Text</SelectItem>
<SelectItem value={"number"}>{t("common.number")}</SelectItem>
<SelectItem value={"text"}>{t("common.text")}</SelectItem>
</SelectContent>
</Select>
)}
@@ -206,7 +225,7 @@ export const SurveyVariablesCardItem = ({
onChange={(e) => {
field.onChange(variableType === "number" ? Number(e.target.value) : e.target.value);
}}
placeholder="Initial value"
placeholder={t("environments.surveys.edit.initial_value")}
type={variableType === "number" ? "number" : "text"}
/>
</FormControl>
@@ -216,7 +235,7 @@ export const SurveyVariablesCardItem = ({
{mode === "create" && (
<Button variant="secondary" type="submit" className="h-10 whitespace-nowrap">
Add variable
{t("environments.surveys.edit.add_variable")}
</Button>
)}

View File

@@ -3,6 +3,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { AlertCircle, CheckIcon, ChevronDownIcon, ChevronUpIcon, PencilIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
@@ -51,6 +52,7 @@ export const TargetingCard = ({
initialSegment,
isFormbricksCloud,
}: TargetingCardProps) => {
const t = useTranslations();
const router = useRouter();
const [segment, setSegment] = useState<TSegment | null>(localSurvey.segment);
const [open, setOpen] = useState(false);
@@ -114,16 +116,16 @@ export const TargetingCard = ({
const handleSaveSegment = async (data: TSegmentUpdateInput) => {
try {
if (!segment) throw new Error("Invalid segment");
if (!segment) throw new Error(t("environments.surveys.edit.invalid_segment"));
await updateBasicSegmentAction({ segmentId: segment?.id, data });
router.refresh();
toast.success("Segment saved successfully");
toast.success(t("environments.surveys.edit.segment_saved_successfully"));
setIsSegmentEditorOpen(false);
setSegmentEditorViewOnly(true);
} catch (err) {
toast.error(err.message ?? "Error Saving Segment");
toast.error(err.message ?? t("environments.surveys.edit.error_saving_segment"));
}
};
@@ -134,7 +136,7 @@ export const TargetingCard = ({
});
return resetBasicSegmentFiltersResponse?.data;
} catch (err) {
toast.error("Error resetting filters");
toast.error(t("environments.surveys.edit.error_resetting_filters"));
}
};
@@ -173,8 +175,10 @@ export const TargetingCard = ({
/>
</div>
<div>
<p className="font-semibold text-slate-800">Target Audience</p>
<p className="mt-1 text-sm text-slate-500">Pre-segment your users with attributes filters.</p>
<p className="font-semibold text-slate-800">{t("environments.surveys.edit.target_audience")}</p>
<p className="mt-1 text-sm text-slate-500">
{t("environments.surveys.edit.pre_segment_your_users_with_attributes_filters")}
</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
@@ -195,7 +199,9 @@ export const TargetingCard = ({
/>
<p className="text-sm italic text-slate-600">
This is an advanced segment. Please upgrade your plan to edit it.
{t(
"environments.surveys.edit.this_is_an_advanced_segment_please_upgrade_your_plan_to_edit_it"
)}
</p>
</div>
) : (
@@ -211,7 +217,7 @@ export const TargetingCard = ({
) : (
<div className="mb-4">
<p className="text-sm font-semibold text-slate-800">
Send survey to audience who match...
{t("environments.surveys.edit.send_survey_to_audience_who_match")}
</p>
</div>
)}
@@ -236,7 +242,7 @@ export const TargetingCard = ({
segment?.isPrivate && !segment?.filters?.length && "mt-0"
)}>
<Button variant="secondary" size="sm" onClick={() => setAddFilterModalOpen(true)}>
Add filter
{t("common.add_filter")}
</Button>
{isSegmentEditorOpen && !segment?.isPrivate && (
@@ -246,7 +252,7 @@ export const TargetingCard = ({
onClick={() => {
handleSaveSegment({ filters: segment?.filters ?? [] });
}}>
Save changes
{t("common.save_changes")}
</Button>
)}
@@ -263,7 +269,7 @@ export const TargetingCard = ({
setSegment(initialSegment);
}
}}>
Cancel
{t("common.cancel")}
</Button>
)}
</div>
@@ -297,7 +303,7 @@ export const TargetingCard = ({
onClick={() => {
setSegmentEditorViewOnly(!segmentEditorViewOnly);
}}>
{segmentEditorViewOnly ? "Hide" : "View"} Filters{" "}
{segmentEditorViewOnly ? t("common.hide_filters") : t("common.view_filters")}{" "}
{segmentEditorViewOnly ? (
<ChevronUpIcon className="ml-2 h-3 w-3" />
) : (
@@ -312,7 +318,7 @@ export const TargetingCard = ({
onClick={() => {
handleCloneSegment();
}}>
Clone & Edit Segment
{t("environments.surveys.edit.clone_edit_segment")}
</Button>
)}
@@ -323,7 +329,7 @@ export const TargetingCard = ({
onClick={() => {
handleEditSegment();
}}>
Edit Segment
{t("environments.surveys.edit.edit_segment")}
<PencilIcon className="ml-2 h-3 w-3" />
</Button>
)}
@@ -331,7 +337,7 @@ export const TargetingCard = ({
{isSegmentUsedInOtherSurveys && (
<p className="mt-1 flex items-center text-xs text-slate-500">
<AlertCircle className="mr-1 inline h-3 w-3" />
This segment is used in other surveys. Make changes{" "}
{t("environments.surveys.edit.this_segment_is_used_in_other_surveys_make_changes")}
<Link
href={`/environments/${environmentId}/segments`}
target="_blank"
@@ -349,12 +355,12 @@ export const TargetingCard = ({
<div className="flex w-full gap-3">
<Button variant="secondary" size="sm" onClick={() => setLoadSegmentModalOpen(true)}>
Load Segment
{t("environments.surveys.edit.load_segment")}
</Button>
{!segment?.isPrivate && !!segment?.filters?.length && (
<Button variant="secondary" size="sm" onClick={() => setResetAllFiltersModalOpen(true)}>
Reset all filters
{t("common.reset_all_filters")}
</Button>
)}
@@ -364,21 +370,21 @@ export const TargetingCard = ({
size="sm"
className="flex items-center gap-2"
onClick={() => setSaveAsNewSegmentModalOpen(true)}>
Save as new Segment
{t("environments.surveys.edit.save_as_new_segment")}
</Button>
)}
</div>
<div className="-mt-1.5">
{isFormbricksCloud ? (
<UpgradePlanNotice
message="For advanced targeting, please"
textForUrl="upgrade to the Scale plan."
message={t("environments.surveys.edit.for_advanced_targeting_please")}
textForUrl={t("environments.surveys.edit.upgrade_to_the_scale_plan")}
url={`/environments/${environmentId}/settings/billing`}
/>
) : (
<UpgradePlanNotice
message="For advanced targeting, please"
textForUrl="request an Enterprise license."
message={t("environments.surveys.edit.for_advanced_targeting_please")}
textForUrl={t("common.request_an_enterprise_license")}
url={`/environments/${environmentId}/settings/enterprise`}
/>
)}
@@ -421,19 +427,19 @@ export const TargetingCard = ({
)}
<AlertDialog
headerText="Are you sure?"
headerText={t("common.are_you_sure")}
open={resetAllFiltersModalOpen}
setOpen={setResetAllFiltersModalOpen}
mainText="This action resets all filters in this survey."
declineBtnLabel="Cancel"
mainText={t("environments.surveys.edit.this_action_resets_all_filters_in_this_survey")}
declineBtnLabel={t("common.cancel")}
onDecline={() => {
setResetAllFiltersModalOpen(false);
}}
confirmBtnLabel="Remove all filters"
confirmBtnLabel={t("environments.surveys.edit.remove_all_filters")}
onConfirm={async () => {
const segment = await handleResetAllFilters();
if (segment) {
toast.success("Filters reset successfully");
toast.success(t("common.filters_reset_successfully"));
router.refresh();
setSegment(segment);
setResetAllFiltersModalOpen(false);
@@ -445,14 +451,14 @@ export const TargetingCard = ({
<Alert className="flex items-center rounded-none bg-slate-50">
<AlertDescription className="ml-2">
<span className="mr-1 text-slate-600">
User targeting is currently only available when{" "}
{t("environments.surveys.edit.user_targeting_is_currently_only_available_when")}
<Link
href="https://formbricks.com//docs/app-surveys/user-identification"
target="blank"
className="underline">
identifying users
{t("environments.surveys.edit.identifying_users")}
</Link>{" "}
with the Formbricks SDK.
{t("environments.surveys.edit.with_the_formbricks_sdk")}
</span>
</AlertDescription>
</Alert>

View File

@@ -2,6 +2,7 @@
import { debounce } from "lodash";
import { SearchIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import UnsplashImage from "next/image";
import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
@@ -120,6 +121,7 @@ const defaultImages = [
];
export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashSurveyBgProps) => {
const t = useTranslations();
const inputFocus = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
const [query, setQuery] = useState("");
@@ -194,10 +196,10 @@ export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashS
<Input
value={query}
onChange={handleChange}
placeholder="Try 'lollipop' or 'mountain'..."
placeholder={t("environments.surveys.edit.try_lollipop_or_mountain")}
className="pl-8"
ref={inputFocus}
aria-label="Search for images"
aria-label={t("environments.surveys.edit.search_for_images")}
/>
</div>
<div className="relative mt-4 grid grid-cols-3 gap-1">
@@ -231,12 +233,12 @@ export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashS
className="col-span-3 mt-3 flex items-center justify-center"
type="button"
onClick={handleLoadMore}>
Load More
{t("common.load_more")}
</Button>
)}
{!isLoading && images.length === 0 && query.trim() !== "" && (
<div className="col-span-3 flex items-center justify-center text-sm text-slate-500">
No images found for &apos;{query}&apos;
{t("environments.surveys.edit.no_images_found_for", { query: query })}
</div>
)}
</div>

View File

@@ -1,5 +1,6 @@
"use client";
import { useTranslations } from "next-intl";
import { useState } from "react";
import toast from "react-hot-toast";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
@@ -21,6 +22,7 @@ export const UpdateQuestionId = ({
questionIdx,
updateQuestion,
}: UpdateQuestionIdProps) => {
const t = useTranslations();
const [currentValue, setCurrentValue] = useState(question.id);
const [prevValue, setPrevValue] = useState(question.id);
const [isInputInvalid, setIsInputInvalid] = useState(
@@ -47,7 +49,7 @@ export const UpdateQuestionId = ({
}
setIsInputInvalid(false);
toast.success("Question ID updated.");
toast.success(t("environments.surveys.edit.question_id_updated"));
updateQuestion(questionIdx, { id: currentValue });
setPrevValue(currentValue); // after successful update, set current value as previous value
};
@@ -59,7 +61,7 @@ export const UpdateQuestionId = ({
return (
<div>
<Label htmlFor="questionId">Question ID</Label>
<Label htmlFor="questionId">{t("common.question_id")}</Label>
<div className="mt-2 inline-flex w-full space-x-2">
<Input
id="questionId"
@@ -73,7 +75,7 @@ export const UpdateQuestionId = ({
className={`h-10 ${isInputInvalid ? "border-red-300 focus:border-red-300" : ""}`}
/>
<Button size="sm" onClick={saveAction} disabled={isButtonDisabled()}>
Save
{t("common.save")}
</Button>
</div>
</div>

View File

@@ -10,6 +10,7 @@ import {
SparklesIcon,
Trash2Icon,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useMemo, useState } from "react";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TActionClass } from "@formbricks/types/action-classes";
@@ -35,6 +36,7 @@ export const WhenToSendCard = ({
propActionClasses,
membershipRole,
}: WhenToSendCardProps) => {
const t = useTranslations();
const [open, setOpen] = useState(localSurvey.type === "app" ? true : false);
const [isAddActionModalOpen, setAddActionModalOpen] = useState(false);
const [actionClasses, setActionClasses] = useState<TActionClass[]>(propActionClasses);
@@ -165,8 +167,10 @@ export const WhenToSendCard = ({
</div>
<div>
<p className="font-semibold text-slate-800">Survey Trigger</p>
<p className="mt-1 text-sm text-slate-500">Choose the actions which trigger the survey.</p>
<p className="font-semibold text-slate-800">{t("environments.surveys.edit.survey_trigger")}</p>
<p className="mt-1 text-sm text-slate-500">
{t("environments.surveys.edit.choose_the_actions_which_trigger_the_survey")}
</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
@@ -177,7 +181,7 @@ export const WhenToSendCard = ({
<div className="px-3 pb-3 pt-1">
<div className="filter-scrollbar flex flex-col gap-4 overflow-auto rounded-lg border border-slate-300 bg-slate-50 p-4">
<p className="text-sm font-semibold text-slate-800">
Trigger survey when one of the actions is fired...
{t("environments.surveys.edit.trigger_survey_when_one_of_the_actions_is_fired")}
</p>
{localSurvey.triggers.filter(Boolean).map((trigger, idx) => {
@@ -207,14 +211,14 @@ export const WhenToSendCard = ({
)}
{trigger.actionClass.type === "code" && (
<span className="mr-1 border-l border-slate-400 pl-1 first:border-l-0 first:pl-0">
Key: <b>{trigger.actionClass.key}</b>
{t("environments.surveys.edit.key")}: <b>{trigger.actionClass.key}</b>
</span>
)}
{trigger.actionClass.type === "noCode" &&
trigger.actionClass.noCodeConfig?.type === "click" &&
trigger.actionClass.noCodeConfig?.elementSelector.cssSelector && (
<span className="mr-1 border-l border-slate-400 pl-1 first:border-l-0 first:pl-0">
CSS Selector:{" "}
{t("environments.surveys.edit.css_selector")}:{" "}
<b>{trigger.actionClass.noCodeConfig?.elementSelector.cssSelector}</b>
</span>
)}
@@ -222,7 +226,7 @@ export const WhenToSendCard = ({
trigger.actionClass.noCodeConfig?.type === "click" &&
trigger.actionClass.noCodeConfig?.elementSelector.innerHtml && (
<span className="mr-1 border-l border-slate-400 pl-1 first:border-l-0 first:pl-0">
Inner Text:{" "}
{t("environments.surveys.edit.inner_text")}:{" "}
<b>{trigger.actionClass.noCodeConfig?.elementSelector.innerHtml}</b>
</span>
)}
@@ -230,7 +234,7 @@ export const WhenToSendCard = ({
trigger.actionClass.noCodeConfig?.urlFilters &&
trigger.actionClass.noCodeConfig.urlFilters.length > 0 ? (
<span className="mr-1 border-l border-slate-400 pl-1 first:border-l-0 first:pl-0">
URL Filters:{" "}
{t("environments.surveys.edit.url_filters")}:{" "}
{trigger.actionClass.noCodeConfig.urlFilters.map((urlFilter, index) => (
<span key={index}>
{urlFilter.rule} <b>{urlFilter.value}</b>
@@ -261,29 +265,35 @@ export const WhenToSendCard = ({
setAddActionModalOpen(true);
}}>
<PlusIcon className="mr-2 h-4 w-4" />
Add action
{t("common.add_action")}
</Button>
</div>
</div>
{/* Survey Display Settings */}
<div className="mb-4 mt-8 space-y-1 px-4">
<h3 className="font-semibold text-slate-800">Survey Display Settings</h3>
<p className="text-sm text-slate-500">Add a delay or auto-close the survey</p>
<h3 className="font-semibold text-slate-800">
{t("environments.surveys.edit.survey_display_settings")}
</h3>
<p className="text-sm text-slate-500">
{t("environments.surveys.edit.add_a_delay_or_auto_close_the_survey")}
</p>
</div>
<AdvancedOptionToggle
htmlId="delay"
isChecked={delay}
onToggle={handleDelayToggle}
title="Add delay before showing survey"
description="Wait a few seconds after the trigger before showing the survey"
title={t("environments.surveys.edit.add_delay_before_showing_survey")}
description={t(
"environments.surveys.edit.wait_a_few_seconds_after_the_trigger_before_showing_the_survey"
)}
childBorder={true}>
<label
htmlFor="triggerDelay"
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
<div>
<p className="text-sm font-semibold text-slate-700">
Wait
{t("environments.surveys.edit.wait")}
<Input
type="number"
min="0"
@@ -292,7 +302,7 @@ export const WhenToSendCard = ({
onChange={(e) => handleTriggerDelay(e)}
className="ml-2 mr-2 inline w-16 bg-white text-center text-sm"
/>
seconds before showing the survey.
{t("environments.surveys.edit.seconds_before_showing_the_survey")}
</p>
</div>
</label>
@@ -301,12 +311,14 @@ export const WhenToSendCard = ({
htmlId="autoClose"
isChecked={autoClose}
onToggle={handleAutoCloseToggle}
title="Auto close on inactivity"
description="Automatically close the survey if the user does not respond after certain number of seconds"
title={t("environments.surveys.edit.auto_close_on_inactivity")}
description={t(
"environments.surveys.edit.automatically_close_the_survey_if_the_user_does_not_respond_after_certain_number_of_seconds"
)}
childBorder={true}>
<label htmlFor="autoCloseSeconds" className="cursor-pointer p-4">
<p className="text-sm font-semibold text-slate-700">
Automatically close survey after
{t("environments.surveys.edit.automatically_close_survey_after")}
<Input
type="number"
min="1"
@@ -315,7 +327,9 @@ export const WhenToSendCard = ({
onChange={(e) => handleInputSeconds(e)}
className="mx-2 inline w-16 bg-white text-center text-sm"
/>
seconds with no initial interaction.
{t(
"environments.surveys.edit.seconds_after_trigger_the_survey_will_be_closed_if_no_response"
)}
</p>
</label>
</AdvancedOptionToggle>
@@ -323,12 +337,14 @@ export const WhenToSendCard = ({
htmlId="randomizer"
isChecked={randomizerToggle}
onToggle={handleDisplayPercentageToggle}
title="Show survey to % of users"
description="Only display the survey to a subset of the users"
title={t("environments.surveys.edit.show_survey_to_users")}
description={t("environments.surveys.edit.only_display_the_survey_to_a_subset_of_the_users")}
childBorder={true}>
<label htmlFor="small-range" className="cursor-pointer p-4">
<p className="text-sm font-semibold text-slate-700">
Show to {localSurvey.displayPercentage}% of targeted users
{t("environments.surveys.edit.show_to_x_percentage_of_targeted_users", {
percentage: localSurvey.displayPercentage,
})}
<Input
id="small-range"
type="number"

View File

@@ -5,43 +5,43 @@ export const logicRules = {
[`${TSurveyQuestionTypeEnum.OpenText}.text`]: {
options: [
{
label: "equals",
label: "environments.surveys.edit.equals",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "does not equal",
label: "environments.surveys.edit.does_not_equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "contains",
label: "environments.surveys.edit.contains",
value: ZSurveyLogicConditionsOperator.Enum.contains,
},
{
label: "does not contain",
label: "environments.surveys.edit.does_not_contain",
value: ZSurveyLogicConditionsOperator.Enum.doesNotContain,
},
{
label: "starts with",
label: "environments.surveys.edit.starts_with",
value: ZSurveyLogicConditionsOperator.Enum.startsWith,
},
{
label: "does not start with",
label: "environments.surveys.edit.does_not_start_with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotStartWith,
},
{
label: "ends with",
label: "environments.surveys.edit.ends_with",
value: ZSurveyLogicConditionsOperator.Enum.endsWith,
},
{
label: "does not end with",
label: "environments.surveys.edit.does_not_end_with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEndWith,
},
{
label: "is submitted",
label: "environments.surveys.edit.is_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -73,11 +73,11 @@ export const logicRules = {
value: ZSurveyLogicConditionsOperator.Enum.isLessThanOrEqual,
},
{
label: "is submitted",
label: "environments.surveys.edit.is_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -85,23 +85,23 @@ export const logicRules = {
[TSurveyQuestionTypeEnum.MultipleChoiceSingle]: {
options: [
{
label: "equals",
label: "environments.surveys.edit.equals",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "does not equal",
label: "environments.surveys.edit.does_not_equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "equals one of",
label: "environments.surveys.edit.equals_one_of",
value: ZSurveyLogicConditionsOperator.Enum.equalsOneOf,
},
{
label: "is submitted",
label: "environments.surveys.edit.is_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -109,27 +109,27 @@ export const logicRules = {
[TSurveyQuestionTypeEnum.MultipleChoiceMulti]: {
options: [
{
label: "equals",
label: "environments.surveys.edit.equals",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "does not equal",
label: "environments.surveys.edit.does_not_equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "includes all of",
label: "environments.surveys.edit.includes_all_of",
value: ZSurveyLogicConditionsOperator.Enum.includesAllOf,
},
{
label: "includes one of",
label: "environments.surveys.edit.includes_one_of",
value: ZSurveyLogicConditionsOperator.Enum.includesOneOf,
},
{
label: "is submitted",
label: "environments.surveys.edit.is_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -137,27 +137,27 @@ export const logicRules = {
[TSurveyQuestionTypeEnum.PictureSelection]: {
options: [
{
label: "equals",
label: "environments.surveys.edit.equals",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "does not equal",
label: "environments.surveys.edit.does_not_equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "includes all of",
label: "environments.surveys.edit.includes_all_of",
value: ZSurveyLogicConditionsOperator.Enum.includesAllOf,
},
{
label: "includes one of",
label: "environments.surveys.edit.includes_one_of",
value: ZSurveyLogicConditionsOperator.Enum.includesOneOf,
},
{
label: "is submitted",
label: "environments.surveys.edit.is_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -189,11 +189,11 @@ export const logicRules = {
value: ZSurveyLogicConditionsOperator.Enum.isLessThanOrEqual,
},
{
label: "is submitted",
label: "environments.surveys.edit.is_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -225,11 +225,11 @@ export const logicRules = {
value: ZSurveyLogicConditionsOperator.Enum.isLessThanOrEqual,
},
{
label: "is submitted",
label: "environments.surveys.edit.is_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -237,11 +237,11 @@ export const logicRules = {
[TSurveyQuestionTypeEnum.CTA]: {
options: [
{
label: "is clicked",
label: "environments.surveys.edit.is_clicked",
value: ZSurveyLogicConditionsOperator.Enum.isClicked,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -249,11 +249,11 @@ export const logicRules = {
[TSurveyQuestionTypeEnum.Consent]: {
options: [
{
label: "is accepted",
label: "environments.surveys.edit.is_accepted",
value: ZSurveyLogicConditionsOperator.Enum.isAccepted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -261,27 +261,27 @@ export const logicRules = {
[TSurveyQuestionTypeEnum.Date]: {
options: [
{
label: "equals",
label: "environments.surveys.edit.e quals",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "does not equal",
label: "environments.surveys.edit.does_not_equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "is before",
label: "environments.surveys.edit.is_before",
value: ZSurveyLogicConditionsOperator.Enum.isBefore,
},
{
label: "is after",
label: "environments.surveys.edit.is_after",
value: ZSurveyLogicConditionsOperator.Enum.isAfter,
},
{
label: "is submitted",
label: "environments.surveys.edit.is_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -289,11 +289,11 @@ export const logicRules = {
[TSurveyQuestionTypeEnum.FileUpload]: {
options: [
{
label: "is submitted",
label: "environments.surveys.edit.is_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -301,11 +301,11 @@ export const logicRules = {
[TSurveyQuestionTypeEnum.Ranking]: {
options: [
{
label: "is submitted",
label: "environments.surveys.edit.is_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -313,11 +313,11 @@ export const logicRules = {
[TSurveyQuestionTypeEnum.Cal]: {
options: [
{
label: "is booked",
label: "environments.surveys.edit.is_booked",
value: ZSurveyLogicConditionsOperator.Enum.isBooked,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -325,15 +325,15 @@ export const logicRules = {
[TSurveyQuestionTypeEnum.Matrix]: {
options: [
{
label: "is partially submitted",
label: "environments.surveys.edit.is_partially_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isPartiallySubmitted,
},
{
label: "is completely submitted",
label: "environments.surveys.edit.is_completely_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isCompletelySubmitted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -341,11 +341,11 @@ export const logicRules = {
[TSurveyQuestionTypeEnum.Address]: {
options: [
{
label: "is submitted",
label: "environments.surveys.edit.is_submitted",
value: ZSurveyLogicConditionsOperator.Enum.isSubmitted,
},
{
label: "is skipped",
label: "environments.surveys.edit.is_skipped",
value: ZSurveyLogicConditionsOperator.Enum.isSkipped,
},
],
@@ -354,35 +354,35 @@ export const logicRules = {
["variable.text"]: {
options: [
{
label: "equals",
label: "environments.surveys.edit.equals",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "does not equal",
label: "environments.surveys.edit.does_not_equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "contains",
label: "environments.surveys.edit.contains",
value: ZSurveyLogicConditionsOperator.Enum.contains,
},
{
label: "does not contain",
label: "environments.surveys.edit.does_not_contain",
value: ZSurveyLogicConditionsOperator.Enum.doesNotContain,
},
{
label: "starts with",
label: "environments.surveys.edit.starts_with",
value: ZSurveyLogicConditionsOperator.Enum.startsWith,
},
{
label: "does not start with",
label: "environments.surveys.edit.does_not_start_with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotStartWith,
},
{
label: "ends with",
label: "environments.surveys.edit.ends_with",
value: ZSurveyLogicConditionsOperator.Enum.endsWith,
},
{
label: "does not end with",
label: "environments.surveys.edit.does_not_end_with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEndWith,
},
],
@@ -418,35 +418,35 @@ export const logicRules = {
hiddenField: {
options: [
{
label: "equals",
label: "environments.surveys.edit.equals",
value: ZSurveyLogicConditionsOperator.Enum.equals,
},
{
label: "does not equal",
label: "environments.surveys.edit.does_not_equal",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEqual,
},
{
label: "contains",
label: "environments.surveys.edit.contains",
value: ZSurveyLogicConditionsOperator.Enum.contains,
},
{
label: "does not contain",
label: "environments.surveys.edit.does_not_contain",
value: ZSurveyLogicConditionsOperator.Enum.doesNotContain,
},
{
label: "starts with",
label: "environments.surveys.edit.starts_with",
value: ZSurveyLogicConditionsOperator.Enum.startsWith,
},
{
label: "does not start with",
label: "environments.surveys.edit.does_not_start_with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotStartWith,
},
{
label: "ends with",
label: "environments.surveys.edit.ends_with",
value: ZSurveyLogicConditionsOperator.Enum.endsWith,
},
{
label: "does not end with",
label: "environments.surveys.edit.does_not_end_with",
value: ZSurveyLogicConditionsOperator.Enum.doesNotEndWith,
},
],

View File

@@ -2,7 +2,7 @@ import { EyeOffIcon, FileDigitIcon, FileType2Icon } from "lucide-react";
import { HTMLInputTypeAttribute } from "react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { isConditionGroup } from "@formbricks/lib/surveyLogic/utils";
import { questionTypes } from "@formbricks/lib/utils/questions";
import { getQuestionTypes } from "@formbricks/lib/utils/questions";
import { recallToHeadline } from "@formbricks/lib/utils/recall";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import {
@@ -41,7 +41,7 @@ export const formatTextWithSlashes = (text: string) => {
});
};
const questionIconMapping = questionTypes.reduce(
const questionIconMapping = getQuestionTypes("en-US").reduce(
(prev, curr) => ({
...prev,
[curr.id]: curr.icon,
@@ -51,7 +51,8 @@ const questionIconMapping = questionTypes.reduce(
export const getConditionValueOptions = (
localSurvey: TSurvey,
currQuestionIdx: number
currQuestionIdx: number,
t: (key: string) => string
): TComboboxGroupedOption[] => {
const hiddenFields = localSurvey.hiddenFields?.fieldIds ?? [];
const variables = localSurvey.variables ?? [];
@@ -95,7 +96,7 @@ export const getConditionValueOptions = (
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
label: t("common.questions"),
value: "questions",
options: questionOptions,
});
@@ -103,7 +104,7 @@ export const getConditionValueOptions = (
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
label: t("common.variables"),
value: "variables",
options: variableOptions,
});
@@ -111,7 +112,7 @@ export const getConditionValueOptions = (
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
label: t("common.hidden_fields"),
value: "hiddenFields",
options: hiddenFieldsOptions,
});
@@ -141,9 +142,9 @@ export const replaceEndingCardHeadlineRecall = (
};
export const actionObjectiveOptions: TComboboxOption[] = [
{ label: "Calculate", value: "calculate" },
{ label: "Require Answer", value: "requireAnswer" },
{ label: "Jump to question", value: "jumpToQuestion" },
{ label: "environments.surveys.edit.calculate", value: "calculate" },
{ label: "environments.surveys.edit.require_answer", value: "requireAnswer" },
{ label: "environments.surveys.edit.jump_to_question", value: "jumpToQuestion" },
];
const getQuestionOperatorOptions = (question: TSurveyQuestion): TComboboxOption[] => {
@@ -193,7 +194,8 @@ export const getConditionOperatorOptions = (
export const getMatchValueProps = (
condition: TSingleCondition,
localSurvey: TSurvey
localSurvey: TSurvey,
t: (key: string) => string
): {
show?: boolean;
showInput?: boolean;
@@ -290,7 +292,7 @@ export const getMatchValueProps = (
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
label: t("common.questions"),
value: "questions",
options: questionOptions,
});
@@ -298,7 +300,7 @@ export const getMatchValueProps = (
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
label: t("common.variables"),
value: "variables",
options: variableOptions,
});
@@ -306,7 +308,7 @@ export const getMatchValueProps = (
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
label: t("common.hidden_fields"),
value: "hiddenFields",
options: hiddenFieldsOptions,
});
@@ -334,13 +336,13 @@ export const getMatchValueProps = (
return {
show: true,
showInput: false,
options: [{ label: "Choices", value: "choices", options: choices }],
options: [{ label: t("common.choices"), value: "choices", options: choices }],
};
} else if (selectedQuestion?.type === TSurveyQuestionTypeEnum.PictureSelection) {
const choices = selectedQuestion.choices.map((choice, idx) => {
return {
imgSrc: choice.imageUrl,
label: `Picture ${idx + 1}`,
label: `${t("environments.surveys.edit.picture_idx")} ${idx + 1}`,
value: choice.id,
meta: {
type: "static",
@@ -351,7 +353,7 @@ export const getMatchValueProps = (
return {
show: true,
showInput: false,
options: [{ label: "Choices", value: "choices", options: choices }],
options: [{ label: t("common.choices"), value: "choices", options: choices }],
};
} else if (selectedQuestion?.type === TSurveyQuestionTypeEnum.Rating) {
const choices = Array.from({ length: selectedQuestion.range }, (_, idx) => {
@@ -381,7 +383,7 @@ export const getMatchValueProps = (
if (choices.length > 0) {
groupedOptions.push({
label: "Choices",
label: t("common.choices"),
value: "choices",
options: choices,
});
@@ -389,7 +391,7 @@ export const getMatchValueProps = (
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
label: t("common.variables"),
value: "variables",
options: variableOptions,
});
@@ -428,7 +430,7 @@ export const getMatchValueProps = (
if (choices.length > 0) {
groupedOptions.push({
label: "Choices",
label: t("common.choices"),
value: "choices",
options: choices,
});
@@ -436,7 +438,7 @@ export const getMatchValueProps = (
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
label: t("common.variables"),
value: "variables",
options: variableOptions,
});
@@ -491,7 +493,7 @@ export const getMatchValueProps = (
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
label: t("common.questions"),
value: "questions",
options: questionOptions,
});
@@ -499,7 +501,7 @@ export const getMatchValueProps = (
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
label: t("common.variables"),
value: "variables",
options: variableOptions,
});
@@ -507,7 +509,7 @@ export const getMatchValueProps = (
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
label: t("common.hidden_fields"),
value: "hiddenFields",
options: hiddenFieldsOptions,
});
@@ -572,7 +574,7 @@ export const getMatchValueProps = (
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
label: t("common.questions"),
value: "questions",
options: questionOptions,
});
@@ -580,7 +582,7 @@ export const getMatchValueProps = (
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
label: t("common.variables"),
value: "variables",
options: variableOptions,
});
@@ -588,7 +590,7 @@ export const getMatchValueProps = (
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
label: t("common.hidden_fields"),
value: "hiddenFields",
options: hiddenFieldsOptions,
});
@@ -644,7 +646,7 @@ export const getMatchValueProps = (
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
label: t("common.questions"),
value: "questions",
options: questionOptions,
});
@@ -652,7 +654,7 @@ export const getMatchValueProps = (
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
label: t("common.variables"),
value: "variables",
options: variableOptions,
});
@@ -660,7 +662,7 @@ export const getMatchValueProps = (
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
label: t("common.hidden_fields"),
value: "hiddenFields",
options: hiddenFieldsOptions,
});
@@ -724,7 +726,7 @@ export const getMatchValueProps = (
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
label: t("common.questions"),
value: "questions",
options: questionOptions,
});
@@ -732,7 +734,7 @@ export const getMatchValueProps = (
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
label: t("common.variables"),
value: "variables",
options: variableOptions,
});
@@ -740,7 +742,7 @@ export const getMatchValueProps = (
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
label: t("common.hidden_fields"),
value: "hiddenFields",
options: hiddenFieldsOptions,
});
@@ -760,7 +762,8 @@ export const getMatchValueProps = (
export const getActionTargetOptions = (
action: TSurveyLogicAction,
localSurvey: TSurvey,
currQuestionIdx: number
currQuestionIdx: number,
t: (key: string) => string
): TComboboxOption[] => {
let questions = localSurvey.questions.filter((_, idx) => idx !== currQuestionIdx);
@@ -782,8 +785,8 @@ export const getActionTargetOptions = (
return {
label:
ending.type === "endScreen"
? getLocalizedValue(ending.headline, "default") || "End Screen"
: ending.label || "Redirect Thank you card",
? getLocalizedValue(ending.headline, "default") || t("environments.surveys.edit.end_screen_card")
: ending.label || t("environments.surveys.edit.redirect_thank_you_card"),
value: ending.id,
};
});
@@ -806,38 +809,41 @@ export const getActionVariableOptions = (localSurvey: TSurvey): TComboboxOption[
});
};
export const getActionOperatorOptions = (variableType?: TSurveyVariable["type"]): TComboboxOption[] => {
export const getActionOperatorOptions = (
t: (key: string) => string,
variableType?: TSurveyVariable["type"]
): TComboboxOption[] => {
if (variableType === "number") {
return [
{
label: "Add +",
label: t("environments.surveys.edit.add"),
value: "add",
},
{
label: "Subtract -",
label: t("environments.surveys.edit.subtract"),
value: "subtract",
},
{
label: "Multiply *",
label: t("environments.surveys.edit.multiply"),
value: "multiply",
},
{
label: "Divide /",
label: t("environments.surveys.edit.divide"),
value: "divide",
},
{
label: "Assign =",
label: t("environments.surveys.edit.assign"),
value: "assign",
},
];
} else if (variableType === "text") {
return [
{
label: "Assign =",
label: t("environments.surveys.edit.assign"),
value: "assign",
},
{
label: "Concat +",
label: t("environments.surveys.edit.concat"),
value: "concat",
},
];
@@ -845,7 +851,11 @@ export const getActionOperatorOptions = (variableType?: TSurveyVariable["type"])
return [];
};
export const getActionValueOptions = (variableId: string, localSurvey: TSurvey): TComboboxGroupedOption[] => {
export const getActionValueOptions = (
variableId: string,
localSurvey: TSurvey,
t: (key: string) => string
): TComboboxGroupedOption[] => {
const hiddenFields = localSurvey.hiddenFields?.fieldIds ?? [];
let variables = localSurvey.variables ?? [];
const questions = localSurvey.questions;
@@ -906,7 +916,7 @@ export const getActionValueOptions = (variableId: string, localSurvey: TSurvey):
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
label: t("common.questions"),
value: "questions",
options: questionOptions,
});
@@ -914,7 +924,7 @@ export const getActionValueOptions = (variableId: string, localSurvey: TSurvey):
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
label: t("common.variables"),
value: "variables",
options: variableOptions,
});
@@ -922,7 +932,7 @@ export const getActionValueOptions = (variableId: string, localSurvey: TSurvey):
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
label: t("common.hidden_fields"),
value: "hiddenFields",
options: hiddenFieldsOptions,
});
@@ -962,7 +972,7 @@ export const getActionValueOptions = (variableId: string, localSurvey: TSurvey):
if (questionOptions.length > 0) {
groupedOptions.push({
label: "Questions",
label: t("common.questions"),
value: "questions",
options: questionOptions,
});
@@ -970,7 +980,7 @@ export const getActionValueOptions = (variableId: string, localSurvey: TSurvey):
if (variableOptions.length > 0) {
groupedOptions.push({
label: "Variables",
label: t("common.variables"),
value: "variables",
options: variableOptions,
});
@@ -978,7 +988,7 @@ export const getActionValueOptions = (variableId: string, localSurvey: TSurvey):
if (hiddenFieldsOptions.length > 0) {
groupedOptions.push({
label: "Hidden Fields",
label: t("common.hidden_fields"),
value: "hiddenFields",
options: hiddenFieldsOptions,
});

View File

@@ -198,10 +198,10 @@ export const isEndingCardValid = (
}
};
export const isSurveyValid = (survey: TSurvey, selectedLanguageCode: string) => {
export const isSurveyValid = (survey: TSurvey, selectedLanguageCode: string, t: (key: string) => string) => {
const questionWithEmptyFallback = checkForEmptyFallBackValue(survey, selectedLanguageCode);
if (questionWithEmptyFallback) {
toast.error("Fallback missing");
toast.error(t("environments.surveys.edit.fallback_missing"));
return false;
}
@@ -212,7 +212,7 @@ export const isSurveyValid = (survey: TSurvey, selectedLanguageCode: string) =>
if (!parsedFilters.success) {
const errMsg =
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message ||
"Invalid targeting: Please check your audience filters";
t("environments.surveys.edit.invalid_targeting");
toast.error(errMsg);
return;
}

View File

@@ -1,9 +1,15 @@
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { getAdvancedTargetingPermission, getMultiLanguagePermission } from "@formbricks/ee/lib/service";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD, SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
import {
DEFAULT_LOCALE,
IS_FORMBRICKS_CLOUD,
SURVEY_BG_COLORS,
UNSPLASH_ACCESS_KEY,
} from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
@@ -12,6 +18,7 @@ import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { getSegments } from "@formbricks/lib/segment/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { getUserLocale } from "@formbricks/lib/user/service";
import { ErrorComponent } from "@formbricks/ui/components/ErrorComponent";
import { SurveyEditor } from "./components/SurveyEditor";
@@ -23,6 +30,7 @@ export const generateMetadata = async ({ params }) => {
};
const Page = async ({ params, searchParams }) => {
const t = await getTranslations();
const [
survey,
product,
@@ -45,17 +53,17 @@ const Page = async ({ params, searchParams }) => {
getSegments(params.environmentId),
]);
if (!session) {
throw new Error("Session not found");
throw new Error(t("common.session_not_found"));
}
if (!organization) {
throw new Error("Organization not found");
throw new Error(t("common.organization_not_found"));
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
const isSurveyCreationDeletionDisabled = isViewer;
const locale = session.user.id ? await getUserLocale(session.user.id) : undefined;
const isUserTargetingAllowed = await getAdvancedTargetingPermission(organization);
const isMultiLanguageAllowed = await getMultiLanguagePermission(organization);
@@ -89,6 +97,7 @@ const Page = async ({ params, searchParams }) => {
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
isUnsplashConfigured={UNSPLASH_ACCESS_KEY ? true : false}
isCxMode={isCxMode}
locale={locale ?? DEFAULT_LOCALE}
/>
);
};

View File

@@ -1,7 +1,7 @@
import { getDefaultEndingCard, welcomeCardDefault } from "@formbricks/lib/templates";
import { getDefaultEndingCard, getDefaultWelcomeCard } from "@formbricks/lib/templates";
import { TSurvey } from "@formbricks/types/surveys/types";
export const minimalSurvey: TSurvey = {
export const getMinimalSurvey = (locale: string): TSurvey => ({
id: "someUniqueId1",
createdAt: new Date(),
updatedAt: new Date(),
@@ -15,9 +15,9 @@ export const minimalSurvey: TSurvey = {
triggers: [],
recontactDays: null,
displayLimit: null,
welcomeCard: welcomeCardDefault,
welcomeCard: getDefaultWelcomeCard(locale),
questions: [],
endings: [getDefaultEndingCard([])],
endings: [getDefaultEndingCard([], locale)],
hiddenFields: {
enabled: false,
},
@@ -39,4 +39,4 @@ export const minimalSurvey: TSurvey = {
isVerifyEmailEnabled: false,
isSingleResponsePerEmailEnabled: false,
variables: [],
};
});

View File

@@ -1,11 +1,13 @@
"use client";
import { ArrowLeftIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { Button } from "@formbricks/ui/components/Button";
export const BackButton = () => {
const router = useRouter();
const t = useTranslations();
return (
<Button
variant="secondary"
@@ -14,7 +16,7 @@ export const BackButton = () => {
onClick={() => {
router.back();
}}>
Back
{t("common.back")}
</Button>
);
};

View File

@@ -2,6 +2,7 @@
import { createAISurveyAction } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/actions";
import { Sparkles } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
@@ -22,6 +23,7 @@ interface FormbricksAICardProps {
}
export const FormbricksAICard = ({ environmentId }: FormbricksAICardProps) => {
const t = useTranslations();
const router = useRouter();
const [aiPrompt, setAiPrompt] = useState("");
const [isLoading, setIsLoading] = useState(false);
@@ -50,16 +52,14 @@ export const FormbricksAICard = ({ environmentId }: FormbricksAICardProps) => {
<Card className="mx-auto w-full bg-gradient-to-tr from-slate-100 to-slate-200">
<CardHeader>
<CardTitle className="text-2xl font-bold">Formbricks AI</CardTitle>
<CardDescription>
Describe your survey and let Formbricks AI create the survey for you
</CardDescription>
<CardDescription>{t("environments.surveys.templates.formbricks_ai_description")}</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<Textarea
className="bg-slate-50"
id="ai-prompt"
placeholder="Enter survey information (e.g. key topics to cover)"
placeholder={t("environments.surveys.templates.formbricks_ai_prompt_placeholder")}
value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)}
required
@@ -75,7 +75,7 @@ export const FormbricksAICard = ({ environmentId }: FormbricksAICardProps) => {
variant="secondary"
loading={isLoading}>
<Sparkles className="mr-2 h-4 w-4" />
Generate
{t("environments.surveys.templates.formbricks_ai_generate")}
</Button>
</CardFooter>
</Card>

View File

@@ -2,8 +2,9 @@
import { FormbricksAICard } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/FormbricksAICard";
import { MenuBar } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/MenuBar";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { customSurvey } from "@formbricks/lib/templates";
import { getCustomSurveyTemplate } from "@formbricks/lib/templates";
import type { TEnvironment } from "@formbricks/types/environment";
import type { TProduct, TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
import type { TTemplate, TTemplateRole } from "@formbricks/types/templates";
@@ -12,7 +13,7 @@ import { PreviewSurvey } from "@formbricks/ui/components/PreviewSurvey";
import { SearchBar } from "@formbricks/ui/components/SearchBar";
import { Separator } from "@formbricks/ui/components/Separator";
import { TemplateList } from "@formbricks/ui/components/TemplateList";
import { minimalSurvey } from "../../lib/minimalSurvey";
import { getMinimalSurvey } from "../../lib/minimalSurvey";
type TemplateContainerWithPreviewProps = {
environmentId: string;
@@ -30,7 +31,8 @@ export const TemplateContainerWithPreview = ({
prefilledFilters,
isAIEnabled,
}: TemplateContainerWithPreviewProps) => {
const initialTemplate = customSurvey;
const t = useTranslations();
const initialTemplate = getCustomSurveyTemplate(user.locale);
const [activeTemplate, setActiveTemplate] = useState<TTemplate>(initialTemplate);
const [activeQuestionId, setActiveQuestionId] = useState<string>(initialTemplate.preset.questions[0].id);
const [templateSearch, setTemplateSearch] = useState<string | null>(null);
@@ -41,12 +43,14 @@ export const TemplateContainerWithPreview = ({
<div className="relative z-0 flex flex-1 overflow-hidden">
<div className="flex-1 flex-col overflow-auto bg-slate-50">
<div className="mb-3 ml-6 mt-6 flex flex-col items-center justify-between md:flex-row md:items-end">
<h1 className="text-2xl font-bold text-slate-800">Create a new survey</h1>
<h1 className="text-2xl font-bold text-slate-800">
{t("environments.surveys.templates.create_a_new_survey")}
</h1>
<div className="px-6">
<SearchBar
value={templateSearch ?? ""}
onChange={setTemplateSearch}
placeholder={"Search..."}
placeholder={t("common.search")}
className="border-slate-700"
/>
</div>
@@ -76,7 +80,7 @@ export const TemplateContainerWithPreview = ({
<aside className="group hidden flex-1 flex-shrink-0 items-center justify-center overflow-hidden border-l border-slate-100 bg-slate-50 md:flex md:flex-col">
{activeTemplate && (
<PreviewSurvey
survey={{ ...minimalSurvey, ...activeTemplate.preset }}
survey={{ ...getMinimalSurvey(user.locale), ...activeTemplate.preset }}
questionId={activeQuestionId}
product={product}
environment={environment}

View File

@@ -1,4 +1,5 @@
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
@@ -22,11 +23,12 @@ interface SurveyTemplateProps {
}
const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
const t = await getTranslations();
const session = await getServerSession(authOptions);
const environmentId = params.environmentId;
if (!session) {
throw new Error("Session not found");
throw new Error(t("common.session_not_found"));
}
const [user, environment, product] = await Promise.all([
@@ -36,15 +38,15 @@ const Page = async ({ params, searchParams }: SurveyTemplateProps) => {
]);
if (!user) {
throw new Error("User not found");
throw new Error(t("common.user_not_found"));
}
if (!product) {
throw new Error("Product not found");
throw new Error(t("common.product_not_found"));
}
if (!environment) {
throw new Error("Environment not found");
throw new Error(t("common.environment_not_found"));
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(
session?.user.id,

View File

@@ -1,5 +1,6 @@
"use client";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { Button } from "@formbricks/ui/components/Button";
import { Confetti } from "@formbricks/ui/components/Confetti";
@@ -9,6 +10,7 @@ interface ConfirmationPageProps {
}
export const ConfirmationPage = ({ environmentId }: ConfirmationPageProps) => {
const t = useTranslations();
const [showConfetti, setShowConfetti] = useState(false);
useEffect(() => {
setShowConfetti(true);
@@ -19,13 +21,15 @@ export const ConfirmationPage = ({ environmentId }: ConfirmationPageProps) => {
{showConfetti && <Confetti />}
<div className="mx-auto max-w-sm py-8 sm:px-6 lg:px-8">
<div className="my-6 sm:flex-auto">
<h1 className="text-center text-xl font-semibold text-slate-900">Upgrade successful</h1>
<h1 className="text-center text-xl font-semibold text-slate-900">
{t("billing_confirmation.upgrade_successful")}
</h1>
<p className="mt-2 text-center text-sm text-slate-700">
Thanks a lot for upgrading your Formbricks subscription.
{t("billing_confirmation.thanks_for_upgrading")}
</p>
</div>
<Button className="w-full justify-center" href={`/environments/${environmentId}/settings/billing`}>
Back to billing overview
{t("billing_confirmation.back_to_billing_overview")}
</Button>
</div>
</div>

View File

@@ -1,6 +1,7 @@
"use client";
import { TagIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
import { convertDateTimeStringShort } from "@formbricks/lib/time";
@@ -20,7 +21,7 @@ export const AttributeActivityTab = ({ attributeClass }: EventActivityTabProps)
const [inactiveSurveys, setInactiveSurveys] = useState<string[] | undefined>();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const t = useTranslations();
useEffect(() => {
setLoading(true);
@@ -51,7 +52,7 @@ export const AttributeActivityTab = ({ attributeClass }: EventActivityTabProps)
<div className="grid grid-cols-3 pb-2">
<div className="col-span-2 space-y-4 pr-6">
<div>
<Label className="text-slate-500">Active surveys</Label>
<Label className="text-slate-500">{t("common.active_surveys")}</Label>
{activeSurveys?.length === 0 && <p className="text-sm text-slate-900">-</p>}
{activeSurveys?.map((surveyName) => (
<p key={surveyName} className="text-sm text-slate-900">
@@ -60,7 +61,7 @@ export const AttributeActivityTab = ({ attributeClass }: EventActivityTabProps)
))}
</div>
<div>
<Label className="text-slate-500">Inactive surveys</Label>
<Label className="text-slate-500">{t("common.inactive_surveys")}</Label>
{inactiveSurveys?.length === 0 && <p className="text-sm text-slate-900">-</p>}
{inactiveSurveys?.map((surveyName) => (
<p key={surveyName} className="text-sm text-slate-900">
@@ -71,19 +72,19 @@ export const AttributeActivityTab = ({ attributeClass }: EventActivityTabProps)
</div>
<div className="col-span-1 space-y-3 rounded-lg border border-slate-100 bg-slate-50 p-2">
<div>
<Label className="text-xs font-normal text-slate-500">Created on</Label>
<Label className="text-xs font-normal text-slate-500">{t("common.created_at")}</Label>
<p className="text-xs text-slate-700">
{convertDateTimeStringShort(attributeClass.createdAt.toString())}
</p>
</div>{" "}
<div>
<Label className="text-xs font-normal text-slate-500">Last updated</Label>
<Label className="text-xs font-normal text-slate-500">{t("common.updated_at")}</Label>
<p className="text-xs text-slate-700">
{convertDateTimeStringShort(attributeClass.updatedAt.toString())}
</p>
</div>
<div>
<Label className="block text-xs font-normal text-slate-500">Type</Label>
<Label className="block text-xs font-normal text-slate-500">{t("common.type")}</Label>
<div className="mt-1 flex items-center">
<div className="mr-1.5 h-4 w-4 text-slate-600">
<TagIcon className="h-4 w-4" />

View File

@@ -1,23 +1,23 @@
"use client";
import { useTranslations } from "next-intl";
import { useMemo, useState } from "react";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { TUserLocale } from "@formbricks/types/user";
import { Switch } from "@formbricks/ui/components/Switch";
import { AttributeDetailModal } from "./AttributeDetailModal";
import { AttributeClassDataRow } from "./AttributeRowData";
import { AttributeTableHeading } from "./AttributeTableHeading";
import { UploadAttributesModal } from "./UploadAttributesModal";
interface AttributeClassesTableProps {
attributeClasses: TAttributeClass[];
locale: TUserLocale;
}
export const AttributeClassesTable = ({ attributeClasses }: AttributeClassesTableProps) => {
export const AttributeClassesTable = ({ attributeClasses, locale }: AttributeClassesTableProps) => {
const [isAttributeDetailModalOpen, setAttributeDetailModalOpen] = useState(false);
const [isUploadCSVModalOpen, setUploadCSVModalOpen] = useState(false);
const [activeAttributeClass, setActiveAttributeClass] = useState<TAttributeClass | null>(null);
const [showArchived, setShowArchived] = useState(false);
const t = useTranslations();
const displayedAttributeClasses = useMemo(() => {
return attributeClasses
? showArchived
@@ -44,20 +44,24 @@ export const AttributeClassesTable = ({ attributeClasses }: AttributeClassesTabl
{hasArchived && (
<div className="my-4 flex items-center justify-end text-right">
<div className="flex items-center text-sm font-medium">
Show archived
{t("environments.attributes.show_archived")}
<Switch className="mx-3" checked={showArchived} onCheckedChange={toggleShowArchived} />
</div>
</div>
)}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<AttributeTableHeading />
<div className="grid h-12 grid-cols-5 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6">{t("common.name")}</div>
<div className="hidden text-center sm:block">{t("common.created_at")}</div>
<div className="hidden text-center sm:block">{t("common.updated_at")}</div>
</div>
<div className="grid-cols-7">
{displayedAttributeClasses.map((attributeClass, index) => (
<button
onClick={() => handleOpenAttributeDetailModalClick(attributeClass)}
className="w-full cursor-default"
key={attributeClass.id}>
<AttributeClassDataRow attributeClass={attributeClass} key={index} />
<AttributeClassDataRow attributeClass={attributeClass} key={index} locale={locale} />
</button>
))}
</div>
@@ -68,8 +72,6 @@ export const AttributeClassesTable = ({ attributeClasses }: AttributeClassesTabl
attributeClass={activeAttributeClass}
/>
)}
<UploadAttributesModal open={isUploadCSVModalOpen} setOpen={setUploadCSVModalOpen} />
</div>
</>
);

View File

@@ -1,4 +1,7 @@
"use client";
import { TagIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { TAttributeClass } from "@formbricks/types/attribute-classes";
import { ModalWithTabs } from "@formbricks/ui/components/ModalWithTabs";
import { AttributeActivityTab } from "./AttributeActivityTab";
@@ -11,13 +14,14 @@ interface AttributeDetailModalProps {
}
export const AttributeDetailModal = ({ open, setOpen, attributeClass }: AttributeDetailModalProps) => {
const t = useTranslations();
const tabs = [
{
title: "Activity",
title: t("common.activity"),
children: <AttributeActivityTab attributeClass={attributeClass} />,
},
{
title: "Settings",
title: t("common.settings"),
children: <AttributeSettingsTab attributeClass={attributeClass} setOpen={setOpen} />,
},
];

View File

@@ -1,8 +1,8 @@
import { TagIcon } from "lucide-react";
import { timeSinceConditionally } from "@formbricks/lib/time";
import { timeSince } from "@formbricks/lib/time";
import { Badge } from "@formbricks/ui/components/Badge";
export const AttributeClassDataRow = ({ attributeClass }) => {
export const AttributeClassDataRow = ({ attributeClass, locale }) => {
return (
<div className="m-2 grid h-16 cursor-pointer grid-cols-5 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
<div className="col-span-5 flex items-center pl-6 text-sm sm:col-span-3">
@@ -21,10 +21,10 @@ export const AttributeClassDataRow = ({ attributeClass }) => {
</div>
<div className="my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 md:block">
<div className="text-slate-900">{timeSinceConditionally(attributeClass.createdAt.toString())}</div>
<div className="text-slate-900">{timeSince(attributeClass.createdAt.toString(), locale)}</div>
</div>
<div className="my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 md:block">
<div className="text-slate-900">{timeSinceConditionally(attributeClass.updatedAt.toString())}</div>
<div className="text-slate-900">{timeSince(attributeClass.updatedAt.toString(), locale)}</div>
</div>
</div>
);

View File

@@ -2,6 +2,7 @@
import type { AttributeClass } from "@prisma/client";
import { ArchiveIcon, ArchiveXIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
@@ -17,6 +18,7 @@ interface AttributeSettingsTabProps {
export const AttributeSettingsTab = async ({ attributeClass, setOpen }: AttributeSettingsTabProps) => {
const router = useRouter();
const t = useTranslations();
const { register, handleSubmit } = useForm({
defaultValues: { name: attributeClass.name, description: attributeClass.description },
});
@@ -41,34 +43,36 @@ export const AttributeSettingsTab = async ({ attributeClass, setOpen }: Attribut
<div>
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
<div>
<Label className="text-slate-600">Name</Label>
<Label className="text-slate-600">{t("common.name")}</Label>
<Input
type="text"
placeholder="e.g. Product Organization Info"
placeholder={t("environments.attributes.ex_user_property")}
{...register("name", {
disabled: attributeClass.type === "automatic" || attributeClass.type === "code" ? true : false,
})}
/>
</div>
<div>
<Label className="text-slate-600">Description</Label>
<Label className="text-slate-600">{t("common.description")}</Label>
<Input
type="text"
placeholder="e.g. Triggers when user changed subscription"
placeholder={t("environments.attributes.ex_user_property")}
{...register("description", {
disabled: attributeClass.type === "automatic" ? true : false,
})}
/>
</div>
<div className="my-6">
<Label>Attribute Type</Label>
<Label>{t("common.attribute_type")}</Label>
{attributeClass.type === "code" ? (
<p className="text-sm text-slate-600">
This is a code attribute. You can only change the description.
{t("environments.attributes.this_is_a_code_attribute_you_can_only_change_the_description")}
</p>
) : attributeClass.type === "automatic" ? (
<p className="text-sm text-slate-600">
This attribute was added automatically. You cannot make changes to it.
{t(
"environments.attributes.this_attribute_was_added_automatically_you_cannot_make_changes_to_it"
)}
</p>
) : null}
</div>
@@ -78,7 +82,7 @@ export const AttributeSettingsTab = async ({ attributeClass, setOpen }: Attribut
variant="secondary"
href="https://formbricks.com/docs/attributes/identify-users"
target="_blank">
Read Docs
{t("common.read_docs")}
</Button>
{attributeClass.type !== "automatic" && (
<Button className="ml-3" variant="secondary" onClick={handleArchiveToggle}>
@@ -86,13 +90,13 @@ export const AttributeSettingsTab = async ({ attributeClass, setOpen }: Attribut
<>
{" "}
<ArchiveXIcon className="mr-2 h-4 text-slate-600" />
<span>Unarchive</span>
<span>{t("common.unarchive")}</span>
</>
) : (
<>
{" "}
<ArchiveIcon className="mr-2 h-4 text-slate-600" />
<span>Archive</span>
<span>{t("common.archive")}</span>
</>
)}
</Button>
@@ -101,7 +105,7 @@ export const AttributeSettingsTab = async ({ attributeClass, setOpen }: Attribut
{attributeClass.type !== "automatic" && (
<div className="flex space-x-2">
<Button type="submit" loading={isAttributeBeingSubmitted}>
Save changes
{t("common.save_changes")}
</Button>
</div>
)}

Some files were not shown because too many files have changed in this diff Show More