Compare commits

...

15 Commits

Author SHA1 Message Date
pandeymangg
f803022d7c Merge branch 'main' into fix/environment-route-cache 2025-02-13 12:09:02 +05:30
Piyush Gupta
22e8a137ef fix: date question accessibility (#4698)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
Co-authored-by: Anshuman Pandey <54475686+pandeymangg@users.noreply.github.com>
2025-02-12 10:47:54 +00:00
github-actions[bot]
a9fe05d64a chore: bump version to v3.1.5 (#4729)
Co-authored-by: GitHub Actions <github-actions@github.com>
2025-02-12 09:50:13 +01:00
pandeymangg
f476693f0d fix 2025-02-11 18:18:14 +05:30
Yannick Torrès
5219065b8e fix: fr-FR translations (#4667)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2025-02-11 05:35:34 +00:00
Dhruwang Jariwala
cb8497229d fix: UI tweaks (#4721) 2025-02-10 04:10:01 +00:00
Dhruwang Jariwala
25b8920d20 docs: basic docs for kubernetes (#4669) 2025-02-08 07:15:00 +00:00
Dhruwang Jariwala
9203db88ab fix: z index issue (#4723) 2025-02-07 10:46:26 +00:00
Dhruwang Jariwala
36378e9c23 feat: tolgee (#4692)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2025-02-07 05:49:35 +00:00
Piyush Gupta
9c33e77755 fix: phone number validations (#4708)
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
2025-02-07 04:29:30 +00:00
Dhruwang Jariwala
88cb4c742f feat: activepieces integration (#4711) 2025-02-06 15:34:02 +00:00
Anshuman Pandey
475cce8253 fix: prisma docker libsso issue (#4717) 2025-02-06 14:37:59 +00:00
Anshuman Pandey
a86c1738d1 fix: renames init to setup (#4714) 2025-02-05 10:12:14 +00:00
Piyush Gupta
96a4d02c80 fix: Branch test coverage (#4710) 2025-02-05 09:46:38 +00:00
Anshuman Pandey
bb6df783ab feat: react native sdk v2 (#4616)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2025-02-04 15:32:04 +00:00
626 changed files with 10987 additions and 5979 deletions

View File

@@ -0,0 +1,39 @@
name: Check Missing Translations
permissions:
contents: read
on:
workflow_dispatch:
pull_request:
types: [opened, synchronize, reopened]
jobs:
check-missing-translations:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install Tolgee CLI
run: npm install -g @tolgee/cli
- name: Compare Tolgee Keys
id: compare
run: |
tolgee compare --api-key ${{ secrets.TOLGEE_API_KEY }} > compare_output.txt
cat compare_output.txt
- name: Check for Missing Translations
run: |
if grep -q "new key found" compare_output.txt; then
echo "New keys found that may require translations:"
exit 1
else
echo "No new keys found."
fi

42
.github/workflows/tolgee.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Tolgee Tagging on PR Merge
permissions:
contents: read
on:
push:
branches:
- main
jobs:
tag-production-keys:
name: Tag Production Keys
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18 # Ensure compatibility with your project
- name: Install Tolgee CLI
run: npm install -g @tolgee/cli
- name: Tag Production Keys
run: |
BRANCH_NAME=${GITHUB_REF##*/}
npx tolgee tag \
--api-key ${{ secrets.TOLGEE_API_KEY }} \
--filter-extracted \
--filter-tag "draft: ${BRANCH_NAME}" \
--tag production \
--untag "draft: ${BRANCH_NAME}"
- name: Tag Deprecated Keys
run: |
npx tolgee tag \
--api-key ${{ secrets.TOLGEE_API_KEY }} \
--filter-not-extracted --filter-tag production \
--tag deprecated --untag production

1
.gitignore vendored
View File

@@ -58,4 +58,5 @@ packages/lib/uploads
# js compiled assets # js compiled assets
apps/web/public/js apps/web/public/js
packages/database/migrations packages/database/migrations

1
.husky/post-checkout Normal file
View File

@@ -0,0 +1 @@
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ../branch.json

1
.husky/post-commit Normal file
View File

@@ -0,0 +1 @@
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ../branch.json

View File

@@ -1 +1,5 @@
pnpm lint-staged pnpm lint-staged
pnpm tolgee-pull || true
echo "{\"branchName\": \"main\"}" > ../branch.json
git add branch.json packages/lib/messages/*.json

31
.tolgeerc.json Normal file
View File

@@ -0,0 +1,31 @@
{
"$schema": "https://docs.tolgee.io/cli-schema.json",
"format": "JSON_TOLGEE",
"patterns": ["./apps/web/**/*.ts?(x)"],
"projectId": 10304,
"pull": {
"path": "./packages/lib/messages"
},
"push": {
"files": [
{
"language": "en-US",
"path": "./packages/lib/messages/en-US.json"
},
{
"language": "de-DE",
"path": "./packages/lib/messages/de-DE.json"
},
{
"language": "fr-FR",
"path": "./packages/lib/messages/fr-FR.json"
},
{
"language": "pt-BR",
"path": "./packages/lib/messages/pt-BR.json"
}
],
"forceMode": "OVERRIDE"
},
"strictNamespace": false
}

View File

@@ -1,2 +1,2 @@
EXPO_PUBLIC_API_HOST=http://192.168.178.20:3000 EXPO_PUBLIC_APP_URL=http://192.168.0.197:3000
EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=clzr04nkd000bcdl110j0ijyq EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=cm5p0cs7r000819182b32j0a1

View File

@@ -18,6 +18,7 @@
}, },
"jsEngine": "hermes", "jsEngine": "hermes",
"name": "react-native-demo", "name": "react-native-demo",
"newArchEnabled": true,
"orientation": "portrait", "orientation": "portrait",
"slug": "react-native-demo", "slug": "react-native-demo",
"splash": { "splash": {

View File

@@ -13,16 +13,17 @@
"dependencies": { "dependencies": {
"@formbricks/js": "workspace:*", "@formbricks/js": "workspace:*",
"@formbricks/react-native": "workspace:*", "@formbricks/react-native": "workspace:*",
"expo": "52.0.18", "@react-native-async-storage/async-storage": "2.1.0",
"expo-status-bar": "2.0.0", "expo": "52.0.28",
"expo-status-bar": "2.0.1",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-native": "0.76.5", "react-native": "0.76.6",
"react-native-webview": "13.12.5" "react-native-webview": "13.12.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.26.0", "@babel/core": "7.26.0",
"@types/react": "19.0.1", "@types/react": "18.3.18",
"typescript": "5.7.2" "typescript": "5.7.2"
}, },
"private": true "private": true

View File

@@ -1,7 +1,14 @@
import { StatusBar } from "expo-status-bar"; import { StatusBar } from "expo-status-bar";
import React, { type JSX } from "react"; import React, { type JSX } from "react";
import { Button, LogBox, StyleSheet, Text, View } from "react-native"; import { Button, LogBox, StyleSheet, Text, View } from "react-native";
import Formbricks, { track } from "@formbricks/react-native"; import Formbricks, {
logout,
setAttribute,
setAttributes,
setLanguage,
setUserId,
track,
} from "@formbricks/react-native";
LogBox.ignoreAllLogs(); LogBox.ignoreAllLogs();
@@ -10,35 +17,92 @@ export default function App(): JSX.Element {
throw new Error("EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID is required"); throw new Error("EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID is required");
} }
if (!process.env.EXPO_PUBLIC_API_HOST) { if (!process.env.EXPO_PUBLIC_APP_URL) {
throw new Error("EXPO_PUBLIC_API_HOST is required"); throw new Error("EXPO_PUBLIC_APP_URL is required");
} }
const config = {
environmentId: process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID as string,
apiHost: process.env.EXPO_PUBLIC_API_HOST as string,
userId: "random-user-id",
attributes: {
language: "en",
testAttr: "attr-test",
},
};
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text>Formbricks React Native SDK Demo</Text> <Text>Formbricks React Native SDK Demo</Text>
<Button <View
title="Trigger Code Action" style={{
onPress={() => { display: "flex",
track("code").catch((error: unknown) => { flexDirection: "column",
// eslint-disable-next-line no-console -- logging is allowed in demo apps gap: 10,
console.error("Error tracking event:", error); }}>
}); <Button
}} title="Trigger Code Action"
/> onPress={() => {
track("code").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error tracking event:", error);
});
}}
/>
<Button
title="Set User Id"
onPress={() => {
setUserId("random-user-id").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting user id:", error);
});
}}
/>
<Button
title="Set User Attributess (multiple)"
onPress={() => {
setAttributes({
testAttr: "attr-test",
testAttr2: "attr-test-2",
testAttr3: "attr-test-3",
testAttr4: "attr-test-4",
}).catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting user attributes:", error);
});
}}
/>
<Button
title="Set User Attributes (single)"
onPress={() => {
setAttribute("testSingleAttr", "testSingleAttr").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting user attributes:", error);
});
}}
/>
<Button
title="Logout"
onPress={() => {
logout().catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error logging out:", error);
});
}}
/>
<Button
title="Set Language (de)"
onPress={() => {
setLanguage("de").catch((error: unknown) => {
// eslint-disable-next-line no-console -- logging is allowed in demo apps
console.error("Error setting language:", error);
});
}}
/>
</View>
<StatusBar style="auto" /> <StatusBar style="auto" />
<Formbricks initConfig={config} />
<Formbricks
appUrl={process.env.EXPO_PUBLIC_APP_URL as string}
environmentId={process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID as string}
/>
</View> </View>
); );
} }

View File

@@ -1,10 +1,10 @@
"use client"; "use client";
import { Button } from "@/components/button";
import { LoadingSpinner } from "@/components/icons/loading-spinner";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { useState } from "react"; import { useState } from "react";
import { RedocStandalone } from "redoc"; import { RedocStandalone } from "redoc";
import { LoadingSpinner } from "@/components/icons/loading-spinner";
import { Button } from "@/components/button";
import "./style.css"; import "./style.css";
export function ApiDocs() { export function ApiDocs() {
@@ -61,7 +61,13 @@ export function ApiDocs() {
<Button href="/developer-docs/rest-api" arrow="left" className="mb-4 mt-8"> <Button href="/developer-docs/rest-api" arrow="left" className="mb-4 mt-8">
Back to docs Back to docs
</Button> </Button>
<RedocStandalone specUrl="/docs/openapi.yaml" onLoaded={() => { setLoading(false); }} options={redocTheme} /> <RedocStandalone
specUrl="/docs/openapi.yaml"
onLoaded={() => {
setLoading(false);
}}
options={redocTheme}
/>
{loading ? <LoadingSpinner /> : null} {loading ? <LoadingSpinner /> : null}
</div> </div>
); );

View File

@@ -1,9 +1,9 @@
import Image from "next/image";
import { Button } from "@/components/button"; import { Button } from "@/components/button";
import logoHtml from "@/images/frameworks/html5.svg"; import logoHtml from "@/images/frameworks/html5.svg";
import logoNextjs from "@/images/frameworks/nextjs.svg"; import logoNextjs from "@/images/frameworks/nextjs.svg";
import logoReactJs from "@/images/frameworks/reactjs.svg"; import logoReactJs from "@/images/frameworks/reactjs.svg";
import logoVueJs from "@/images/frameworks/vuejs.svg"; import logoVueJs from "@/images/frameworks/vuejs.svg";
import Image from "next/image";
const libraries = [ const libraries = [
{ {

View File

@@ -357,17 +357,11 @@ Now, update your App.js/App.tsx file to initialize Formbricks:
// other imports // other imports
import Formbricks from "@formbricks/react-native"; import Formbricks from "@formbricks/react-native";
const config = {
environmentId: "<environment-id>",
apiHost: "<api-host>",
userId: "<user-id>", // optional
};
export default function App() { export default function App() {
return ( return (
<> <>
{/* Your app content */} {/* Your app content */}
<Formbricks initConfig={config} /> <Formbricks appUrl="https://app.formbricks.com" environmentId="your-environment-id" />
</> </>
); );
} }
@@ -381,7 +375,7 @@ export default function App() {
<Property name="environment-id" type="string"> <Property name="environment-id" type="string">
Formbricks Environment ID. Formbricks Environment ID.
</Property> </Property>
<Property name="api-host" type="string"> <Property name="app-url" type="string">
URL of the hosted Formbricks instance. URL of the hosted Formbricks instance.
</Property> </Property>
</Properties> </Properties>

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View File

@@ -0,0 +1,155 @@
import { MdxImage } from "@/components/mdx-image";
import CreateConnection from "./create-connection.webp";
import CreateNewFlow from "./create-new-flow.webp";
import DuplicateSurvey from "./duplicate-survey.webp";
import ConfigureConnection from "./configure-connection.webp";
import SearchFormbricks from "./search-formbricks.webp";
import SelectSurvey from "./select-survey.webp";
import TestTrigger from "./test-trigger.webp";
import UpdateQuestionId from "./update-question-id.webp";
import SuccessResponse from "./success-response.webp";
import SelectGSSheet from "./select-gs-sheet.webp";
import SelectGoogleSheet from "./select-google-sheet.webp";
import MatchData from "./match-data.webp";
import Result from "./result.webp";
export const metadata = {
title: "Formbricks Integration with Activepieces: A Step-by-Step Guide",
description:
"Learn how to integrate Formbricks with Activepieces. Follow our detailed guide to set up workflows, connect with various apps, and send your survey data to multiple platforms.",
};
#### Integrations
# Activepieces Setup
Activepieces is a versatile tool to automate workflows between Formbricks and numerous applications. Here's how to set it up.
<Note>
Ensure your survey is finalized before setting up Activepieces. Any changes in the survey will require additional adjustments in the workflow.
</Note>
## Step 1: Setup your survey incl. `questionId` for every question
Set up the `questionId`s of your survey questions before publishing.
<MdxImage
src={UpdateQuestionId}
alt="Update Question ID"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
_Update the Question ID field in every question card under Advanced Settings._
<Note>
Already published? Duplicate survey You can only update the questionId before publishing the survey. If already published, simply duplicate it.
<MdxImage
src={DuplicateSurvey}
alt="Duplicate Survey"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
</Note>
## Step 2: Setup Activepieces
Visit [Activepieces](https://activepieces.com) to start a new Flow.
<MdxImage
src={CreateNewFlow}
alt="Create New Flow"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Search for `Formbricks` and choose the event to trigger the flow, we will choose `Response Finished` here
<MdxImage
src={SearchFormbricks}
alt="Search Formbricks"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## Step 3: Connect Formbricks with Activepieces
Click on `Create connection`:
<MdxImage
src={CreateConnection}
alt="Create Connection"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Enter the Formbricks API Host and API Key. API Host is by default set to https://app.formbricks.com but can be modified for self-hosting instances. Learn how to get an API Key from the [API Key tutorial](/additional-features/api#how-to-generate-an-api-key).
<MdxImage
src={ConfigureConnection}
alt="Configure Connection"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## Step 4: Select Survey
Choose from your created surveys:
<MdxImage
src={SelectSurvey}
alt="Select Survey"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## Step 5: Send a test response
You need a test response for Activepieces setup. Click on Test trigger and submit a test response in the connected Formbricks survey to see the data in Activepieces.
<MdxImage
src={TestTrigger}
alt="Test Trigger"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
If the test response is successful, you will see the data in Activepieces.
<MdxImage
src={SuccessResponse}
alt="Success Response"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## Step 6: Set up Google Sheet
Decide on the desired action for the data. Here, we'll send submissions to a Google Sheet. Add Google sheet step to your flow and configure it as follows:
Choose "Add a Row" for the action. Authenticate with Google and select the spreadsheet you want to add the data to.
<MdxImage
src={SelectGSSheet}
alt="Select Google sheet"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Specify the fields you want to add to the spreadsheet.
<MdxImage
src={MatchData}
alt="Match Data"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
A new row gets added to the spreadsheet for every response:
<MdxImage
src={Result}
alt="Result"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -18,25 +18,27 @@ const jost = Jost({ subsets: ["latin"] });
async function RootLayout({ children }: { children: React.ReactNode }) { async function RootLayout({ children }: { children: React.ReactNode }) {
const pages = await glob("**/*.mdx", { cwd: "src/app" }); const pages = await glob("**/*.mdx", { cwd: "src/app" });
const allSectionsEntries: [string, Section[]][] = (await Promise.all( const allSectionsEntries: [string, Section[]][] = await Promise.all(
pages.map(async (filename) => [ pages.map(async (filename) => [
`/${filename.replace(/(?:^|\/)page\.mdx$/, "")}`, `/${filename.replace(/(?:^|\/)page\.mdx$/, "")}`,
(await import(`./${filename}`) as { sections: Section[] }).sections, ((await import(`./${filename}`)) as { sections: Section[] }).sections,
]) ])
)); );
const allSections = Object.fromEntries(allSectionsEntries); const allSections = Object.fromEntries(allSectionsEntries);
return ( return (
<html lang="en" className="h-full" suppressHydrationWarning> <html lang="en" className="h-full" suppressHydrationWarning>
<head> <head>
{process.env.NEXT_PUBLIC_LAYER_API_KEY ? <Script {process.env.NEXT_PUBLIC_LAYER_API_KEY ? (
strategy="afterInteractive" <Script
src="https://storage.googleapis.com/generic-assets/buildwithlayer-widget-4.js" strategy="afterInteractive"
primary-color="#00C4B8" src="https://storage.googleapis.com/generic-assets/buildwithlayer-widget-4.js"
api-key={process.env.NEXT_PUBLIC_LAYER_API_KEY} primary-color="#00C4B8"
walkthrough-enabled="false" api-key={process.env.NEXT_PUBLIC_LAYER_API_KEY}
design-style="copilot" walkthrough-enabled="false"
/> : null} design-style="copilot"
/>
) : null}
</head> </head>
<body className={`flex min-h-full bg-white antialiased dark:bg-zinc-900 ${jost.className}`}> <body className={`flex min-h-full bg-white antialiased dark:bg-zinc-900 ${jost.className}`}>
<Providers> <Providers>

View File

@@ -0,0 +1,158 @@
export const metadata = {
title: "Kubernetes Deployment",
description: "Deploy Formbricks on a Kubernetes cluster using Helm.",
};
# Deploying Formbricks on Kubernetes
This guide explains how to deploy Formbricks on a **Kubernetes cluster** using **Helm**. It assumes that:
- You **already have a Kubernetes cluster** running (e.g., DigitalOcean, GKE, AWS, Minikube).
- An **Ingress controller** (e.g., Traefik, Nginx) is configured.
- You have **Helm installed** on your local machine.
---
## 🚀 **Step 1: Install Formbricks with Helm**
### **1⃣ Clone the Formbricks Helm Chart**
```sh
git clone https://github.com/formbricks/formbricks.git
cd formbricks/helm-chart
```
### **2⃣ Deploy Formbricks**
```sh
helm install my-formbricks ./ \
--namespace formbricks \
--create-namespace \
--set replicaCount=2
```
## 🎯 **Step 2: Verify and Access Formbricks**
### **Check the Running Services**
```sh
kubectl get pods -n formbricks
kubectl get svc -n formbricks
kubectl get ingress -n formbricks
```
### **Access Formbricks**
- If running locally with **Minikube**:
```sh
minikube service my-formbricks -n formbricks
```
- If deployed on a **cloud cluster**, visit:
```
https://formbricks.example.com
```
---
## Upgrading Formbricks
This section provides guidance on how to upgrade your Formbricks deployment using Helm, including examples of common upgrade scenarios.
### Upgrade Process
To upgrade your Formbricks deployment when using a local chart (e.g., with Minikube), use:
```bash
# From the helm-chart directory
helm upgrade my-formbricks ./ --namespace formbricks
```
For installations from the Helm repository (typically for production deployments):
```bash
helm repo update
helm upgrade my-formbricks formbricks/formbricks --namespace formbricks
```
### Common Upgrade Scenarios
#### 1. Updating Environment Variables
To update or add new environment variables, use the `--set` flag with the `env` prefix:
```bash
helm upgrade my-formbricks formbricks/formbricks \
--set env.SMTP_HOST=new-smtp.example.com \
--set env.SMTP_PORT=587 \
--set env.NEW_CUSTOM_VAR=newvalue
```
This command updates the SMTP host and port, and adds a new custom environment variable.
#### 2. Enabling or Disabling Features
You can enable or disable features by updating their respective values:
```bash
# Disable Redis
helm upgrade my-formbricks formbricks/formbricks --set redis.enabled=false
# Enable Redis
helm upgrade my-formbricks formbricks/formbricks --set redis.enabled=true
```
#### 3. Scaling Resources
To adjust resource allocation:
```bash
helm upgrade my-formbricks formbricks/formbricks \
--set resources.limits.cpu=1 \
--set resources.limits.memory=2Gi \
--set resources.requests.cpu=500m \
--set resources.requests.memory=1Gi
```
#### 4. Updating Autoscaling Configuration
To modify autoscaling settings:
```bash
helm upgrade my-formbricks formbricks/formbricks \
--set autoscaling.minReplicas=3 \
--set autoscaling.maxReplicas=10 \
--set autoscaling.metrics[0].resource.target.averageUtilization=75
```
#### 5. Changing Database Credentials
To update PostgreSQL database credentials:
To switch from the built-in PostgreSQL to an external database or update the external database credentials:
```bash
helm upgrade my-formbricks formbricks/formbricks \
--set postgresql.enabled=false \
--set postgresql.externalUrl="postgresql://newuser:newpassword@external-postgres-host:5432/newdatabase"
```
This command disables the built-in PostgreSQL and configures Formbricks to use an external PostgreSQL database. Make sure your external database is set up and accessible before making this change.
## Full Values Documentation
Below is a comprehensive list of all configurable values in the Formbricks Helm chart:
| Field | Description | Default |
| ----------------------------------------------------------- | ------------------------------------------ | ------------------------------- |
| `image.repository` | Docker image repository for Formbricks | `ghcr.io/formbricks/formbricks` |
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
| `image.tag` | Docker image tag | `"2.6.0"` |
| `service.type` | Kubernetes service type | `ClusterIP` |
| `service.port` | Kubernetes service port | `80` |
| `service.targetPort` | Container port to expose | `3000` |
| `resources.limits.cpu` | CPU resource limit | `500m` |
| `resources.limits.memory` | Memory resource limit | `1Gi` |
| `resources.requests.cpu` | Memory resource request | `null` |
| `resources.requests.memory` | Memory resource request | `null` |
| `autoscaling.enabled` | Enable autoscaling | `false` |
| `autoscaling.minReplicas` | Minimum number of replicas | `1` |
| `autoscaling.maxReplicas` | Maximum number of replicas | `5` |
| `autoscaling.metrics[0].type` | Type of metric for autoscaling | `Resource` |
| `autoscaling.metrics[0].resource.name` | Resource name for autoscaling metric | `cpu` |
| `autoscaling.metrics[0].resource.target.type` | Target type for autoscaling | `Utilization` |
| `autoscaling.metrics[0].resource.target.averageUtilization` | Average utilization target for autoscaling | `80`

View File

@@ -30,11 +30,17 @@ type ButtonProps = {
variant?: keyof typeof variantStyles; variant?: keyof typeof variantStyles;
arrow?: "left" | "right"; arrow?: "left" | "right";
} & ( } & (
| React.ComponentPropsWithoutRef<typeof Link> | React.ComponentPropsWithoutRef<typeof Link>
| (React.ComponentPropsWithoutRef<"button"> & { href?: undefined }) | (React.ComponentPropsWithoutRef<"button"> & { href?: undefined })
); );
export function Button({ variant = "primary", className, children, arrow, ...props }: ButtonProps): React.JSX.Element { export function Button({
variant = "primary",
className,
children,
arrow,
...props
}: ButtonProps): React.JSX.Element {
const buttonClassName = clsx( const buttonClassName = clsx(
"inline-flex gap-0.5 justify-center items-center overflow-hidden font-medium transition text-center", "inline-flex gap-0.5 justify-center items-center overflow-hidden font-medium transition text-center",
variantStyles[variant], variantStyles[variant],

View File

@@ -1,10 +1,10 @@
"use client"; "use client";
import { Tag } from "@/components/tag";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
import clsx from "clsx"; import clsx from "clsx";
import { Children, createContext, isValidElement, useContext, useEffect, useRef, useState } from "react"; import { Children, createContext, isValidElement, useContext, useEffect, useRef, useState } from "react";
import { create } from "zustand"; import { create } from "zustand";
import { Tag } from "@/components/tag";
const languageNames: Record<string, string> = { const languageNames: Record<string, string> = {
js: "JavaScript", js: "JavaScript",
@@ -49,7 +49,9 @@ function CopyButton({ code }: { code: string }) {
useEffect(() => { useEffect(() => {
if (copyCount > 0) { if (copyCount > 0) {
const timeout = setTimeout(() => { setCopyCount(0); }, 1000); const timeout = setTimeout(() => {
setCopyCount(0);
}, 1000);
return () => { return () => {
clearTimeout(timeout); clearTimeout(timeout);
}; };
@@ -98,9 +100,11 @@ function CodePanelHeader({ tag, label }: { tag?: string; label?: string }): Reac
return ( return (
<div className="border-b-white/7.5 bg-white/2.5 dark:bg-white/1 flex h-9 items-center gap-2 border-y border-t-transparent bg-slate-900 px-4 dark:border-b-white/5"> <div className="border-b-white/7.5 bg-white/2.5 dark:bg-white/1 flex h-9 items-center gap-2 border-y border-t-transparent bg-slate-900 px-4 dark:border-b-white/5">
{tag ? <div className="dark flex"> {tag ? (
<Tag variant="small">{tag}</Tag> <div className="dark flex">
</div> : null} <Tag variant="small">{tag}</Tag>
</div>
) : null}
{tag && label ? <span className="h-0.5 w-0.5 rounded-full bg-slate-500" /> : null} {tag && label ? <span className="h-0.5 w-0.5 rounded-full bg-slate-500" /> : null}
{label ? <span className="font-mono text-xs text-slate-400">{label}</span> : null} {label ? <span className="font-mono text-xs text-slate-400">{label}</span> : null}
</div> </div>
@@ -162,30 +166,34 @@ function CodeGroupHeader({
return ( return (
<div className="flex min-h-[calc(theme(spacing.12)+1px)] flex-wrap items-start gap-x-4 border-b border-slate-700 bg-slate-800 px-4 dark:border-slate-800 dark:bg-transparent"> <div className="flex min-h-[calc(theme(spacing.12)+1px)] flex-wrap items-start gap-x-4 border-b border-slate-700 bg-slate-800 px-4 dark:border-slate-800 dark:bg-transparent">
{title ? <h3 className="mr-auto pt-3 text-xs font-semibold text-white">{title}</h3> : null} {title ? <h3 className="mr-auto pt-3 text-xs font-semibold text-white">{title}</h3> : null}
{hasTabs ? <Tab.List className="-mb-px flex gap-4 text-xs font-medium"> {hasTabs ? (
{Children.map(children, (child, childIndex) => { <Tab.List className="-mb-px flex gap-4 text-xs font-medium">
if (isValidElement(child)) { {Children.map(children, (child, childIndex) => {
return ( if (isValidElement(child)) {
<Tab return (
className={clsx( <Tab
"ui-not-focus-visible:outline-none border-b py-3 transition", className={clsx(
childIndex === selectedIndex "ui-not-focus-visible:outline-none border-b py-3 transition",
? "border-teal-500 text-teal-400" childIndex === selectedIndex
: "border-transparent text-slate-400 hover:text-slate-300" ? "border-teal-500 text-teal-400"
)} : "border-transparent text-slate-400 hover:text-slate-300"
> )}>
{getPanelTitle(child.props as { title?: string; language?: string })} {getPanelTitle(child.props as { title?: string; language?: string })}
</Tab> </Tab>
); );
} }
return null; return null;
})} })}
</Tab.List> : null} </Tab.List>
) : null}
</div> </div>
); );
} }
function CodeGroupPanels({ children, ...props }: React.ComponentPropsWithoutRef<typeof CodePanel>): React.JSX.Element { function CodeGroupPanels({
children,
...props
}: React.ComponentPropsWithoutRef<typeof CodePanel>): React.JSX.Element {
const hasTabs = Children.count(children) >= 1; const hasTabs = Children.count(children) >= 1;
if (hasTabs) { if (hasTabs) {
@@ -264,7 +272,9 @@ const useTabGroupProps = (availableLanguages: string[]) => {
const { positionRef, preventLayoutShift } = usePreventLayoutShift(); const { positionRef, preventLayoutShift } = usePreventLayoutShift();
const onChange = (index: number) => { const onChange = (index: number) => {
preventLayoutShift(() => { addPreferredLanguage(availableLanguages[index] ?? ""); }); preventLayoutShift(() => {
addPreferredLanguage(availableLanguages[index] ?? "");
});
}; };
return { return {
@@ -331,7 +341,10 @@ export function Code({ children, ...props }: React.ComponentPropsWithoutRef<"cod
return <code {...props}>{children}</code>; return <code {...props}>{children}</code>;
} }
export function Pre({ children, ...props }: React.ComponentPropsWithoutRef<typeof CodeGroup>): React.ReactNode { export function Pre({
children,
...props
}: React.ComponentPropsWithoutRef<typeof CodeGroup>): React.ReactNode {
const isGrouped = useContext(CodeGroupContext); const isGrouped = useContext(CodeGroupContext);
if (isGrouped) { if (isGrouped) {

View File

@@ -18,7 +18,9 @@ function CheckIcon(props: React.ComponentPropsWithoutRef<"svg">): React.JSX.Elem
); );
} }
function FeedbackButton(props: Omit<React.ComponentPropsWithoutRef<"button">, "type" | "className">): React.JSX.Element { function FeedbackButton(
props: Omit<React.ComponentPropsWithoutRef<"button">, "type" | "className">
): React.JSX.Element {
return ( return (
<button <button
type="submit" type="submit"
@@ -49,16 +51,18 @@ const FeedbackForm = forwardRef<
FeedbackForm.displayName = "FeedbackForm"; FeedbackForm.displayName = "FeedbackForm";
const FeedbackThanks = forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>((_props, ref): React.JSX.Element => { const FeedbackThanks = forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
return ( (_props, ref): React.JSX.Element => {
<div ref={ref} className="absolute inset-0 flex justify-center md:justify-start"> return (
<div className="flex items-center gap-3 rounded-full bg-teal-50/50 py-1 pl-1.5 pr-3 text-sm text-teal-900 ring-1 ring-inset ring-teal-500/20 dark:bg-teal-500/5 dark:text-teal-200 dark:ring-teal-500/30"> <div ref={ref} className="absolute inset-0 flex justify-center md:justify-start">
<CheckIcon className="h-5 w-5 flex-none fill-teal-500 stroke-white dark:fill-teal-200/20 dark:stroke-teal-200" /> <div className="flex items-center gap-3 rounded-full bg-teal-50/50 py-1 pl-1.5 pr-3 text-sm text-teal-900 ring-1 ring-inset ring-teal-500/20 dark:bg-teal-500/5 dark:text-teal-200 dark:ring-teal-500/30">
Thanks for your feedback! <CheckIcon className="h-5 w-5 flex-none fill-teal-500 stroke-white dark:fill-teal-200/20 dark:stroke-teal-200" />
Thanks for your feedback!
</div>
</div> </div>
</div> );
); }
}); );
FeedbackThanks.displayName = "FeedbackThanks"; FeedbackThanks.displayName = "FeedbackThanks";

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import { navigation } from "@/lib/navigation";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { navigation } from "@/lib/navigation";
import { Button } from "./button"; import { Button } from "./button";
import { DiscordIcon } from "./icons/discord-icon"; import { DiscordIcon } from "./icons/discord-icon";
import { GithubIcon } from "./icons/github-icon"; import { GithubIcon } from "./icons/github-icon";

View File

@@ -24,18 +24,20 @@ export function GridPattern({
</pattern> </pattern>
</defs> </defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${patternId})`} /> <rect width="100%" height="100%" strokeWidth={0} fill={`url(#${patternId})`} />
{squares.length > 0 ? <svg x={x} y={y} className="overflow-visible"> {squares.length > 0 ? (
{squares.map(([sqX, sqY]) => ( <svg x={x} y={y} className="overflow-visible">
<rect {squares.map(([sqX, sqY]) => (
strokeWidth="0" <rect
key={`${sqX.toString()}-${sqY.toString()}`} strokeWidth="0"
width={width + 1} key={`${sqX.toString()}-${sqY.toString()}`}
height={height + 1} width={width + 1}
x={sqX * width} height={height + 1}
y={sqY * height} x={sqX * width}
/> y={sqY * height}
))} />
</svg> : null} ))}
</svg>
) : null}
</svg> </svg>
); );
} }

View File

@@ -1,16 +1,16 @@
"use client"; "use client";
import { Logo } from "@/components/logo";
import { Navigation } from "@/components/navigation";
import { Search } from "@/components/search";
import { useIsInsideMobileNavigation, useMobileNavigationStore } from "@/hooks/use-mobile-navigation";
import clsx from "clsx"; import clsx from "clsx";
import { type MotionStyle, motion, useScroll, useTransform } from "framer-motion"; import { type MotionStyle, motion, useScroll, useTransform } from "framer-motion";
import Link from "next/link"; import Link from "next/link";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { Search } from "@/components/search";
import { Logo } from "@/components/logo";
import { Button } from "./button"; import { Button } from "./button";
import { MobileNavigation } from "./mobile-navigation"; import { MobileNavigation } from "./mobile-navigation";
import { ThemeToggle } from "./theme-toggle"; import { ThemeToggle } from "./theme-toggle";
import { Navigation } from "@/components/navigation";
import { useIsInsideMobileNavigation, useMobileNavigationStore } from "@/hooks/use-mobile-navigation";
function TopLevelNavItem({ href, children }: { href: string; children: React.ReactNode }): React.JSX.Element { function TopLevelNavItem({ href, children }: { href: string; children: React.ReactNode }): React.JSX.Element {
return ( return (

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import { useInView } from "framer-motion";
import Link from "next/link";
import { useEffect, useRef } from "react";
import { useSectionStore } from "@/components/section-provider"; import { useSectionStore } from "@/components/section-provider";
import { Tag } from "@/components/tag"; import { Tag } from "@/components/tag";
import { remToPx } from "@/lib/rem-to-px"; import { remToPx } from "@/lib/rem-to-px";
import { useInView } from "framer-motion";
import Link from "next/link";
import { useEffect, useRef } from "react";
function AnchorIcon(props: React.ComponentPropsWithoutRef<"svg">): React.JSX.Element { function AnchorIcon(props: React.ComponentPropsWithoutRef<"svg">): React.JSX.Element {
return ( return (
@@ -29,14 +29,24 @@ function Eyebrow({ tag, label }: { tag?: string; label?: string }): React.JSX.El
); );
} }
function Anchor({ id, inView, children }: { id: string; inView: boolean; children: React.ReactNode }): React.JSX.Element { function Anchor({
id,
inView,
children,
}: {
id: string;
inView: boolean;
children: React.ReactNode;
}): React.JSX.Element {
return ( return (
<Link href={`#${id}`} className="group text-inherit no-underline hover:text-inherit"> <Link href={`#${id}`} className="group text-inherit no-underline hover:text-inherit">
{inView ? <div className="absolute ml-[calc(-1*var(--width))] mt-1 hidden w-[var(--width)] opacity-0 transition [--width:calc(1.35rem+0.85px+38%-min(38%,calc(theme(maxWidth.lg)+theme(spacing.2))))] group-hover:opacity-100 group-focus:opacity-100 md:block lg:z-50 2xl:[--width:theme(spacing.10)]"> {inView ? (
<div className="group/anchor block h-5 w-5 rounded-lg bg-zinc-50 ring-1 ring-inset ring-zinc-300 transition hover:ring-zinc-500 dark:bg-zinc-800 dark:ring-zinc-700 dark:hover:bg-zinc-700 dark:hover:ring-zinc-600"> <div className="absolute ml-[calc(-1*var(--width))] mt-1 hidden w-[var(--width)] opacity-0 transition [--width:calc(1.35rem+0.85px+38%-min(38%,calc(theme(maxWidth.lg)+theme(spacing.2))))] group-hover:opacity-100 group-focus:opacity-100 md:block lg:z-50 2xl:[--width:theme(spacing.10)]">
<AnchorIcon className="h-5 w-5 stroke-zinc-500 transition dark:stroke-zinc-400 dark:group-hover/anchor:stroke-white" /> <div className="group/anchor block h-5 w-5 rounded-lg bg-zinc-50 ring-1 ring-inset ring-zinc-300 transition hover:ring-zinc-500 dark:bg-zinc-800 dark:ring-zinc-700 dark:hover:bg-zinc-700 dark:hover:ring-zinc-600">
<AnchorIcon className="h-5 w-5 stroke-zinc-500 transition dark:stroke-zinc-400 dark:group-hover/anchor:stroke-white" />
</div>
</div> </div>
</div> : null} ) : null}
{children} {children}
</Link> </Link>
); );
@@ -67,7 +77,7 @@ export function Heading<Level extends 2 | 3 | 4>({
const ref = useRef<HTMLHeadingElement>(null); const ref = useRef<HTMLHeadingElement>(null);
const registerHeading = useSectionStore((s) => s.registerHeading); const registerHeading = useSectionStore((s) => s.registerHeading);
const topMargin = remToPx(-3.5) const topMargin = remToPx(-3.5);
const inView = useInView(ref, { const inView = useInView(ref, {
margin: `${topMargin}px 0px 0px 0px`, margin: `${topMargin}px 0px 0px 0px`,
amount: "all", amount: "all",
@@ -75,18 +85,18 @@ export function Heading<Level extends 2 | 3 | 4>({
useEffect(() => { useEffect(() => {
if (headingLevel === 2) { if (headingLevel === 2) {
registerHeading({ id: props.id, ref, offsetRem: tag ?? label ? 8 : 6 }); registerHeading({ id: props.id, ref, offsetRem: (tag ?? label) ? 8 : 6 });
} else if (headingLevel === 3) { } else if (headingLevel === 3) {
registerHeading({ id: props.id, ref, offsetRem: tag ?? label ? 7 : 5 }); registerHeading({ id: props.id, ref, offsetRem: (tag ?? label) ? 7 : 5 });
} else if (headingLevel === 4) { } else if (headingLevel === 4) {
registerHeading({ id: props.id, ref, offsetRem: tag ?? label ? 6 : 4 }); registerHeading({ id: props.id, ref, offsetRem: (tag ?? label) ? 6 : 4 });
} }
}, [label, headingLevel, props.id, registerHeading, tag]); }, [label, headingLevel, props.id, registerHeading, tag]);
return ( return (
<> <>
<Eyebrow tag={tag} label={label} /> <Eyebrow tag={tag} label={label} />
<Component ref={ref} className={tag ?? label ? "mt-2 scroll-mt-32" : "scroll-mt-24"} {...props}> <Component ref={ref} className={(tag ?? label) ? "mt-2 scroll-mt-32" : "scroll-mt-24"} {...props}>
{anchor ? ( {anchor ? (
<Anchor id={props.id} inView={inView}> <Anchor id={props.id} inView={inView}>
{children} {children}

View File

@@ -12,4 +12,4 @@ export function GithubIcon(props: React.ComponentPropsWithoutRef<"svg">): React.
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" /> <path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
</svg> </svg>
); );
}; }

View File

@@ -7,4 +7,3 @@ export function LoadingSpinner(props: React.ComponentPropsWithoutRef<"div">): Re
</div> </div>
); );
} }

View File

@@ -12,4 +12,4 @@ export function TwitterIcon(props: React.ComponentPropsWithoutRef<"svg">): React
<path d="M403.229 0h78.506L310.219 196.04 512 462.799H354.002L230.261 301.007 88.669 462.799h-78.56l183.455-209.683L0 0h161.999l111.856 147.88L403.229 0zm-27.556 415.805h43.505L138.363 44.527h-46.68l283.99 371.278z" /> <path d="M403.229 0h78.506L310.219 196.04 512 462.799H354.002L230.261 301.007 88.669 462.799h-78.56l183.455-209.683L0 0h161.999l111.856 147.88L403.229 0zm-27.556 415.805h43.505L138.363 44.527h-46.68l283.99 371.278z" />
</svg> </svg>
); );
}; }

View File

@@ -1,11 +1,11 @@
"use client"; "use client";
import { motion } from "framer-motion";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Logo } from "@/components/logo"; import { Logo } from "@/components/logo";
import { Navigation } from "@/components/navigation"; import { Navigation } from "@/components/navigation";
import { SideNavigation } from "@/components/side-navigation"; import { SideNavigation } from "@/components/side-navigation";
import { motion } from "framer-motion";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Footer } from "./footer"; import { Footer } from "./footer";
import { Header } from "./header"; import { Header } from "./header";
import { type Section, SectionProvider } from "./section-provider"; import { type Section, SectionProvider } from "./section-provider";

View File

@@ -1,15 +1,23 @@
import Image from "next/image";
import logoDark from "@/images/logo/logo-dark.svg"; import logoDark from "@/images/logo/logo-dark.svg";
import logoLight from "@/images/logo/logo-light.svg"; import logoLight from "@/images/logo/logo-light.svg";
import Image from "next/image";
export function Logo({ className }: { className?: string }) { export function Logo({ className }: { className?: string }) {
return ( return (
<div> <div>
<div className="block dark:hidden"> <div className="block dark:hidden">
<Image className={className} src={logoLight as string} alt="Formbricks Open source Forms & Surveys Logo" /> <Image
className={className}
src={logoLight as string}
alt="Formbricks Open source Forms & Surveys Logo"
/>
</div> </div>
<div className="hidden dark:block"> <div className="hidden dark:block">
<Image className={className} src={logoDark as string} alt="Formbricks Open source Forms & Surveys Logo" /> <Image
className={className}
src={logoDark as string}
alt="Formbricks Open source Forms & Surveys Logo"
/>
</div> </div>
</div> </div>
); );

View File

@@ -57,7 +57,7 @@ function MobileNavigationDialog({
if ( if (
link && link &&
link.pathname + link.search + link.hash === link.pathname + link.search + link.hash ===
window.location.pathname + window.location.search + window.location.hash window.location.pathname + window.location.search + window.location.hash
) { ) {
close(); close();
} }

View File

@@ -1,15 +1,15 @@
"use client"; "use client";
import { useIsInsideMobileNavigation } from "@/hooks/use-mobile-navigation";
import { navigation } from "@/lib/navigation";
import { remToPx } from "@/lib/rem-to-px";
import clsx from "clsx"; import clsx from "clsx";
import { AnimatePresence, motion, useIsPresent } from "framer-motion"; import { AnimatePresence, motion, useIsPresent } from "framer-motion";
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"; import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { remToPx } from "@/lib/rem-to-px";
import { navigation } from "@/lib/navigation";
import { Button } from "./button"; import { Button } from "./button";
import { useIsInsideMobileNavigation } from "@/hooks/use-mobile-navigation";
import { useSectionStore } from "./section-provider"; import { useSectionStore } from "./section-provider";
export interface BaseLink { export interface BaseLink {
@@ -79,7 +79,6 @@ function NavLink({
<span className="flex w-full truncate">{children}</span> <span className="flex w-full truncate">{children}</span>
</div> </div>
); );
} }
function VisibleSectionHighlight({ group, pathname }: { group: NavGroup; pathname: string }) { function VisibleSectionHighlight({ group, pathname }: { group: NavGroup; pathname: string }) {
@@ -97,7 +96,7 @@ function VisibleSectionHighlight({ group, pathname }: { group: NavGroup; pathnam
const activePageIndex = group.links.findIndex( const activePageIndex = group.links.findIndex(
(link) => (link) =>
(link.href && pathname.startsWith(link.href)) ?? (link.href && pathname.startsWith(link.href)) ??
(link.children?.some((child) => pathname.startsWith(child.href))) link.children?.some((child) => pathname.startsWith(child.href))
); );
const height = isPresent ? Math.max(1, visibleSections.length) * itemHeight : itemHeight; const height = isPresent ? Math.max(1, visibleSections.length) * itemHeight : itemHeight;
@@ -116,13 +115,19 @@ function VisibleSectionHighlight({ group, pathname }: { group: NavGroup; pathnam
); );
} }
function ActivePageMarker({ group, pathname }: { group: NavGroup; pathname: string }): React.JSX.Element | null { function ActivePageMarker({
group,
pathname,
}: {
group: NavGroup;
pathname: string;
}): React.JSX.Element | null {
const itemHeight = remToPx(2); const itemHeight = remToPx(2);
const offset = remToPx(0.25); const offset = remToPx(0.25);
const activePageIndex = group.links.findIndex( const activePageIndex = group.links.findIndex(
(link) => (link) =>
(link.href && pathname.startsWith(link.href)) ?? (link.href && pathname.startsWith(link.href)) ??
(link.children?.some((child) => pathname.startsWith(child.href))) link.children?.some((child) => pathname.startsWith(child.href))
); );
if (activePageIndex === -1) return null; if (activePageIndex === -1) return null;
const top = offset + activePageIndex * itemHeight; const top = offset + activePageIndex * itemHeight;
@@ -228,21 +233,25 @@ function NavigationGroup({
key={link.title} key={link.title}
layout="position" layout="position"
className="relative" className="relative"
onClick={() => { setIsActiveGroup(true); }}> onClick={() => {
setIsActiveGroup(true);
}}>
{link.href ? ( {link.href ? (
<NavLink <NavLink href={link.href} active={Boolean(pathname.startsWith(link.href))}>
href={link.href}
active={Boolean(pathname.startsWith(link.href))}>
{link.title} {link.title}
</NavLink> </NavLink>
) : ( ) : (
<button onClick={() => { toggleParentTitle(`${group.title}-${link.title}`); }} className="w-full"> <button
onClick={() => {
toggleParentTitle(`${group.title}-${link.title}`);
}}
className="w-full">
<NavLink <NavLink
href={!isMobile ? link.children?.[0]?.href ?? "" : undefined} href={!isMobile ? (link.children?.[0]?.href ?? "") : undefined}
active={ active={Boolean(
Boolean(isParentOpen(`${group.title}-${link.title}`) && isParentOpen(`${group.title}-${link.title}`) &&
link.children?.some((child) => pathname.startsWith(child.href))) link.children?.some((child) => pathname.startsWith(child.href))
}> )}>
<span className="flex w-full justify-between"> <span className="flex w-full justify-between">
{link.title} {link.title}
{isParentOpen(`${group.title}-${link.title}`) ? ( {isParentOpen(`${group.title}-${link.title}`) ? (
@@ -255,19 +264,24 @@ function NavigationGroup({
</button> </button>
)} )}
<AnimatePresence mode="popLayout" initial={false}> <AnimatePresence mode="popLayout" initial={false}>
{isActiveGroup && link.children && isParentOpen(`${group.title}-${link.title}`) ? <motion.ul {isActiveGroup && link.children && isParentOpen(`${group.title}-${link.title}`) ? (
role="list" <motion.ul
initial={{ opacity: 0 }} role="list"
animate={{ opacity: 1, transition: { delay: 0.1 } }} initial={{ opacity: 0 }}
exit={{ opacity: 0, transition: { duration: 0.15 } }}> animate={{ opacity: 1, transition: { delay: 0.1 } }}
{link.children.map((child) => ( exit={{ opacity: 0, transition: { duration: 0.15 } }}>
<li key={child.href}> {link.children.map((child) => (
<NavLink href={child.href} isAnchorLink active={Boolean(pathname.startsWith(child.href))}> <li key={child.href}>
{child.title} <NavLink
</NavLink> href={child.href}
</li> isAnchorLink
))} active={Boolean(pathname.startsWith(child.href))}>
</motion.ul> : null} {child.title}
</NavLink>
</li>
))}
</motion.ul>
) : null}
</AnimatePresence> </AnimatePresence>
</motion.li> </motion.li>
))} ))}
@@ -306,7 +320,7 @@ export function Navigation({ isMobile, ...props }: NavigationProps) {
return ( return (
<nav {...props}> <nav {...props}>
<ul > <ul>
{navigation.map((group, groupIndex) => ( {navigation.map((group, groupIndex) => (
<NavigationGroup <NavigationGroup
key={group.title} key={group.title}

View File

@@ -1,13 +1,13 @@
"use client"; "use client";
import { type MotionValue, motion, useMotionTemplate, useMotionValue } from "framer-motion";
import Link from "next/link";
import { GridPattern } from "@/components/grid-pattern"; import { GridPattern } from "@/components/grid-pattern";
import { Heading } from "@/components/heading"; import { Heading } from "@/components/heading";
import { ChatBubbleIcon } from "@/components/icons/chat-bubble-icon"; import { ChatBubbleIcon } from "@/components/icons/chat-bubble-icon";
import { EnvelopeIcon } from "@/components/icons/envelope-icon"; import { EnvelopeIcon } from "@/components/icons/envelope-icon";
import { UserIcon } from "@/components/icons/user-icon"; import { UserIcon } from "@/components/icons/user-icon";
import { UsersIcon } from "@/components/icons/users-icon"; import { UsersIcon } from "@/components/icons/users-icon";
import { type MotionValue, motion, useMotionTemplate, useMotionValue } from "framer-motion";
import Link from "next/link";
interface TResource { interface TResource {
href: string; href: string;

View File

@@ -10,7 +10,8 @@ export function ResponsiveVideo({ src, title }: { src: string; title: string }):
className="absolute left-0 top-0 h-full w-full" className="absolute left-0 top-0 h-full w-full"
referrerPolicy="strict-origin-when-cross-origin" referrerPolicy="strict-origin-when-cross-origin"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen /> allowFullScreen
/>
</div> </div>
</div> </div>
); );

View File

@@ -54,7 +54,11 @@ export function Search(): React.JSX.Element {
useDocSearchKeyboardEvents({ useDocSearchKeyboardEvents({
isOpen, isOpen,
onOpen: isSearchDisabled ? () => { return void 0 } : onOpen, onOpen: isSearchDisabled
? () => {
return void 0;
}
: onOpen,
onClose, onClose,
}); });
@@ -111,7 +115,6 @@ export function Search(): React.JSX.Element {
}; };
}, [isLightMode]); }, [isLightMode]);
return ( return (
<> <>
<button <button

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import { remToPx } from "@/lib/rem-to-px";
import { createContext, useContext, useEffect, useLayoutEffect, useState } from "react"; import { createContext, useContext, useEffect, useLayoutEffect, useState } from "react";
import { type StoreApi, createStore, useStore } from "zustand"; import { type StoreApi, createStore, useStore } from "zustand";
import { remToPx } from "@/lib/rem-to-px";
export interface Section { export interface Section {
id: string; id: string;
@@ -31,7 +31,9 @@ const createSectionStore = (sections: Section[]) => {
return createStore<SectionState>()((set) => ({ return createStore<SectionState>()((set) => ({
sections, sections,
visibleSections: [], visibleSections: [],
setVisibleSections: (visibleSections) => { set((state) => (state.visibleSections.join() === visibleSections.join() ? {} : { visibleSections })); }, setVisibleSections: (visibleSections) => {
set((state) => (state.visibleSections.join() === visibleSections.join() ? {} : { visibleSections }));
},
registerHeading: ({ id, ref, offsetRem }) => { registerHeading: ({ id, ref, offsetRem }) => {
set((state) => { set((state) => {
return { return {
@@ -92,7 +94,9 @@ const useVisibleSections = (sectionStore: StoreApi<SectionState>) => {
setVisibleSections(newVisibleSections); setVisibleSections(newVisibleSections);
}; };
const raf = window.requestAnimationFrame(() => { checkVisibleSections(); }); const raf = window.requestAnimationFrame(() => {
checkVisibleSections();
});
window.addEventListener("scroll", checkVisibleSections, { passive: true }); window.addEventListener("scroll", checkVisibleSections, { passive: true });
window.addEventListener("resize", checkVisibleSections); window.addEventListener("resize", checkVisibleSections);
@@ -108,13 +112,7 @@ const SectionStoreContext = createContext<StoreApi<SectionState> | null>(null);
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect; const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
export function SectionProvider({ export function SectionProvider({ sections, children }: { sections: Section[]; children: React.ReactNode }) {
sections,
children,
}: {
sections: Section[];
children: React.ReactNode;
}) {
const [sectionStore] = useState(() => createSectionStore(sections)); const [sectionStore] = useState(() => createSectionStore(sections));
useVisibleSections(sectionStore); useVisibleSections(sectionStore);

View File

@@ -48,7 +48,7 @@ export function SideNavigation({ pathname }: { pathname: string }): React.JSX.El
return ( return (
<li <li
key={heading.text} key={heading.text}
className={clsx(`mb-4 text-slate-900 dark:text-white ml-4`, { className={clsx(`mb-4 ml-4 text-slate-900 dark:text-white`, {
"ml-0": heading.level === 2, "ml-0": heading.level === 2,
"ml-4": heading.level === 3, "ml-4": heading.level === 3,
"ml-6": heading.level === 4, "ml-6": heading.level === 4,

View File

@@ -22,4 +22,3 @@ export default function SurveyEmbed({ surveyUrl }: SurveyEmbedProps): React.JSX.
</div> </div>
); );
} }

View File

@@ -12,7 +12,8 @@ export function TellaVideo({ tellaVideoIdentifier }: { tellaVideoIdentifier: str
}} }}
src={`https://www.tella.tv/video/${tellaVideoIdentifier}/embed?b=0&title=0&a=1&loop=0&autoPlay=true&t=0&muted=1&wt=0`} src={`https://www.tella.tv/video/${tellaVideoIdentifier}/embed?b=0&title=0&a=1&loop=0&autoPlay=true&t=0&muted=1&wt=0`}
allowFullScreen allowFullScreen
title="Tella Video Help" /> title="Tella Video Help"
/>
</div> </div>
); );
} }

View File

@@ -35,7 +35,9 @@ export function ThemeToggle(): React.JSX.Element {
type="button" type="button"
className="flex h-6 w-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 dark:hover:bg-white/5" className="flex h-6 w-6 items-center justify-center rounded-md transition hover:bg-zinc-900/5 dark:hover:bg-white/5"
aria-label={mounted ? `Switch to ${otherTheme} theme` : "Toggle theme"} aria-label={mounted ? `Switch to ${otherTheme} theme` : "Toggle theme"}
onClick={() => { setTheme(otherTheme); }}> onClick={() => {
setTheme(otherTheme);
}}>
<SunIcon className="h-5 w-5 stroke-zinc-900 dark:hidden" /> <SunIcon className="h-5 w-5 stroke-zinc-900 dark:hidden" />
<MoonIcon className="hidden h-5 w-5 stroke-white dark:block" /> <MoonIcon className="hidden h-5 w-5 stroke-white dark:block" />
</button> </button>

View File

@@ -14,7 +14,13 @@ export const useMobileNavigationStore = create<{
toggle: () => void; toggle: () => void;
}>()((set) => ({ }>()((set) => ({
isOpen: false, isOpen: false,
open: () => { set({ isOpen: true }); }, open: () => {
close: () => { set({ isOpen: false }); }, set({ isOpen: true });
toggle: () => { set((state) => ({ isOpen: !state.isOpen })); }, },
close: () => {
set({ isOpen: false });
},
toggle: () => {
set((state) => ({ isOpen: !state.isOpen }));
},
})); }));

View File

@@ -24,12 +24,9 @@ export const useTableContentObserver = (setActiveId: (id: string) => void, pathn
useEffect(() => { useEffect(() => {
const callback = (headings: HeadingElement[]) => { const callback = (headings: HeadingElement[]) => {
// Create a map of heading elements, where the key is the heading's ID and the value is the heading element // Create a map of heading elements, where the key is the heading's ID and the value is the heading element
headingElementsRef.current = headings.reduce( headingElementsRef.current = headings.reduce((map, headingElement) => {
(map, headingElement) => { return { ...map, [headingElement.target.id]: headingElement };
return { ...map, [headingElement.target.id]: headingElement }; }, {});
},
{}
);
// Find the visible headings (i.e., headings that are currently intersecting with the viewport) // Find the visible headings (i.e., headings that are currently intersecting with the viewport)
const visibleHeadings: HeadingElement[] = []; const visibleHeadings: HeadingElement[] = [];

View File

@@ -120,6 +120,7 @@ export const navigation: NavGroup[] = [
{ title: "Airtable", href: "/developer-docs/integrations/airtable" }, { title: "Airtable", href: "/developer-docs/integrations/airtable" },
{ title: "Google Sheets", href: "/developer-docs/integrations/google-sheets" }, { title: "Google Sheets", href: "/developer-docs/integrations/google-sheets" },
{ title: "Make", href: "/developer-docs/integrations/make" }, { title: "Make", href: "/developer-docs/integrations/make" },
{ title: "Activepieces", href: "/developer-docs/integrations/activepieces" },
{ title: "n8n", href: "/developer-docs/integrations/n8n" }, { title: "n8n", href: "/developer-docs/integrations/n8n" },
{ title: "Notion", href: "/developer-docs/integrations/notion" }, { title: "Notion", href: "/developer-docs/integrations/notion" },
{ title: "Slack", href: "/developer-docs/integrations/slack" }, { title: "Slack", href: "/developer-docs/integrations/slack" },
@@ -145,6 +146,7 @@ export const navigation: NavGroup[] = [
{ title: "License", href: "/self-hosting/license" }, { title: "License", href: "/self-hosting/license" },
{ title: "Cluster Setup", href: "/self-hosting/cluster-setup" }, { title: "Cluster Setup", href: "/self-hosting/cluster-setup" },
{ title: "Rate Limiting", href: "/self-hosting/rate-limiting" }, { title: "Rate Limiting", href: "/self-hosting/rate-limiting" },
{ title: "Kubernetes", href: "/self-hosting/kubernetes" },
], ],
}, },
{ {

View File

@@ -1,4 +1,4 @@
FROM node:22-alpine AS base FROM node:22-alpine3.20 AS base
# #
## step 1: Prune monorepo ## step 1: Prune monorepo
@@ -18,7 +18,8 @@ FROM node:22-alpine AS base
FROM base AS installer FROM base AS installer
# Enable corepack and prepare pnpm # Enable corepack and prepare pnpm
RUN npm install -g pnpm@9.15.0 RUN npm install -g corepack@latest
RUN corepack enable
# Install necessary build tools and compilers # Install necessary build tools and compilers
RUN apk update && apk add --no-cache g++ cmake make gcc python3 openssl-dev jq RUN apk update && apk add --no-cache g++ cmake make gcc python3 openssl-dev jq
@@ -61,6 +62,8 @@ RUN jq -r '.devDependencies.prisma' packages/database/package.json > /prisma_ver
## step 3: setup production runner ## step 3: setup production runner
# #
FROM base AS runner FROM base AS runner
RUN npm install -g corepack@latest
RUN corepack enable RUN corepack enable
RUN apk add --no-cache curl \ RUN apk add --no-cache curl \

View File

@@ -1,8 +1,8 @@
"use client"; "use client";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react";
import { ArrowRight } from "lucide-react"; import { ArrowRight } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { cn } from "@formbricks/lib/cn"; import { cn } from "@formbricks/lib/cn";
@@ -23,7 +23,7 @@ export const ConnectWithFormbricks = ({
widgetSetupCompleted, widgetSetupCompleted,
channel, channel,
}: ConnectWithFormbricksProps) => { }: ConnectWithFormbricksProps) => {
const t = useTranslations(); const { t } = useTranslate();
const router = useRouter(); const router = useRouter();
const handleFinishOnboarding = async () => { const handleFinishOnboarding = async () => {
router.push(`/environments/${environment.id}/surveys`); router.push(`/environments/${environment.id}/surveys`);

View File

@@ -4,7 +4,7 @@ import { Button } from "@/modules/ui/components/button";
import { CodeBlock } from "@/modules/ui/components/code-block"; import { CodeBlock } from "@/modules/ui/components/code-block";
import { Html5Icon, NpmIcon } from "@/modules/ui/components/icons"; import { Html5Icon, NpmIcon } from "@/modules/ui/components/icons";
import { TabBar } from "@/modules/ui/components/tab-bar"; import { TabBar } from "@/modules/ui/components/tab-bar";
import { useTranslations } from "next-intl"; import { useTranslate } from "@tolgee/react";
import Link from "next/link"; import Link from "next/link";
import "prismjs/themes/prism.css"; import "prismjs/themes/prism.css";
import { useState } from "react"; import { useState } from "react";
@@ -29,7 +29,7 @@ export const OnboardingSetupInstructions = ({
channel, channel,
widgetSetupCompleted, widgetSetupCompleted,
}: OnboardingSetupInstructionsProps) => { }: OnboardingSetupInstructionsProps) => {
const t = useTranslations(); const { t } = useTranslate();
const [activeTab, setActiveTab] = useState(tabs[0].id); const [activeTab, setActiveTab] = useState(tabs[0].id);
const htmlSnippetForAppSurveys = `<!-- START Formbricks Surveys --> const htmlSnippetForAppSurveys = `<!-- START Formbricks Surveys -->
<script type="text/javascript"> <script type="text/javascript">

View File

@@ -1,8 +1,8 @@
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks"; import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header"; import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { XIcon } from "lucide-react"; import { XIcon } from "lucide-react";
import { getTranslations } from "next-intl/server";
import Link from "next/link"; import Link from "next/link";
import { WEBAPP_URL } from "@formbricks/lib/constants"; import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service"; import { getEnvironment } from "@formbricks/lib/environment/service";
@@ -16,7 +16,7 @@ interface ConnectPageProps {
const Page = async (props: ConnectPageProps) => { const Page = async (props: ConnectPageProps) => {
const params = await props.params; const params = await props.params;
const t = await getTranslations(); const t = await getTranslate();
const environment = await getEnvironment(params.environmentId); const environment = await getEnvironment(params.environmentId);
if (!environment) { if (!environment) {

View File

@@ -5,8 +5,8 @@ import { getXMTemplates } from "@/app/(app)/(onboarding)/environments/[environme
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer"; import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { createSurveyAction } from "@/modules/surveys/components/TemplateList/actions"; import { createSurveyAction } from "@/modules/surveys/components/TemplateList/actions";
import { useTranslate } from "@tolgee/react";
import { ActivityIcon, ShoppingCartIcon, SmileIcon, StarIcon, ThumbsUpIcon, UsersIcon } from "lucide-react"; import { ActivityIcon, ShoppingCartIcon, SmileIcon, StarIcon, ThumbsUpIcon, UsersIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
@@ -23,7 +23,7 @@ interface XMTemplateListProps {
export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListProps) => { export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListProps) => {
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null); const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
const t = useTranslations(); const { t } = useTranslate();
const router = useRouter(); const router = useRouter();
const createSurvey = async (activeTemplate: TXMTemplate) => { const createSurvey = async (activeTemplate: TXMTemplate) => {
@@ -47,7 +47,7 @@ export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListP
const handleTemplateClick = (templateIdx: number) => { const handleTemplateClick = (templateIdx: number) => {
setActiveTemplateId(templateIdx); setActiveTemplateId(templateIdx);
const template = getXMTemplates(user.locale)[templateIdx]; const template = getXMTemplates(t)[templateIdx];
const newTemplate = replacePresetPlaceholders(template, project); const newTemplate = replacePresetPlaceholders(template, project);
createSurvey(newTemplate); createSurvey(newTemplate);
}; };

View File

@@ -5,7 +5,7 @@ import { TXMTemplate } from "@formbricks/types/templates";
// replace all occurences of projectName with the actual project name in the current template // replace all occurences of projectName with the actual project name in the current template
export const replacePresetPlaceholders = (template: TXMTemplate, project: TProject) => { export const replacePresetPlaceholders = (template: TXMTemplate, project: TProject) => {
const survey = structuredClone(template); const survey = structuredClone(template);
survey.name = survey.name.replace("{{projectName}}", project.name); survey.name = survey.name.replace("$[projectName]", project.name);
survey.questions = survey.questions.map((question) => { survey.questions = survey.questions.map((question) => {
return replaceQuestionPresetPlaceholders(question, project); return replaceQuestionPresetPlaceholders(question, project);
}); });

View File

@@ -1,25 +1,18 @@
import { getDefaultEndingCard } from "@/app/lib/templates";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import { getDefaultEndingCard, translate } from "@formbricks/lib/templates"; import { TFnType } from "@tolgee/react";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { TXMTemplate } from "@formbricks/types/templates"; import { TXMTemplate } from "@formbricks/types/templates";
function validateLocale(locale: string): boolean {
// Add logic to validate the locale, e.g., check against a list of supported locales
return typeof locale === "string" && locale.length > 0;
}
function logError(error: Error, context: string) { function logError(error: Error, context: string) {
console.error(`Error in ${context}:`, error); console.error(`Error in ${context}:`, error);
} }
export const getXMSurveyDefault = (locale: string): TXMTemplate => { export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
try { try {
if (!validateLocale(locale)) {
throw new Error("Invalid locale");
}
return { return {
name: "", name: "",
endings: [getDefaultEndingCard([], locale)], endings: [getDefaultEndingCard([], t)],
questions: [], questions: [],
styling: { styling: {
overwriteThemeStyling: true, overwriteThemeStyling: true,
@@ -31,24 +24,24 @@ export const getXMSurveyDefault = (locale: string): TXMTemplate => {
} }
}; };
const NPSSurvey = (locale: string): TXMTemplate => { const npsSurvey = (t: TFnType): TXMTemplate => {
return { return {
...getXMSurveyDefault(locale), ...getXMSurveyDefault(t),
name: translate("nps_survey_name", locale), name: t("templates.nps_survey_name"),
questions: [ questions: [
{ {
id: createId(), id: createId(),
type: TSurveyQuestionTypeEnum.NPS, type: TSurveyQuestionTypeEnum.NPS,
headline: { default: translate("nps_survey_question_1_headline", locale) }, headline: { default: t("templates.nps_survey_question_1_headline") },
required: true, required: true,
lowerLabel: { default: translate("nps_survey_question_1_lower_label", locale) }, lowerLabel: { default: t("templates.nps_survey_question_1_lower_label") },
upperLabel: { default: translate("nps_survey_question_1_upper_label", locale) }, upperLabel: { default: t("templates.nps_survey_question_1_upper_label") },
isColorCodingEnabled: true, isColorCodingEnabled: true,
}, },
{ {
id: createId(), id: createId(),
type: TSurveyQuestionTypeEnum.OpenText, type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("nps_survey_question_2_headline", locale) }, headline: { default: t("templates.nps_survey_question_2_headline") },
required: false, required: false,
inputType: "text", inputType: "text",
charLimit: { charLimit: {
@@ -58,7 +51,7 @@ const NPSSurvey = (locale: string): TXMTemplate => {
{ {
id: createId(), id: createId(),
type: TSurveyQuestionTypeEnum.OpenText, type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("nps_survey_question_3_headline", locale) }, headline: { default: t("templates.nps_survey_question_3_headline") },
required: false, required: false,
inputType: "text", inputType: "text",
charLimit: { charLimit: {
@@ -69,13 +62,13 @@ const NPSSurvey = (locale: string): TXMTemplate => {
}; };
}; };
const StarRatingSurvey = (locale: string): TXMTemplate => { const starRatingSurvey = (t: TFnType): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()]; const reusableQuestionIds = [createId(), createId(), createId()];
const defaultSurvey = getXMSurveyDefault(locale); const defaultSurvey = getXMSurveyDefault(t);
return { return {
...defaultSurvey, ...defaultSurvey,
name: translate("star_rating_survey_name", locale), name: t("templates.star_rating_survey_name"),
questions: [ questions: [
{ {
id: reusableQuestionIds[0], id: reusableQuestionIds[0],
@@ -112,15 +105,15 @@ const StarRatingSurvey = (locale: string): TXMTemplate => {
], ],
range: 5, range: 5,
scale: "number", scale: "number",
headline: { default: translate("star_rating_survey_question_1_headline", locale) }, headline: { default: t("templates.star_rating_survey_question_1_headline") },
required: true, required: true,
lowerLabel: { default: translate("star_rating_survey_question_1_lower_label", locale) }, lowerLabel: { default: t("templates.star_rating_survey_question_1_lower_label") },
upperLabel: { default: translate("star_rating_survey_question_1_upper_label", locale) }, upperLabel: { default: t("templates.star_rating_survey_question_1_upper_label") },
isColorCodingEnabled: false, isColorCodingEnabled: false,
}, },
{ {
id: reusableQuestionIds[1], id: reusableQuestionIds[1],
html: { default: translate("star_rating_survey_question_2_html", locale) }, html: { default: t("templates.star_rating_survey_question_2_html") },
type: TSurveyQuestionTypeEnum.CTA, type: TSurveyQuestionTypeEnum.CTA,
logic: [ logic: [
{ {
@@ -148,20 +141,20 @@ const StarRatingSurvey = (locale: string): TXMTemplate => {
], ],
}, },
], ],
headline: { default: translate("star_rating_survey_question_2_headline", locale) }, headline: { default: t("templates.star_rating_survey_question_2_headline") },
required: true, required: true,
buttonUrl: "https://formbricks.com/github", buttonUrl: "https://formbricks.com/github",
buttonLabel: { default: translate("star_rating_survey_question_2_button_label", locale) }, buttonLabel: { default: t("templates.star_rating_survey_question_2_button_label") },
buttonExternal: true, buttonExternal: true,
}, },
{ {
id: reusableQuestionIds[2], id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText, type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: "Sorry to hear! What is ONE thing we can do better?" }, headline: { default: t("templates.star_rating_survey_question_3_headline") },
required: true, required: true,
subheader: { default: "Help us improve your experience." }, subheader: { default: t("templates.star_rating_survey_question_3_subheader") },
buttonLabel: { default: "Send" }, buttonLabel: { default: t("templates.star_rating_survey_question_3_button_label") },
placeholder: { default: "Type your answer here..." }, placeholder: { default: t("templates.star_rating_survey_question_3_placeholder") },
inputType: "text", inputType: "text",
charLimit: { charLimit: {
enabled: false, enabled: false,
@@ -171,13 +164,13 @@ const StarRatingSurvey = (locale: string): TXMTemplate => {
}; };
}; };
const CSATSurvey = (locale: string): TXMTemplate => { const csatSurvey = (t: TFnType): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()]; const reusableQuestionIds = [createId(), createId(), createId()];
const defaultSurvey = getXMSurveyDefault(locale); const defaultSurvey = getXMSurveyDefault(t);
return { return {
...defaultSurvey, ...defaultSurvey,
name: translate("csat_survey_name", locale), name: t("templates.csat_survey_name"),
questions: [ questions: [
{ {
id: reusableQuestionIds[0], id: reusableQuestionIds[0],
@@ -214,10 +207,10 @@ const CSATSurvey = (locale: string): TXMTemplate => {
], ],
range: 5, range: 5,
scale: "smiley", scale: "smiley",
headline: { default: translate("csat_survey_question_1_headline", locale) }, headline: { default: t("templates.csat_survey_question_1_headline") },
required: true, required: true,
lowerLabel: { default: translate("csat_survey_question_1_lower_label", locale) }, lowerLabel: { default: t("templates.csat_survey_question_1_lower_label") },
upperLabel: { default: translate("csat_survey_question_1_upper_label", locale) }, upperLabel: { default: t("templates.csat_survey_question_1_upper_label") },
isColorCodingEnabled: false, isColorCodingEnabled: false,
}, },
{ {
@@ -249,9 +242,9 @@ const CSATSurvey = (locale: string): TXMTemplate => {
], ],
}, },
], ],
headline: { default: translate("csat_survey_question_2_headline", locale) }, headline: { default: t("templates.csat_survey_question_2_headline") },
required: false, required: false,
placeholder: { default: translate("csat_survey_question_2_placeholder", locale) }, placeholder: { default: t("templates.csat_survey_question_2_placeholder") },
inputType: "text", inputType: "text",
charLimit: { charLimit: {
enabled: false, enabled: false,
@@ -260,9 +253,9 @@ const CSATSurvey = (locale: string): TXMTemplate => {
{ {
id: reusableQuestionIds[2], id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText, type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("csat_survey_question_3_headline", locale) }, headline: { default: t("templates.csat_survey_question_3_headline") },
required: false, required: false,
placeholder: { default: translate("csat_survey_question_3_placeholder", locale) }, placeholder: { default: t("templates.csat_survey_question_3_placeholder") },
inputType: "text", inputType: "text",
charLimit: { charLimit: {
enabled: false, enabled: false,
@@ -272,28 +265,28 @@ const CSATSurvey = (locale: string): TXMTemplate => {
}; };
}; };
const CESSurvey = (locale: string): TXMTemplate => { const cessSurvey = (t: TFnType): TXMTemplate => {
return { return {
...getXMSurveyDefault(locale), ...getXMSurveyDefault(t),
name: translate("cess_survey_name", locale), name: t("templates.cess_survey_name"),
questions: [ questions: [
{ {
id: createId(), id: createId(),
type: TSurveyQuestionTypeEnum.Rating, type: TSurveyQuestionTypeEnum.Rating,
range: 5, range: 5,
scale: "number", scale: "number",
headline: { default: translate("cess_survey_question_1_headline", locale) }, headline: { default: t("templates.cess_survey_question_1_headline") },
required: true, required: true,
lowerLabel: { default: translate("cess_survey_question_1_lower_label", locale) }, lowerLabel: { default: t("templates.cess_survey_question_1_lower_label") },
upperLabel: { default: translate("cess_survey_question_1_upper_label", locale) }, upperLabel: { default: t("templates.cess_survey_question_1_upper_label") },
isColorCodingEnabled: false, isColorCodingEnabled: false,
}, },
{ {
id: createId(), id: createId(),
type: TSurveyQuestionTypeEnum.OpenText, type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("cess_survey_question_2_headline", locale) }, headline: { default: t("templates.cess_survey_question_2_headline") },
required: true, required: true,
placeholder: { default: translate("cess_survey_question_2_placeholder", locale) }, placeholder: { default: t("templates.cess_survey_question_2_placeholder") },
inputType: "text", inputType: "text",
charLimit: { charLimit: {
enabled: false, enabled: false,
@@ -303,13 +296,13 @@ const CESSurvey = (locale: string): TXMTemplate => {
}; };
}; };
const SmileysRatingSurvey = (locale: string): TXMTemplate => { const smileysRatingSurvey = (t: TFnType): TXMTemplate => {
const reusableQuestionIds = [createId(), createId(), createId()]; const reusableQuestionIds = [createId(), createId(), createId()];
const defaultSurvey = getXMSurveyDefault(locale); const defaultSurvey = getXMSurveyDefault(t);
return { return {
...defaultSurvey, ...defaultSurvey,
name: translate("smileys_survey_name", locale), name: t("templates.smileys_survey_name"),
questions: [ questions: [
{ {
id: reusableQuestionIds[0], id: reusableQuestionIds[0],
@@ -346,15 +339,15 @@ const SmileysRatingSurvey = (locale: string): TXMTemplate => {
], ],
range: 5, range: 5,
scale: "smiley", scale: "smiley",
headline: { default: translate("smileys_survey_question_1_headline", locale) }, headline: { default: t("templates.smileys_survey_question_1_headline") },
required: true, required: true,
lowerLabel: { default: translate("smileys_survey_question_1_lower_label", locale) }, lowerLabel: { default: t("templates.smileys_survey_question_1_lower_label") },
upperLabel: { default: translate("smileys_survey_question_1_upper_label", locale) }, upperLabel: { default: t("templates.smileys_survey_question_1_upper_label") },
isColorCodingEnabled: false, isColorCodingEnabled: false,
}, },
{ {
id: reusableQuestionIds[1], id: reusableQuestionIds[1],
html: { default: translate("smileys_survey_question_2_html", locale) }, html: { default: t("templates.smileys_survey_question_2_html") },
type: TSurveyQuestionTypeEnum.CTA, type: TSurveyQuestionTypeEnum.CTA,
logic: [ logic: [
{ {
@@ -382,20 +375,20 @@ const SmileysRatingSurvey = (locale: string): TXMTemplate => {
], ],
}, },
], ],
headline: { default: translate("smileys_survey_question_2_headline", locale) }, headline: { default: t("templates.smileys_survey_question_2_headline") },
required: true, required: true,
buttonUrl: "https://formbricks.com/github", buttonUrl: "https://formbricks.com/github",
buttonLabel: { default: translate("smileys_survey_question_2_button_label", locale) }, buttonLabel: { default: t("templates.smileys_survey_question_2_button_label") },
buttonExternal: true, buttonExternal: true,
}, },
{ {
id: reusableQuestionIds[2], id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText, type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("smileys_survey_question_3_headline", locale) }, headline: { default: t("templates.smileys_survey_question_3_headline") },
required: true, required: true,
subheader: { default: translate("smileys_survey_question_3_subheader", locale) }, subheader: { default: t("templates.smileys_survey_question_3_subheader") },
buttonLabel: { default: translate("smileys_survey_question_3_button_label", locale) }, buttonLabel: { default: t("templates.smileys_survey_question_3_button_label") },
placeholder: { default: translate("smileys_survey_question_3_placeholder", locale) }, placeholder: { default: t("templates.smileys_survey_question_3_placeholder") },
inputType: "text", inputType: "text",
charLimit: { charLimit: {
enabled: false, enabled: false,
@@ -405,26 +398,26 @@ const SmileysRatingSurvey = (locale: string): TXMTemplate => {
}; };
}; };
const eNPSSurvey = (locale: string): TXMTemplate => { const enpsSurvey = (t: TFnType): TXMTemplate => {
return { return {
...getXMSurveyDefault(locale), ...getXMSurveyDefault(t),
name: translate("enps_survey_name", locale), name: t("templates.enps_survey_name"),
questions: [ questions: [
{ {
id: createId(), id: createId(),
type: TSurveyQuestionTypeEnum.NPS, type: TSurveyQuestionTypeEnum.NPS,
headline: { headline: {
default: translate("enps_survey_question_1_headline", locale), default: t("templates.enps_survey_question_1_headline"),
}, },
required: false, required: false,
lowerLabel: { default: translate("enps_survey_question_1_lower_label", locale) }, lowerLabel: { default: t("templates.enps_survey_question_1_lower_label") },
upperLabel: { default: translate("enps_survey_question_1_upper_label", locale) }, upperLabel: { default: t("templates.enps_survey_question_1_upper_label") },
isColorCodingEnabled: true, isColorCodingEnabled: true,
}, },
{ {
id: createId(), id: createId(),
type: TSurveyQuestionTypeEnum.OpenText, type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("enps_survey_question_2_headline", locale) }, headline: { default: t("templates.enps_survey_question_2_headline") },
required: false, required: false,
inputType: "text", inputType: "text",
charLimit: { charLimit: {
@@ -434,7 +427,7 @@ const eNPSSurvey = (locale: string): TXMTemplate => {
{ {
id: createId(), id: createId(),
type: TSurveyQuestionTypeEnum.OpenText, type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("enps_survey_question_3_headline", locale) }, headline: { default: t("templates.enps_survey_question_3_headline") },
required: false, required: false,
inputType: "text", inputType: "text",
charLimit: { charLimit: {
@@ -445,18 +438,15 @@ const eNPSSurvey = (locale: string): TXMTemplate => {
}; };
}; };
export const getXMTemplates = (locale: string): TXMTemplate[] => { export const getXMTemplates = (t: TFnType): TXMTemplate[] => {
try { try {
if (!validateLocale(locale)) {
throw new Error("Invalid locale");
}
return [ return [
NPSSurvey(locale), npsSurvey(t),
StarRatingSurvey(locale), starRatingSurvey(t),
CSATSurvey(locale), csatSurvey(t),
CESSurvey(locale), cessSurvey(t),
SmileysRatingSurvey(locale), smileysRatingSurvey(t),
eNPSSurvey(locale), enpsSurvey(t),
]; ];
} catch (error) { } catch (error) {
logError(error, "getXMTemplates"); logError(error, "getXMTemplates");

View File

@@ -3,9 +3,9 @@ import { getOrganizationIdFromEnvironmentId } from "@/lib/utils/helper";
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header"; import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { XIcon } from "lucide-react"; import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import Link from "next/link"; import Link from "next/link";
import { getEnvironment } from "@formbricks/lib/environment/service"; import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProjectByEnvironmentId, getUserProjects } from "@formbricks/lib/project/service"; import { getProjectByEnvironmentId, getUserProjects } from "@formbricks/lib/project/service";
@@ -21,7 +21,7 @@ const Page = async (props: XMTemplatePageProps) => {
const params = await props.params; const params = await props.params;
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
const environment = await getEnvironment(params.environmentId); const environment = await getEnvironment(params.environmentId);
const t = await getTranslations(); const t = await getTranslate();
if (!session) { if (!session) {
throw new Error(t("common.session_not_found")); throw new Error(t("common.session_not_found"));
} }

View File

@@ -17,9 +17,9 @@ import {
DropdownMenuSubTrigger, DropdownMenuSubTrigger,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu"; } from "@/modules/ui/components/dropdown-menu";
import { useTranslate } from "@tolgee/react";
import { ArrowUpRightIcon, ChevronRightIcon, LogOutIcon, PlusIcon } from "lucide-react"; import { ArrowUpRightIcon, ChevronRightIcon, LogOutIcon, PlusIcon } from "lucide-react";
import { signOut } from "next-auth/react"; import { signOut } from "next-auth/react";
import { useTranslations } from "next-intl";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
@@ -44,7 +44,7 @@ export const LandingSidebar = ({
}: LandingSidebarProps) => { }: LandingSidebarProps) => {
const [openCreateOrganizationModal, setOpenCreateOrganizationModal] = useState<boolean>(false); const [openCreateOrganizationModal, setOpenCreateOrganizationModal] = useState<boolean>(false);
const t = useTranslations(); const { t } = useTranslate();
const router = useRouter(); const router = useRouter();

View File

@@ -2,15 +2,15 @@ import { LandingSidebar } from "@/app/(app)/(onboarding)/organizations/[organiza
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils"; import { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
import { Header } from "@/modules/ui/components/header"; import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { getOrganization, getOrganizationsByUserId } from "@formbricks/lib/organization/service"; import { getOrganization, getOrganizationsByUserId } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service"; import { getUser } from "@formbricks/lib/user/service";
const Page = async (props) => { const Page = async (props) => {
const params = await props.params; const params = await props.params;
const t = await getTranslations(); const t = await getTranslate();
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session || !session.user) { if (!session || !session.user) {
return redirect(`/auth/login`); return redirect(`/auth/login`);

View File

@@ -1,8 +1,8 @@
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify"; import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { ToasterClient } from "@/modules/ui/components/toaster-client"; import { ToasterClient } from "@/modules/ui/components/toaster-client";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth"; import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
import { getOrganization } from "@formbricks/lib/organization/service"; import { getOrganization } from "@formbricks/lib/organization/service";
@@ -14,7 +14,7 @@ const ProjectOnboardingLayout = async (props) => {
const { children } = props; const { children } = props;
const t = await getTranslations(); const t = await getTranslate();
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session || !session.user) { if (!session || !session.user) {
return redirect(`/auth/login`); return redirect(`/auth/login`);

View File

@@ -2,9 +2,9 @@ import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizatio
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header"; import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react"; import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getUserProjects } from "@formbricks/lib/project/service"; import { getUserProjects } from "@formbricks/lib/project/service";
@@ -22,7 +22,7 @@ const Page = async (props: ChannelPageProps) => {
return redirect(`/auth/login`); return redirect(`/auth/login`);
} }
const t = await getTranslations(); const t = await getTranslate();
const channelOptions = [ const channelOptions = [
{ {
title: t("organizations.projects.new.channel.link_and_email_surveys"), title: t("organizations.projects.new.channel.link_and_email_surveys"),

View File

@@ -1,7 +1,7 @@
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils"; import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service"; import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils"; import { getAccessFlags } from "@formbricks/lib/membership/utils";
@@ -12,7 +12,7 @@ const OnboardingLayout = async (props) => {
const params = await props.params; const params = await props.params;
const { children } = props; const { children } = props;
const t = await getTranslations(); const t = await getTranslate();
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session || !session.user) { if (!session || !session.user) {

View File

@@ -2,9 +2,9 @@ import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizatio
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header"; import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react"; import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getUserProjects } from "@formbricks/lib/project/service"; import { getUserProjects } from "@formbricks/lib/project/service";
@@ -22,7 +22,7 @@ const Page = async (props: ModePageProps) => {
return redirect(`/auth/login`); return redirect(`/auth/login`);
} }
const t = await getTranslations(); const t = await getTranslate();
const channelOptions = [ const channelOptions = [
{ {
title: t("organizations.projects.new.mode.formbricks_surveys"), title: t("organizations.projects.new.mode.formbricks_surveys"),

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions"; import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
import { previewSurvey } from "@/app/lib/templates";
import { getFormattedErrorMessage } from "@/lib/utils/helper"; import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team"; import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal"; import { CreateTeamModal } from "@/modules/ee/teams/team-list/components/create-team-modal";
@@ -19,14 +20,13 @@ import { Input } from "@/modules/ui/components/input";
import { MultiSelect } from "@/modules/ui/components/multi-select"; import { MultiSelect } from "@/modules/ui/components/multi-select";
import { SurveyInline } from "@/modules/ui/components/survey"; import { SurveyInline } from "@/modules/ui/components/survey";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl"; import { useTranslate } from "@tolgee/react";
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage"; import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
import { getPreviewSurvey } from "@formbricks/lib/styling/constants";
import { import {
TProjectConfigChannel, TProjectConfigChannel,
TProjectConfigIndustry, TProjectConfigIndustry,
@@ -43,7 +43,6 @@ interface ProjectSettingsProps {
defaultBrandColor: string; defaultBrandColor: string;
organizationTeams: TOrganizationTeam[]; organizationTeams: TOrganizationTeam[];
canDoRoleManagement: boolean; canDoRoleManagement: boolean;
locale: string;
userProjectsCount: number; userProjectsCount: number;
} }
@@ -55,13 +54,12 @@ export const ProjectSettings = ({
defaultBrandColor, defaultBrandColor,
organizationTeams, organizationTeams,
canDoRoleManagement = false, canDoRoleManagement = false,
locale,
userProjectsCount, userProjectsCount,
}: ProjectSettingsProps) => { }: ProjectSettingsProps) => {
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false); const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const { t } = useTranslate();
const addProject = async (data: TProjectUpdateInput) => { const addProject = async (data: TProjectUpdateInput) => {
try { try {
const createProjectResponse = await createProjectAction({ const createProjectResponse = await createProjectAction({
@@ -233,7 +231,7 @@ export const ProjectSettings = ({
<p className="text-sm text-slate-400">{t("common.preview")}</p> <p className="text-sm text-slate-400">{t("common.preview")}</p>
<div className="z-0 h-3/4 w-3/4"> <div className="z-0 h-3/4 w-3/4">
<SurveyInline <SurveyInline
survey={getPreviewSurvey(locale, projectName || "my Product")} survey={previewSurvey(projectName || "my Product", t)}
styling={{ brandColor: { light: brandColor } }} styling={{ brandColor: { light: brandColor } }}
isBrandingEnabled={false} isBrandingEnabled={false}
languageCode="default" languageCode="default"

View File

@@ -4,15 +4,14 @@ import { authOptions } from "@/modules/auth/lib/authOptions";
import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils"; import { getRoleManagementPermission } from "@/modules/ee/license-check/lib/utils";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header"; import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { XIcon } from "lucide-react"; import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { DEFAULT_BRAND_COLOR, DEFAULT_LOCALE } from "@formbricks/lib/constants"; import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
import { getOrganization } from "@formbricks/lib/organization/service"; import { getOrganization } from "@formbricks/lib/organization/service";
import { getUserProjects } from "@formbricks/lib/project/service"; import { getUserProjects } from "@formbricks/lib/project/service";
import { getUserLocale } from "@formbricks/lib/user/service";
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project"; import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
interface ProjectSettingsPageProps { interface ProjectSettingsPageProps {
@@ -29,7 +28,7 @@ interface ProjectSettingsPageProps {
const Page = async (props: ProjectSettingsPageProps) => { const Page = async (props: ProjectSettingsPageProps) => {
const searchParams = await props.searchParams; const searchParams = await props.searchParams;
const params = await props.params; const params = await props.params;
const t = await getTranslations(); const t = await getTranslate();
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session || !session.user) { if (!session || !session.user) {
@@ -39,7 +38,6 @@ const Page = async (props: ProjectSettingsPageProps) => {
const channel = searchParams.channel || null; const channel = searchParams.channel || null;
const industry = searchParams.industry || null; const industry = searchParams.industry || null;
const mode = searchParams.mode || "surveys"; const mode = searchParams.mode || "surveys";
const locale = session?.user.id ? await getUserLocale(session.user.id) : undefined;
const projects = await getUserProjects(session.user.id, params.organizationId); const projects = await getUserProjects(session.user.id, params.organizationId);
const organizationTeams = await getTeamsByOrganizationId(params.organizationId); const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
@@ -70,7 +68,6 @@ const Page = async (props: ProjectSettingsPageProps) => {
defaultBrandColor={DEFAULT_BRAND_COLOR} defaultBrandColor={DEFAULT_BRAND_COLOR}
organizationTeams={organizationTeams} organizationTeams={organizationTeams}
canDoRoleManagement={canDoRoleManagement} canDoRoleManagement={canDoRoleManagement}
locale={locale ?? DEFAULT_LOCALE}
userProjectsCount={projects.length} userProjectsCount={projects.length}
/> />
{projects.length >= 1 && ( {projects.length >= 1 && (

View File

@@ -4,8 +4,8 @@ import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]
import { authOptions } from "@/modules/auth/lib/authOptions"; import { authOptions } from "@/modules/auth/lib/authOptions";
import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner"; import { DevEnvironmentBanner } from "@/modules/ui/components/dev-environment-banner";
import { ToasterClient } from "@/modules/ui/components/toaster-client"; import { ToasterClient } from "@/modules/ui/components/toaster-client";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getEnvironment } from "@formbricks/lib/environment/service"; import { getEnvironment } from "@formbricks/lib/environment/service";
@@ -18,7 +18,7 @@ const SurveyEditorEnvironmentLayout = async (props) => {
const { children } = props; const { children } = props;
const t = await getTranslations(); const t = await getTranslate();
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session || !session.user) { if (!session || !session.user) {
return redirect(`/auth/login`); return redirect(`/auth/login`);

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { ModalWithTabs } from "@/modules/ui/components/modal-with-tabs"; import { ModalWithTabs } from "@/modules/ui/components/modal-with-tabs";
import { useTranslations } from "next-intl"; import { useTranslate } from "@tolgee/react";
import { TActionClass } from "@formbricks/types/action-classes"; import { TActionClass } from "@formbricks/types/action-classes";
import { TSurvey } from "@formbricks/types/surveys/types"; import { TSurvey } from "@formbricks/types/surveys/types";
import { CreateNewActionTab } from "./CreateNewActionTab"; import { CreateNewActionTab } from "./CreateNewActionTab";
@@ -28,7 +28,7 @@ export const AddActionModal = ({
isReadOnly, isReadOnly,
environmentId, environmentId,
}: AddActionModalProps) => { }: AddActionModalProps) => {
const t = useTranslations(); const { t } = useTranslate();
const tabs = [ const tabs = [
{ {
title: t("environments.surveys.edit.select_saved_action"), title: t("environments.surveys.edit.select_saved_action"),

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { TSurvey } from "@formbricks/types/surveys/types"; import { TSurvey } from "@formbricks/types/surveys/types";
interface AddEndingCardButtonProps { interface AddEndingCardButtonProps {
@@ -11,7 +11,7 @@ interface AddEndingCardButtonProps {
} }
export const AddEndingCardButton = ({ localSurvey, addEndingCard }: AddEndingCardButtonProps) => { export const AddEndingCardButton = ({ localSurvey, addEndingCard }: AddEndingCardButtonProps) => {
const t = useTranslations(); const { t } = useTranslate();
return ( return (
<div <div
className="group inline-flex rounded-lg border border-slate-300 bg-slate-50 hover:cursor-pointer hover:bg-white" className="group inline-flex rounded-lg border border-slate-300 bg-slate-50 hover:cursor-pointer hover:bg-white"

View File

@@ -3,8 +3,8 @@
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import { cn } from "@formbricks/lib/cn"; import { cn } from "@formbricks/lib/cn";
import { import {
@@ -19,14 +19,13 @@ interface AddQuestionButtonProps {
addQuestion: (question: any) => void; addQuestion: (question: any) => void;
project: TProject; project: TProject;
isCxMode: boolean; isCxMode: boolean;
locale: string;
} }
export const AddQuestionButton = ({ addQuestion, project, isCxMode, locale }: AddQuestionButtonProps) => { export const AddQuestionButton = ({ addQuestion, project, isCxMode }: AddQuestionButtonProps) => {
const t = useTranslations(); const { t } = useTranslate();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [hoveredQuestionId, setHoveredQuestionId] = useState<string | null>(null); const [hoveredQuestionId, setHoveredQuestionId] = useState<string | null>(null);
const availableQuestionTypes = isCxMode ? getCXQuestionTypes(locale) : getQuestionTypes(locale); const availableQuestionTypes = isCxMode ? getCXQuestionTypes(t) : getQuestionTypes(t);
const [parent] = useAutoAnimate(); const [parent] = useAutoAnimate();
return ( return (
@@ -60,7 +59,7 @@ export const AddQuestionButton = ({ addQuestion, project, isCxMode, locale }: Ad
onClick={() => { onClick={() => {
addQuestion({ addQuestion({
...universalQuestionPresets, ...universalQuestionPresets,
...getQuestionDefaults(questionType.id, project, locale), ...getQuestionDefaults(questionType.id, project, t),
id: createId(), id: createId(),
type: questionType.id, type: questionType.id,
}); });

View File

@@ -4,8 +4,8 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { QuestionToggleTable } from "@/modules/ui/components/question-toggle-table"; import { QuestionToggleTable } from "@/modules/ui/components/question-toggle-table";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { type JSX, useEffect } from "react"; import { type JSX, useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils"; import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys/types"; import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys/types";
@@ -34,7 +34,7 @@ export const AddressQuestionForm = ({
locale, locale,
}: AddressQuestionFormProps): JSX.Element => { }: AddressQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []); const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
const t = useTranslations(); const { t } = useTranslate();
const fields = [ const fields = [
{ {
id: "addressLine1", id: "addressLine1",

View File

@@ -5,8 +5,8 @@ import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@/
import { Slider } from "@/modules/ui/components/slider"; import { Slider } from "@/modules/ui/components/slider";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { CheckIcon } from "lucide-react"; import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { UseFormReturn } from "react-hook-form"; import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn"; import { cn } from "@formbricks/lib/cn";
import { TProjectStyling } from "@formbricks/types/project"; import { TProjectStyling } from "@formbricks/types/project";
@@ -34,7 +34,7 @@ export const BackgroundStylingCard = ({
isUnsplashConfigured, isUnsplashConfigured,
form, form,
}: BackgroundStylingCardProps) => { }: BackgroundStylingCardProps) => {
const t = useTranslations(); const { t } = useTranslate();
const [parent] = useAutoAnimate(); const [parent] = useAutoAnimate();
return ( return (

View File

@@ -6,7 +6,7 @@ import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label"; import { Label } from "@/modules/ui/components/label";
import { OptionsSwitch } from "@/modules/ui/components/options-switch"; import { OptionsSwitch } from "@/modules/ui/components/options-switch";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslations } from "next-intl"; import { useTranslate } from "@tolgee/react";
import { type JSX, useState } from "react"; import { type JSX, useState } from "react";
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys/types"; import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user"; import { TUserLocale } from "@formbricks/types/user";
@@ -34,7 +34,7 @@ export const CTAQuestionForm = ({
setSelectedLanguageCode, setSelectedLanguageCode,
locale, locale,
}: CTAQuestionFormProps): JSX.Element => { }: CTAQuestionFormProps): JSX.Element => {
const t = useTranslations(); const { t } = useTranslate();
const options = [ const options = [
{ {
value: "internal", value: "internal",

View File

@@ -1,10 +1,12 @@
"use client";
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput"; import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle"; import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input"; import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label"; import { Label } from "@/modules/ui/components/label";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { type JSX, useEffect, useState } from "react"; import { type JSX, useEffect, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils"; import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys/types"; import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys/types";
@@ -34,7 +36,7 @@ export const CalQuestionForm = ({
}: CalQuestionFormProps): JSX.Element => { }: CalQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages); const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const [isCalHostEnabled, setIsCalHostEnabled] = useState(!!question.calHost); const [isCalHostEnabled, setIsCalHostEnabled] = useState(!!question.calHost);
const t = useTranslations(); const { t } = useTranslate();
useEffect(() => { useEffect(() => {
if (!isCalHostEnabled) { if (!isCalHostEnabled) {
updateQuestion(questionIdx, { calHost: undefined }); updateQuestion(questionIdx, { calHost: undefined });

View File

@@ -8,8 +8,8 @@ import { Slider } from "@/modules/ui/components/slider";
import { Switch } from "@/modules/ui/components/switch"; import { Switch } from "@/modules/ui/components/switch";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { CheckIcon } from "lucide-react"; import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import React from "react"; import React from "react";
import { UseFormReturn } from "react-hook-form"; import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn"; import { cn } from "@formbricks/lib/cn";
@@ -36,7 +36,7 @@ export const CardStylingSettings = ({
setOpen, setOpen,
form, form,
}: CardStylingSettingsProps) => { }: CardStylingSettingsProps) => {
const t = useTranslations(); const { t } = useTranslate();
const isAppSurvey = surveyType === "app"; const isAppSurvey = surveyType === "app";
const surveyTypeDerived = isAppSurvey ? "App" : "Link"; const surveyTypeDerived = isAppSurvey ? "App" : "Link";
const isLogoVisible = !!project.logo?.url; const isLogoVisible = !!project.logo?.url;

View File

@@ -1,3 +1,5 @@
"use client";
import { LogicEditor } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditor"; import { LogicEditor } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditor";
import { import {
getDefaultOperatorForQuestion, getDefaultOperatorForQuestion,
@@ -13,6 +15,7 @@ import {
import { Label } from "@/modules/ui/components/label"; import { Label } from "@/modules/ui/components/label";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import { useTranslate } from "@tolgee/react";
import { import {
ArrowDownIcon, ArrowDownIcon,
ArrowUpIcon, ArrowUpIcon,
@@ -22,7 +25,6 @@ import {
SplitIcon, SplitIcon,
TrashIcon, TrashIcon,
} from "lucide-react"; } from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo } from "react"; import { useMemo } from "react";
import { duplicateLogicItem } from "@formbricks/lib/surveyLogic/utils"; import { duplicateLogicItem } from "@formbricks/lib/surveyLogic/utils";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall"; import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
@@ -41,7 +43,7 @@ export function ConditionalLogic({
questionIdx, questionIdx,
updateQuestion, updateQuestion,
}: ConditionalLogicProps) { }: ConditionalLogicProps) {
const t = useTranslations(); const { t } = useTranslate();
const transformedSurvey = useMemo(() => { const transformedSurvey = useMemo(() => {
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default"); let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default");
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default"); modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default");

View File

@@ -3,7 +3,7 @@
import { LocalizedEditor } from "@/modules/ee/multi-language-surveys/components/localized-editor"; import { LocalizedEditor } from "@/modules/ee/multi-language-surveys/components/localized-editor";
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput"; import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { Label } from "@/modules/ui/components/label"; import { Label } from "@/modules/ui/components/label";
import { useTranslations } from "next-intl"; import { useTranslate } from "@tolgee/react";
import { type JSX, useState } from "react"; import { type JSX, useState } from "react";
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys/types"; import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys/types";
import { TUserLocale } from "@formbricks/types/user"; import { TUserLocale } from "@formbricks/types/user";
@@ -30,7 +30,7 @@ export const ConsentQuestionForm = ({
locale, locale,
}: ConsentQuestionFormProps): JSX.Element => { }: ConsentQuestionFormProps): JSX.Element => {
const [firstRender, setFirstRender] = useState(true); const [firstRender, setFirstRender] = useState(true);
const t = useTranslations(); const { t } = useTranslate();
return ( return (
<form> <form>
<QuestionFormInput <QuestionFormInput

View File

@@ -4,8 +4,8 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { QuestionToggleTable } from "@/modules/ui/components/question-toggle-table"; import { QuestionToggleTable } from "@/modules/ui/components/question-toggle-table";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { type JSX, useEffect } from "react"; import { type JSX, useEffect } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils"; import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TSurvey, TSurveyContactInfoQuestion } from "@formbricks/types/surveys/types"; import { TSurvey, TSurveyContactInfoQuestion } from "@formbricks/types/surveys/types";
@@ -33,7 +33,7 @@ export const ContactInfoQuestionForm = ({
setSelectedLanguageCode, setSelectedLanguageCode,
locale, locale,
}: ContactInfoQuestionFormProps): JSX.Element => { }: ContactInfoQuestionFormProps): JSX.Element => {
const t = useTranslations(); const { t } = useTranslate();
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []); const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
const fields = [ const fields = [

View File

@@ -1,3 +1,5 @@
"use client";
import { isValidCssSelector } from "@/app/lib/actionClass/actionClass"; import { isValidCssSelector } from "@/app/lib/actionClass/actionClass";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { CodeActionForm } from "@/modules/ui/components/code-action-form"; import { CodeActionForm } from "@/modules/ui/components/code-action-form";
@@ -7,7 +9,7 @@ import { Label } from "@/modules/ui/components/label";
import { NoCodeActionForm } from "@/modules/ui/components/no-code-action-form"; import { NoCodeActionForm } from "@/modules/ui/components/no-code-action-form";
import { TabToggle } from "@/modules/ui/components/tab-toggle"; import { TabToggle } from "@/modules/ui/components/tab-toggle";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl"; import { useTranslate } from "@tolgee/react";
import { useMemo } from "react"; import { useMemo } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
@@ -38,7 +40,7 @@ export const CreateNewActionTab = ({
setLocalSurvey, setLocalSurvey,
environmentId, environmentId,
}: CreateNewActionTabProps) => { }: CreateNewActionTabProps) => {
const t = useTranslations(); const { t } = useTranslate();
const actionClassNames = useMemo( const actionClassNames = useMemo(
() => actionClasses.map((actionClass) => actionClass.name), () => actionClasses.map((actionClass) => actionClass.name),
[actionClasses] [actionClasses]

View File

@@ -1,10 +1,12 @@
"use client";
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput"; import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Label } from "@/modules/ui/components/label"; import { Label } from "@/modules/ui/components/label";
import { OptionsSwitch } from "@/modules/ui/components/options-switch"; import { OptionsSwitch } from "@/modules/ui/components/options-switch";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react"; import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils"; import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys/types"; import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys/types";
@@ -48,7 +50,7 @@ export const DateQuestionForm = ({
locale, locale,
}: IDateQuestionFormProps): JSX.Element => { }: IDateQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages); const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const t = useTranslations(); const { t } = useTranslate();
const [parent] = useAutoAnimate(); const [parent] = useAutoAnimate();
return ( return (
<form> <form>

View File

@@ -14,8 +14,8 @@ import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities"; import { CSS } from "@dnd-kit/utilities";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { GripIcon, Handshake, Undo2 } from "lucide-react"; import { GripIcon, Handshake, Undo2 } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { cn } from "@formbricks/lib/cn"; import { cn } from "@formbricks/lib/cn";
@@ -59,7 +59,7 @@ export const EditEndingCard = ({
locale, locale,
}: EditEndingCardProps) => { }: EditEndingCardProps) => {
const endingCard = localSurvey.endings[endingCardIndex]; const endingCard = localSurvey.endings[endingCardIndex];
const t = useTranslations(); const { t } = useTranslate();
const isRedirectToUrlDisabled = isFormbricksCloud const isRedirectToUrlDisabled = isFormbricksCloud
? plan === "free" && endingCard.type !== "redirectToUrl" ? plan === "free" && endingCard.type !== "redirectToUrl"
: false; : false;
@@ -231,7 +231,6 @@ export const EditEndingCard = ({
updateCard={() => {}} updateCard={() => {}}
addCard={addEndingCard} addCard={addEndingCard}
cardType="ending" cardType="ending"
locale={locale}
/> />
</div> </div>
</div> </div>

View File

@@ -6,8 +6,8 @@ import { FileInput } from "@/modules/ui/components/file-input";
import { Label } from "@/modules/ui/components/label"; import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch"; import { Switch } from "@/modules/ui/components/switch";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { Hand } from "lucide-react"; import { Hand } from "lucide-react";
import { useTranslations } from "next-intl";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { cn } from "@formbricks/lib/cn"; import { cn } from "@formbricks/lib/cn";
@@ -35,7 +35,7 @@ export const EditWelcomeCard = ({
setSelectedLanguageCode, setSelectedLanguageCode,
locale, locale,
}: EditWelcomeCardProps) => { }: EditWelcomeCardProps) => {
const t = useTranslations(); const { t } = useTranslate();
const [firstRender, setFirstRender] = useState(true); const [firstRender, setFirstRender] = useState(true);
const path = usePathname(); const path = usePathname();
const environmentId = path?.split("/environments/")[1]?.split("/")[0]; const environmentId = path?.split("/environments/")[1]?.split("/")[0];

View File

@@ -13,13 +13,13 @@ import {
} from "@/modules/ui/components/dropdown-menu"; } from "@/modules/ui/components/dropdown-menu";
import { TooltipRenderer } from "@/modules/ui/components/tooltip"; import { TooltipRenderer } from "@/modules/ui/components/tooltip";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import { useTranslate } from "@tolgee/react";
import { ArrowDownIcon, ArrowUpIcon, CopyIcon, EllipsisIcon, TrashIcon } from "lucide-react"; import { ArrowDownIcon, ArrowUpIcon, CopyIcon, EllipsisIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import { import {
QUESTIONS_ICON_MAP,
getCXQuestionNameMap, getCXQuestionNameMap,
getQuestionDefaults, getQuestionDefaults,
getQuestionIconMap,
getQuestionNameMap, getQuestionNameMap,
} from "@formbricks/lib/utils/questions"; } from "@formbricks/lib/utils/questions";
import { TProject } from "@formbricks/types/project"; import { TProject } from "@formbricks/types/project";
@@ -44,7 +44,6 @@ interface EditorCardMenuProps {
cardType: "question" | "ending"; cardType: "question" | "ending";
project?: TProject; project?: TProject;
isCxMode?: boolean; isCxMode?: boolean;
locale: string;
} }
export const EditorCardMenu = ({ export const EditorCardMenu = ({
@@ -60,9 +59,9 @@ export const EditorCardMenu = ({
addCard, addCard,
cardType, cardType,
isCxMode = false, isCxMode = false,
locale,
}: EditorCardMenuProps) => { }: EditorCardMenuProps) => {
const t = useTranslations(); const { t } = useTranslate();
const QUESTIONS_ICON_MAP = getQuestionIconMap(t);
const [logicWarningModal, setLogicWarningModal] = useState(false); const [logicWarningModal, setLogicWarningModal] = useState(false);
const [changeToType, setChangeToType] = useState(() => { const [changeToType, setChangeToType] = useState(() => {
if (card.type !== "endScreen" && card.type !== "redirectToUrl") { if (card.type !== "endScreen" && card.type !== "redirectToUrl") {
@@ -76,7 +75,7 @@ export const EditorCardMenu = ({
? survey.questions.length === 1 ? survey.questions.length === 1
: survey.type === "link" && survey.endings.length === 1; : survey.type === "link" && survey.endings.length === 1;
const availableQuestionTypes = isCxMode ? getCXQuestionNameMap(locale) : getQuestionNameMap(locale); const availableQuestionTypes = isCxMode ? getCXQuestionNameMap(t) : getQuestionNameMap(t);
const changeQuestionType = (type?: TSurveyQuestionTypeEnum) => { const changeQuestionType = (type?: TSurveyQuestionTypeEnum) => {
if (!type) return; if (!type) return;
@@ -84,7 +83,7 @@ export const EditorCardMenu = ({
const { headline, required, subheader, imageUrl, videoUrl, buttonLabel, backButtonLabel } = const { headline, required, subheader, imageUrl, videoUrl, buttonLabel, backButtonLabel } =
card as TSurveyQuestion; card as TSurveyQuestion;
const questionDefaults = getQuestionDefaults(type, project, locale); const questionDefaults = getQuestionDefaults(type, project, t);
if ( if (
(type === TSurveyQuestionTypeEnum.MultipleChoiceSingle && (type === TSurveyQuestionTypeEnum.MultipleChoiceSingle &&
@@ -123,7 +122,7 @@ export const EditorCardMenu = ({
}; };
const addQuestionCardBelow = (type: TSurveyQuestionTypeEnum) => { const addQuestionCardBelow = (type: TSurveyQuestionTypeEnum) => {
const questionDefaults = getQuestionDefaults(type, project, locale); const questionDefaults = getQuestionDefaults(type, project, t);
addCard( addCard(
{ {

View File

@@ -5,7 +5,7 @@ import { RecallWrapper } from "@/modules/surveys/components/QuestionFormInput/co
import { Input } from "@/modules/ui/components/input"; import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label"; import { Label } from "@/modules/ui/components/label";
import { Switch } from "@/modules/ui/components/switch"; import { Switch } from "@/modules/ui/components/switch";
import { useTranslations } from "next-intl"; import { useTranslate } from "@tolgee/react";
import { useState } from "react"; import { useState } from "react";
import { useRef } from "react"; import { useRef } from "react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
@@ -34,7 +34,7 @@ export const EndScreenForm = ({
endingCard, endingCard,
locale, locale,
}: EndScreenFormProps) => { }: EndScreenFormProps) => {
const t = useTranslations(); const { t } = useTranslate();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [showEndingCardCTA, setshowEndingCardCTA] = useState<boolean>( const [showEndingCardCTA, setshowEndingCardCTA] = useState<boolean>(
endingCard.type === "endScreen" && endingCard.type === "endScreen" &&

View File

@@ -6,8 +6,8 @@ import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input"; import { Input } from "@/modules/ui/components/input";
import { useGetBillingInfo } from "@/modules/utils/hooks/useGetBillingInfo"; import { useGetBillingInfo } from "@/modules/utils/hooks/useGetBillingInfo";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslate } from "@tolgee/react";
import { PlusIcon, XCircleIcon } from "lucide-react"; import { PlusIcon, XCircleIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { type JSX, useMemo, useState } from "react"; import { type JSX, useMemo, useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
@@ -45,7 +45,7 @@ export const FileUploadQuestionForm = ({
locale, locale,
}: FileUploadFormProps): JSX.Element => { }: FileUploadFormProps): JSX.Element => {
const [extension, setExtension] = useState(""); const [extension, setExtension] = useState("");
const t = useTranslations(); const { t } = useTranslate();
const [isMaxSizeError, setMaxSizeError] = useState(false); const [isMaxSizeError, setMaxSizeError] = useState(false);
const { const {
billingInfo, billingInfo,
@@ -227,7 +227,7 @@ export const FileUploadQuestionForm = ({
className="underline" className="underline"
target="_blank" target="_blank"
href={`/environments/${localSurvey.environmentId}/settings/billing`}> href={`/environments/${localSurvey.environmentId}/settings/billing`}>
{t("environments.surveys.edit.upgrade_your_plan")} {t("common.please_upgrade_your_plan")}
</Link> </Link>
</p> </p>
)} )}

View File

@@ -5,8 +5,8 @@ import { ColorPicker } from "@/modules/ui/components/color-picker";
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@/modules/ui/components/form"; import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { CheckIcon, SparklesIcon } from "lucide-react"; import { CheckIcon, SparklesIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import React from "react"; import React from "react";
import { UseFormReturn } from "react-hook-form"; import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn"; import { cn } from "@formbricks/lib/cn";
@@ -30,7 +30,7 @@ export const FormStylingSettings = ({
setOpen, setOpen,
form, form,
}: FormStylingSettingsProps) => { }: FormStylingSettingsProps) => {
const t = useTranslations(); const { t } = useTranslate();
const brandColor = form.watch("brandColor.light") || COLOR_DEFAULTS.brandColor; const brandColor = form.watch("brandColor.light") || COLOR_DEFAULTS.brandColor;
const background = form.watch("background"); const background = form.watch("background");
const highlightBorderColor = form.watch("highlightBorderColor"); const highlightBorderColor = form.watch("highlightBorderColor");

View File

@@ -8,8 +8,8 @@ import { Switch } from "@/modules/ui/components/switch";
import { Tag } from "@/modules/ui/components/tag"; import { Tag } from "@/modules/ui/components/tag";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { EyeOff } from "lucide-react"; import { EyeOff } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { cn } from "@formbricks/lib/cn"; import { cn } from "@formbricks/lib/cn";
@@ -32,7 +32,7 @@ export const HiddenFieldsCard = ({
}: HiddenFieldsCardProps) => { }: HiddenFieldsCardProps) => {
const open = activeQuestionId == "hidden"; const open = activeQuestionId == "hidden";
const [hiddenField, setHiddenField] = useState<string>(""); const [hiddenField, setHiddenField] = useState<string>("");
const t = useTranslations(); const { t } = useTranslate();
const setOpen = (open: boolean) => { const setOpen = (open: boolean) => {
if (open) { if (open) {
setActiveQuestionId("hidden"); setActiveQuestionId("hidden");

View File

@@ -1,16 +1,16 @@
"use client"; "use client";
import { getDefaultEndingCard } from "@/app/lib/templates";
import { Badge } from "@/modules/ui/components/badge"; import { Badge } from "@/modules/ui/components/badge";
import { Label } from "@/modules/ui/components/label"; import { Label } from "@/modules/ui/components/label";
import { RadioGroup, RadioGroupItem } from "@/modules/ui/components/radio-group"; import { RadioGroup, RadioGroupItem } from "@/modules/ui/components/radio-group";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { AlertCircleIcon, CheckIcon, LinkIcon, MonitorIcon } from "lucide-react"; import { AlertCircleIcon, CheckIcon, LinkIcon, MonitorIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn"; import { cn } from "@formbricks/lib/cn";
import { getDefaultEndingCard } from "@formbricks/lib/templates";
import { TEnvironment } from "@formbricks/types/environment"; import { TEnvironment } from "@formbricks/types/environment";
import { TSegment } from "@formbricks/types/segment"; import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyType } from "@formbricks/types/surveys/types"; import { TSurvey, TSurveyType } from "@formbricks/types/surveys/types";
@@ -19,13 +19,12 @@ interface HowToSendCardProps {
localSurvey: TSurvey; localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey | ((TSurvey: TSurvey) => TSurvey)) => void; setLocalSurvey: (survey: TSurvey | ((TSurvey: TSurvey) => TSurvey)) => void;
environment: TEnvironment; environment: TEnvironment;
locale: string;
} }
export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, locale }: HowToSendCardProps) => { export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowToSendCardProps) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [appSetupCompleted, setAppSetupCompleted] = useState(false); const [appSetupCompleted, setAppSetupCompleted] = useState(false);
const t = useTranslations(); const { t } = useTranslate();
useEffect(() => { useEffect(() => {
if (environment) { if (environment) {
setAppSetupCompleted(environment.appSetupCompleted); setAppSetupCompleted(environment.appSetupCompleted);
@@ -35,7 +34,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, locale
const setSurveyType = (type: TSurveyType) => { const setSurveyType = (type: TSurveyType) => {
const endingsTemp = localSurvey.endings; const endingsTemp = localSurvey.endings;
if (type === "link" && localSurvey.endings.length === 0) { if (type === "link" && localSurvey.endings.length === 0) {
endingsTemp.push(getDefaultEndingCard(localSurvey.languages, locale)); endingsTemp.push(getDefaultEndingCard(localSurvey.languages, t));
} }
setLocalSurvey((prevSurvey) => ({ setLocalSurvey((prevSurvey) => ({
...prevSurvey, ...prevSurvey,

View File

@@ -1,3 +1,5 @@
"use client";
import { LogicEditorActions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorActions"; 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 { LogicEditorConditions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions";
import { import {
@@ -7,11 +9,11 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/modules/ui/components/select"; } from "@/modules/ui/components/select";
import { useTranslate } from "@tolgee/react";
import { ArrowRightIcon } from "lucide-react"; import { ArrowRightIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { ReactElement, useMemo } from "react"; import { ReactElement, useMemo } from "react";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils"; import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { QUESTIONS_ICON_MAP } from "@formbricks/lib/utils/questions"; import { getQuestionIconMap } from "@formbricks/lib/utils/questions";
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types"; import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
interface LogicEditorProps { interface LogicEditorProps {
@@ -33,8 +35,8 @@ export function LogicEditor({
logicIdx, logicIdx,
isLast, isLast,
}: LogicEditorProps) { }: LogicEditorProps) {
const t = useTranslations(); const { t } = useTranslate();
const QUESTIONS_ICON_MAP = getQuestionIconMap(t);
const fallbackOptions = useMemo(() => { const fallbackOptions = useMemo(() => {
let options: { let options: {
icon?: ReactElement; icon?: ReactElement;

View File

@@ -1,3 +1,5 @@
"use client";
import { import {
getActionObjectiveOptions, getActionObjectiveOptions,
getActionOperatorOptions, getActionOperatorOptions,
@@ -14,8 +16,8 @@ import {
} from "@/modules/ui/components/dropdown-menu"; } from "@/modules/ui/components/dropdown-menu";
import { InputCombobox } from "@/modules/ui/components/input-combo-box"; import { InputCombobox } from "@/modules/ui/components/input-combo-box";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import { useTranslate } from "@tolgee/react";
import { CopyIcon, CornerDownRightIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon } from "lucide-react"; import { CopyIcon, CornerDownRightIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { getUpdatedActionBody } from "@formbricks/lib/surveyLogic/utils"; import { getUpdatedActionBody } from "@formbricks/lib/surveyLogic/utils";
import { import {
TActionNumberVariableCalculateOperator, TActionNumberVariableCalculateOperator,
@@ -46,7 +48,7 @@ export function LogicEditorActions({
questionIdx, questionIdx,
}: LogicEditorActions) { }: LogicEditorActions) {
const actions = logicItem.actions; const actions = logicItem.actions;
const t = useTranslations(); const { t } = useTranslate();
const handleActionsChange = ( const handleActionsChange = (
operation: "remove" | "addBelow" | "duplicate" | "update", operation: "remove" | "addBelow" | "duplicate" | "update",
actionIdx: number, actionIdx: number,

View File

@@ -1,3 +1,5 @@
"use client";
import { import {
getConditionOperatorOptions, getConditionOperatorOptions,
getConditionValueOptions, getConditionValueOptions,
@@ -13,8 +15,8 @@ import {
import { InputCombobox, TComboboxOption } from "@/modules/ui/components/input-combo-box"; import { InputCombobox, TComboboxOption } from "@/modules/ui/components/input-combo-box";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import { useTranslate } from "@tolgee/react";
import { CopyIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon, WorkflowIcon } from "lucide-react"; import { CopyIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon, WorkflowIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { cn } from "@formbricks/lib/cn"; import { cn } from "@formbricks/lib/cn";
import { import {
addConditionBelow, addConditionBelow,
@@ -54,7 +56,7 @@ export function LogicEditorConditions({
updateQuestion, updateQuestion,
depth = 0, depth = 0,
}: LogicEditorConditionsProps) { }: LogicEditorConditionsProps) {
const t = useTranslations(); const { t } = useTranslate();
const [parent] = useAutoAnimate(); const [parent] = useAutoAnimate();
const handleAddConditionBelow = (resourceId: string) => { const handleAddConditionBelow = (resourceId: string) => {

View File

@@ -6,8 +6,8 @@ import { Label } from "@/modules/ui/components/label";
import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select"; import { ShuffleOptionSelect } from "@/modules/ui/components/shuffle-option-select";
import { TooltipRenderer } from "@/modules/ui/components/tooltip"; import { TooltipRenderer } from "@/modules/ui/components/tooltip";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslate } from "@tolgee/react";
import { PlusIcon, TrashIcon } from "lucide-react"; import { PlusIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react"; import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils"; import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types"; import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";
@@ -37,7 +37,7 @@ export const MatrixQuestionForm = ({
locale, locale,
}: MatrixQuestionFormProps): JSX.Element => { }: MatrixQuestionFormProps): JSX.Element => {
const languageCodes = extractLanguageCodes(localSurvey.languages); const languageCodes = extractLanguageCodes(localSurvey.languages);
const t = useTranslations(); const { t } = useTranslate();
// Function to add a new Label input field // Function to add a new Label input field
const handleAddLabel = (type: "row" | "column") => { const handleAddLabel = (type: "row" | "column") => {
if (type === "row") { if (type === "row") {

View File

@@ -9,8 +9,8 @@ import { DndContext } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable"; import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2"; import { createId } from "@paralleldrive/cuid2";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { type JSX, useEffect, useRef, useState } from "react"; import { type JSX, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils"; import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
@@ -46,7 +46,7 @@ export const MultipleChoiceQuestionForm = ({
setSelectedLanguageCode, setSelectedLanguageCode,
locale, locale,
}: MultipleChoiceQuestionFormProps): JSX.Element => { }: MultipleChoiceQuestionFormProps): JSX.Element => {
const t = useTranslations(); const { t } = useTranslate();
const lastChoiceRef = useRef<HTMLInputElement>(null); const lastChoiceRef = useRef<HTMLInputElement>(null);
const [isNew, setIsNew] = useState(true); const [isNew, setIsNew] = useState(true);
const [isInvalidValue, setisInvalidValue] = useState<string | null>(null); const [isInvalidValue, setisInvalidValue] = useState<string | null>(null);

View File

@@ -4,8 +4,8 @@ import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInpu
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle"; import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react"; import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react"; import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils"; import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys/types"; import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys/types";
@@ -34,7 +34,7 @@ export const NPSQuestionForm = ({
setSelectedLanguageCode, setSelectedLanguageCode,
locale, locale,
}: NPSQuestionFormProps): JSX.Element => { }: NPSQuestionFormProps): JSX.Element => {
const t = useTranslations(); const { t } = useTranslate();
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages); const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
// Auto animate // Auto animate
const [parent] = useAutoAnimate(); const [parent] = useAutoAnimate();

View File

@@ -7,8 +7,8 @@ import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label"; import { Label } from "@/modules/ui/components/label";
import { OptionsSwitch } from "@/modules/ui/components/options-switch"; import { OptionsSwitch } from "@/modules/ui/components/options-switch";
import { useAutoAnimate } from "@formkit/auto-animate/react"; import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslate } from "@tolgee/react";
import { HashIcon, LinkIcon, MailIcon, MessageSquareTextIcon, PhoneIcon, PlusIcon } from "lucide-react"; import { HashIcon, LinkIcon, MailIcon, MessageSquareTextIcon, PhoneIcon, PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { JSX, useEffect, useState } from "react"; import { JSX, useEffect, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils"; import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { import {
@@ -40,7 +40,7 @@ export const OpenQuestionForm = ({
setSelectedLanguageCode, setSelectedLanguageCode,
locale, locale,
}: OpenQuestionFormProps): JSX.Element => { }: OpenQuestionFormProps): JSX.Element => {
const t = useTranslations(); const { t } = useTranslate();
const questionTypes = [ const questionTypes = [
{ value: "text", label: t("common.text"), icon: <MessageSquareTextIcon className="h-4 w-4" /> }, { value: "text", label: t("common.text"), icon: <MessageSquareTextIcon className="h-4 w-4" /> },
{ value: "email", label: t("common.email"), icon: <MailIcon className="h-4 w-4" /> }, { value: "email", label: t("common.email"), icon: <MailIcon className="h-4 w-4" /> },

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