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
apps/web/public/js
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_FORMBRICKS_ENVIRONMENT_ID=clzr04nkd000bcdl110j0ijyq
EXPO_PUBLIC_APP_URL=http://192.168.0.197:3000
EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=cm5p0cs7r000819182b32j0a1

View File

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

View File

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

View File

@@ -1,7 +1,14 @@
import { StatusBar } from "expo-status-bar";
import React, { type JSX } from "react";
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();
@@ -10,35 +17,92 @@ export default function App(): JSX.Element {
throw new Error("EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID is required");
}
if (!process.env.EXPO_PUBLIC_API_HOST) {
throw new Error("EXPO_PUBLIC_API_HOST is required");
if (!process.env.EXPO_PUBLIC_APP_URL) {
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 (
<View style={styles.container}>
<Text>Formbricks React Native SDK Demo</Text>
<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);
});
}}
/>
<View
style={{
display: "flex",
flexDirection: "column",
gap: 10,
}}>
<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" />
<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 File

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

View File

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

View File

@@ -357,17 +357,11 @@ Now, update your App.js/App.tsx file to initialize Formbricks:
// other imports
import Formbricks from "@formbricks/react-native";
const config = {
environmentId: "<environment-id>",
apiHost: "<api-host>",
userId: "<user-id>", // optional
};
export default function App() {
return (
<>
{/* 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">
Formbricks Environment ID.
</Property>
<Property name="api-host" type="string">
<Property name="app-url" type="string">
URL of the hosted Formbricks instance.
</Property>
</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 }) {
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) => [
`/${filename.replace(/(?:^|\/)page\.mdx$/, "")}`,
(await import(`./${filename}`) as { sections: Section[] }).sections,
((await import(`./${filename}`)) as { sections: Section[] }).sections,
])
));
);
const allSections = Object.fromEntries(allSectionsEntries);
return (
<html lang="en" className="h-full" suppressHydrationWarning>
<head>
{process.env.NEXT_PUBLIC_LAYER_API_KEY ? <Script
strategy="afterInteractive"
src="https://storage.googleapis.com/generic-assets/buildwithlayer-widget-4.js"
primary-color="#00C4B8"
api-key={process.env.NEXT_PUBLIC_LAYER_API_KEY}
walkthrough-enabled="false"
design-style="copilot"
/> : null}
{process.env.NEXT_PUBLIC_LAYER_API_KEY ? (
<Script
strategy="afterInteractive"
src="https://storage.googleapis.com/generic-assets/buildwithlayer-widget-4.js"
primary-color="#00C4B8"
api-key={process.env.NEXT_PUBLIC_LAYER_API_KEY}
walkthrough-enabled="false"
design-style="copilot"
/>
) : null}
</head>
<body className={`flex min-h-full bg-white antialiased dark:bg-zinc-900 ${jost.className}`}>
<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;
arrow?: "left" | "right";
} & (
| React.ComponentPropsWithoutRef<typeof Link>
| (React.ComponentPropsWithoutRef<"button"> & { href?: undefined })
);
| React.ComponentPropsWithoutRef<typeof Link>
| (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(
"inline-flex gap-0.5 justify-center items-center overflow-hidden font-medium transition text-center",
variantStyles[variant],

View File

@@ -1,10 +1,10 @@
"use client";
import { Tag } from "@/components/tag";
import { Tab } from "@headlessui/react";
import clsx from "clsx";
import { Children, createContext, isValidElement, useContext, useEffect, useRef, useState } from "react";
import { create } from "zustand";
import { Tag } from "@/components/tag";
const languageNames: Record<string, string> = {
js: "JavaScript",
@@ -49,7 +49,9 @@ function CopyButton({ code }: { code: string }) {
useEffect(() => {
if (copyCount > 0) {
const timeout = setTimeout(() => { setCopyCount(0); }, 1000);
const timeout = setTimeout(() => {
setCopyCount(0);
}, 1000);
return () => {
clearTimeout(timeout);
};
@@ -98,9 +100,11 @@ function CodePanelHeader({ tag, label }: { tag?: string; label?: string }): Reac
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">
{tag ? <div className="dark flex">
<Tag variant="small">{tag}</Tag>
</div> : null}
{tag ? (
<div className="dark flex">
<Tag variant="small">{tag}</Tag>
</div>
) : 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}
</div>
@@ -162,30 +166,34 @@ function CodeGroupHeader({
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">
{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">
{Children.map(children, (child, childIndex) => {
if (isValidElement(child)) {
return (
<Tab
className={clsx(
"ui-not-focus-visible:outline-none border-b py-3 transition",
childIndex === selectedIndex
? "border-teal-500 text-teal-400"
: "border-transparent text-slate-400 hover:text-slate-300"
)}
>
{getPanelTitle(child.props as { title?: string; language?: string })}
</Tab>
);
}
return null;
})}
</Tab.List> : null}
{hasTabs ? (
<Tab.List className="-mb-px flex gap-4 text-xs font-medium">
{Children.map(children, (child, childIndex) => {
if (isValidElement(child)) {
return (
<Tab
className={clsx(
"ui-not-focus-visible:outline-none border-b py-3 transition",
childIndex === selectedIndex
? "border-teal-500 text-teal-400"
: "border-transparent text-slate-400 hover:text-slate-300"
)}>
{getPanelTitle(child.props as { title?: string; language?: string })}
</Tab>
);
}
return null;
})}
</Tab.List>
) : null}
</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;
if (hasTabs) {
@@ -264,7 +272,9 @@ const useTabGroupProps = (availableLanguages: string[]) => {
const { positionRef, preventLayoutShift } = usePreventLayoutShift();
const onChange = (index: number) => {
preventLayoutShift(() => { addPreferredLanguage(availableLanguages[index] ?? ""); });
preventLayoutShift(() => {
addPreferredLanguage(availableLanguages[index] ?? "");
});
};
return {
@@ -331,7 +341,10 @@ export function Code({ children, ...props }: React.ComponentPropsWithoutRef<"cod
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);
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 (
<button
type="submit"
@@ -49,16 +51,18 @@ const FeedbackForm = forwardRef<
FeedbackForm.displayName = "FeedbackForm";
const FeedbackThanks = forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>((_props, ref): React.JSX.Element => {
return (
<div ref={ref} className="absolute inset-0 flex justify-center md:justify-start">
<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">
<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!
const FeedbackThanks = forwardRef<React.ElementRef<"div">, React.ComponentPropsWithoutRef<"div">>(
(_props, ref): React.JSX.Element => {
return (
<div ref={ref} className="absolute inset-0 flex justify-center md:justify-start">
<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">
<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>
);
});
);
}
);
FeedbackThanks.displayName = "FeedbackThanks";

View File

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

View File

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

View File

@@ -1,16 +1,16 @@
"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 { type MotionStyle, motion, useScroll, useTransform } from "framer-motion";
import Link from "next/link";
import { forwardRef } from "react";
import { Search } from "@/components/search";
import { Logo } from "@/components/logo";
import { Button } from "./button";
import { MobileNavigation } from "./mobile-navigation";
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 {
return (

View File

@@ -1,11 +1,11 @@
"use client";
import { useInView } from "framer-motion";
import Link from "next/link";
import { useEffect, useRef } from "react";
import { useSectionStore } from "@/components/section-provider";
import { Tag } from "@/components/tag";
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 {
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 (
<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)]">
<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" />
{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)]">
<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> : null}
) : null}
{children}
</Link>
);
@@ -67,7 +77,7 @@ export function Heading<Level extends 2 | 3 | 4>({
const ref = useRef<HTMLHeadingElement>(null);
const registerHeading = useSectionStore((s) => s.registerHeading);
const topMargin = remToPx(-3.5)
const topMargin = remToPx(-3.5);
const inView = useInView(ref, {
margin: `${topMargin}px 0px 0px 0px`,
amount: "all",
@@ -75,18 +85,18 @@ export function Heading<Level extends 2 | 3 | 4>({
useEffect(() => {
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) {
registerHeading({ id: props.id, ref, offsetRem: tag ?? label ? 7 : 5 });
registerHeading({ id: props.id, ref, offsetRem: (tag ?? label) ? 7 : 5 });
} 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]);
return (
<>
<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 id={props.id} inView={inView}>
{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" />
</svg>
);
};
}

View File

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

View File

@@ -1,11 +1,11 @@
"use client";
import { motion } from "framer-motion";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Logo } from "@/components/logo";
import { Navigation } from "@/components/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 { Header } from "./header";
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 logoLight from "@/images/logo/logo-light.svg";
import Image from "next/image";
export function Logo({ className }: { className?: string }) {
return (
<div>
<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 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>
);

View File

@@ -57,7 +57,7 @@ function MobileNavigationDialog({
if (
link &&
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();
}

View File

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

View File

@@ -1,13 +1,13 @@
"use client";
import { type MotionValue, motion, useMotionTemplate, useMotionValue } from "framer-motion";
import Link from "next/link";
import { GridPattern } from "@/components/grid-pattern";
import { Heading } from "@/components/heading";
import { ChatBubbleIcon } from "@/components/icons/chat-bubble-icon";
import { EnvelopeIcon } from "@/components/icons/envelope-icon";
import { UserIcon } from "@/components/icons/user-icon";
import { UsersIcon } from "@/components/icons/users-icon";
import { type MotionValue, motion, useMotionTemplate, useMotionValue } from "framer-motion";
import Link from "next/link";
interface TResource {
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"
referrerPolicy="strict-origin-when-cross-origin"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen />
allowFullScreen
/>
</div>
</div>
);

View File

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

View File

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

View File

@@ -48,7 +48,7 @@ export function SideNavigation({ pathname }: { pathname: string }): React.JSX.El
return (
<li
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-4": heading.level === 3,
"ml-6": heading.level === 4,

View File

@@ -22,4 +22,3 @@ export default function SurveyEmbed({ surveyUrl }: SurveyEmbedProps): React.JSX.
</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`}
allowFullScreen
title="Tella Video Help" />
title="Tella Video Help"
/>
</div>
);
}

View File

@@ -35,7 +35,9 @@ export function ThemeToggle(): React.JSX.Element {
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"
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" />
<MoonIcon className="hidden h-5 w-5 stroke-white dark:block" />
</button>

View File

@@ -14,7 +14,13 @@ export const useMobileNavigationStore = create<{
toggle: () => void;
}>()((set) => ({
isOpen: false,
open: () => { set({ isOpen: true }); },
close: () => { set({ isOpen: false }); },
toggle: () => { set((state) => ({ isOpen: !state.isOpen })); },
open: () => {
set({ isOpen: true });
},
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(() => {
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
headingElementsRef.current = headings.reduce(
(map, headingElement) => {
return { ...map, [headingElement.target.id]: headingElement };
},
{}
);
headingElementsRef.current = headings.reduce((map, headingElement) => {
return { ...map, [headingElement.target.id]: headingElement };
}, {});
// Find the visible headings (i.e., headings that are currently intersecting with the viewport)
const visibleHeadings: HeadingElement[] = [];

View File

@@ -120,6 +120,7 @@ export const navigation: NavGroup[] = [
{ title: "Airtable", href: "/developer-docs/integrations/airtable" },
{ title: "Google Sheets", href: "/developer-docs/integrations/google-sheets" },
{ title: "Make", href: "/developer-docs/integrations/make" },
{ title: "Activepieces", href: "/developer-docs/integrations/activepieces" },
{ title: "n8n", href: "/developer-docs/integrations/n8n" },
{ title: "Notion", href: "/developer-docs/integrations/notion" },
{ title: "Slack", href: "/developer-docs/integrations/slack" },
@@ -145,6 +146,7 @@ export const navigation: NavGroup[] = [
{ title: "License", href: "/self-hosting/license" },
{ title: "Cluster Setup", href: "/self-hosting/cluster-setup" },
{ 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
@@ -18,7 +18,8 @@ FROM node:22-alpine AS base
FROM base AS installer
# 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
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
#
FROM base AS runner
RUN npm install -g corepack@latest
RUN corepack enable
RUN apk add --no-cache curl \

View File

@@ -1,8 +1,8 @@
"use client";
import { Button } from "@/modules/ui/components/button";
import { useTranslate } from "@tolgee/react";
import { ArrowRight } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { cn } from "@formbricks/lib/cn";
@@ -23,7 +23,7 @@ export const ConnectWithFormbricks = ({
widgetSetupCompleted,
channel,
}: ConnectWithFormbricksProps) => {
const t = useTranslations();
const { t } = useTranslate();
const router = useRouter();
const handleFinishOnboarding = async () => {
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 { Html5Icon, NpmIcon } from "@/modules/ui/components/icons";
import { TabBar } from "@/modules/ui/components/tab-bar";
import { useTranslations } from "next-intl";
import { useTranslate } from "@tolgee/react";
import Link from "next/link";
import "prismjs/themes/prism.css";
import { useState } from "react";
@@ -29,7 +29,7 @@ export const OnboardingSetupInstructions = ({
channel,
widgetSetupCompleted,
}: OnboardingSetupInstructionsProps) => {
const t = useTranslations();
const { t } = useTranslate();
const [activeTab, setActiveTab] = useState(tabs[0].id);
const htmlSnippetForAppSurveys = `<!-- START Formbricks Surveys -->
<script type="text/javascript">

View File

@@ -1,8 +1,8 @@
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
import { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { XIcon } from "lucide-react";
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
@@ -16,7 +16,7 @@ interface ConnectPageProps {
const Page = async (props: ConnectPageProps) => {
const params = await props.params;
const t = await getTranslations();
const t = await getTranslate();
const environment = await getEnvironment(params.environmentId);
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 { getFormattedErrorMessage } from "@/lib/utils/helper";
import { createSurveyAction } from "@/modules/surveys/components/TemplateList/actions";
import { useTranslate } from "@tolgee/react";
import { ActivityIcon, ShoppingCartIcon, SmileIcon, StarIcon, ThumbsUpIcon, UsersIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
@@ -23,7 +23,7 @@ interface XMTemplateListProps {
export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListProps) => {
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
const t = useTranslations();
const { t } = useTranslate();
const router = useRouter();
const createSurvey = async (activeTemplate: TXMTemplate) => {
@@ -47,7 +47,7 @@ export const XMTemplateList = ({ project, user, environmentId }: XMTemplateListP
const handleTemplateClick = (templateIdx: number) => {
setActiveTemplateId(templateIdx);
const template = getXMTemplates(user.locale)[templateIdx];
const template = getXMTemplates(t)[templateIdx];
const newTemplate = replacePresetPlaceholders(template, project);
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
export const replacePresetPlaceholders = (template: TXMTemplate, project: TProject) => {
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) => {
return replaceQuestionPresetPlaceholders(question, project);
});

View File

@@ -1,25 +1,18 @@
import { getDefaultEndingCard } from "@/app/lib/templates";
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 { 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) {
console.error(`Error in ${context}:`, error);
}
export const getXMSurveyDefault = (locale: string): TXMTemplate => {
export const getXMSurveyDefault = (t: TFnType): TXMTemplate => {
try {
if (!validateLocale(locale)) {
throw new Error("Invalid locale");
}
return {
name: "",
endings: [getDefaultEndingCard([], locale)],
endings: [getDefaultEndingCard([], t)],
questions: [],
styling: {
overwriteThemeStyling: true,
@@ -31,24 +24,24 @@ export const getXMSurveyDefault = (locale: string): TXMTemplate => {
}
};
const NPSSurvey = (locale: string): TXMTemplate => {
const npsSurvey = (t: TFnType): TXMTemplate => {
return {
...getXMSurveyDefault(locale),
name: translate("nps_survey_name", locale),
...getXMSurveyDefault(t),
name: t("templates.nps_survey_name"),
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: { default: translate("nps_survey_question_1_headline", locale) },
headline: { default: t("templates.nps_survey_question_1_headline") },
required: true,
lowerLabel: { default: translate("nps_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("nps_survey_question_1_upper_label", locale) },
lowerLabel: { default: t("templates.nps_survey_question_1_lower_label") },
upperLabel: { default: t("templates.nps_survey_question_1_upper_label") },
isColorCodingEnabled: true,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("nps_survey_question_2_headline", locale) },
headline: { default: t("templates.nps_survey_question_2_headline") },
required: false,
inputType: "text",
charLimit: {
@@ -58,7 +51,7 @@ const NPSSurvey = (locale: string): TXMTemplate => {
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("nps_survey_question_3_headline", locale) },
headline: { default: t("templates.nps_survey_question_3_headline") },
required: false,
inputType: "text",
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 defaultSurvey = getXMSurveyDefault(locale);
const defaultSurvey = getXMSurveyDefault(t);
return {
...defaultSurvey,
name: translate("star_rating_survey_name", locale),
name: t("templates.star_rating_survey_name"),
questions: [
{
id: reusableQuestionIds[0],
@@ -112,15 +105,15 @@ const StarRatingSurvey = (locale: string): TXMTemplate => {
],
range: 5,
scale: "number",
headline: { default: translate("star_rating_survey_question_1_headline", locale) },
headline: { default: t("templates.star_rating_survey_question_1_headline") },
required: true,
lowerLabel: { default: translate("star_rating_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("star_rating_survey_question_1_upper_label", locale) },
lowerLabel: { default: t("templates.star_rating_survey_question_1_lower_label") },
upperLabel: { default: t("templates.star_rating_survey_question_1_upper_label") },
isColorCodingEnabled: false,
},
{
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,
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,
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,
},
{
id: reusableQuestionIds[2],
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,
subheader: { default: "Help us improve your experience." },
buttonLabel: { default: "Send" },
placeholder: { default: "Type your answer here..." },
subheader: { default: t("templates.star_rating_survey_question_3_subheader") },
buttonLabel: { default: t("templates.star_rating_survey_question_3_button_label") },
placeholder: { default: t("templates.star_rating_survey_question_3_placeholder") },
inputType: "text",
charLimit: {
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 defaultSurvey = getXMSurveyDefault(locale);
const defaultSurvey = getXMSurveyDefault(t);
return {
...defaultSurvey,
name: translate("csat_survey_name", locale),
name: t("templates.csat_survey_name"),
questions: [
{
id: reusableQuestionIds[0],
@@ -214,10 +207,10 @@ const CSATSurvey = (locale: string): TXMTemplate => {
],
range: 5,
scale: "smiley",
headline: { default: translate("csat_survey_question_1_headline", locale) },
headline: { default: t("templates.csat_survey_question_1_headline") },
required: true,
lowerLabel: { default: translate("csat_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("csat_survey_question_1_upper_label", locale) },
lowerLabel: { default: t("templates.csat_survey_question_1_lower_label") },
upperLabel: { default: t("templates.csat_survey_question_1_upper_label") },
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,
placeholder: { default: translate("csat_survey_question_2_placeholder", locale) },
placeholder: { default: t("templates.csat_survey_question_2_placeholder") },
inputType: "text",
charLimit: {
enabled: false,
@@ -260,9 +253,9 @@ const CSATSurvey = (locale: string): TXMTemplate => {
{
id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("csat_survey_question_3_headline", locale) },
headline: { default: t("templates.csat_survey_question_3_headline") },
required: false,
placeholder: { default: translate("csat_survey_question_3_placeholder", locale) },
placeholder: { default: t("templates.csat_survey_question_3_placeholder") },
inputType: "text",
charLimit: {
enabled: false,
@@ -272,28 +265,28 @@ const CSATSurvey = (locale: string): TXMTemplate => {
};
};
const CESSurvey = (locale: string): TXMTemplate => {
const cessSurvey = (t: TFnType): TXMTemplate => {
return {
...getXMSurveyDefault(locale),
name: translate("cess_survey_name", locale),
...getXMSurveyDefault(t),
name: t("templates.cess_survey_name"),
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.Rating,
range: 5,
scale: "number",
headline: { default: translate("cess_survey_question_1_headline", locale) },
headline: { default: t("templates.cess_survey_question_1_headline") },
required: true,
lowerLabel: { default: translate("cess_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("cess_survey_question_1_upper_label", locale) },
lowerLabel: { default: t("templates.cess_survey_question_1_lower_label") },
upperLabel: { default: t("templates.cess_survey_question_1_upper_label") },
isColorCodingEnabled: false,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("cess_survey_question_2_headline", locale) },
headline: { default: t("templates.cess_survey_question_2_headline") },
required: true,
placeholder: { default: translate("cess_survey_question_2_placeholder", locale) },
placeholder: { default: t("templates.cess_survey_question_2_placeholder") },
inputType: "text",
charLimit: {
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 defaultSurvey = getXMSurveyDefault(locale);
const defaultSurvey = getXMSurveyDefault(t);
return {
...defaultSurvey,
name: translate("smileys_survey_name", locale),
name: t("templates.smileys_survey_name"),
questions: [
{
id: reusableQuestionIds[0],
@@ -346,15 +339,15 @@ const SmileysRatingSurvey = (locale: string): TXMTemplate => {
],
range: 5,
scale: "smiley",
headline: { default: translate("smileys_survey_question_1_headline", locale) },
headline: { default: t("templates.smileys_survey_question_1_headline") },
required: true,
lowerLabel: { default: translate("smileys_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("smileys_survey_question_1_upper_label", locale) },
lowerLabel: { default: t("templates.smileys_survey_question_1_lower_label") },
upperLabel: { default: t("templates.smileys_survey_question_1_upper_label") },
isColorCodingEnabled: false,
},
{
id: reusableQuestionIds[1],
html: { default: translate("smileys_survey_question_2_html", locale) },
html: { default: t("templates.smileys_survey_question_2_html") },
type: TSurveyQuestionTypeEnum.CTA,
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,
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,
},
{
id: reusableQuestionIds[2],
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("smileys_survey_question_3_headline", locale) },
headline: { default: t("templates.smileys_survey_question_3_headline") },
required: true,
subheader: { default: translate("smileys_survey_question_3_subheader", locale) },
buttonLabel: { default: translate("smileys_survey_question_3_button_label", locale) },
placeholder: { default: translate("smileys_survey_question_3_placeholder", locale) },
subheader: { default: t("templates.smileys_survey_question_3_subheader") },
buttonLabel: { default: t("templates.smileys_survey_question_3_button_label") },
placeholder: { default: t("templates.smileys_survey_question_3_placeholder") },
inputType: "text",
charLimit: {
enabled: false,
@@ -405,26 +398,26 @@ const SmileysRatingSurvey = (locale: string): TXMTemplate => {
};
};
const eNPSSurvey = (locale: string): TXMTemplate => {
const enpsSurvey = (t: TFnType): TXMTemplate => {
return {
...getXMSurveyDefault(locale),
name: translate("enps_survey_name", locale),
...getXMSurveyDefault(t),
name: t("templates.enps_survey_name"),
questions: [
{
id: createId(),
type: TSurveyQuestionTypeEnum.NPS,
headline: {
default: translate("enps_survey_question_1_headline", locale),
default: t("templates.enps_survey_question_1_headline"),
},
required: false,
lowerLabel: { default: translate("enps_survey_question_1_lower_label", locale) },
upperLabel: { default: translate("enps_survey_question_1_upper_label", locale) },
lowerLabel: { default: t("templates.enps_survey_question_1_lower_label") },
upperLabel: { default: t("templates.enps_survey_question_1_upper_label") },
isColorCodingEnabled: true,
},
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("enps_survey_question_2_headline", locale) },
headline: { default: t("templates.enps_survey_question_2_headline") },
required: false,
inputType: "text",
charLimit: {
@@ -434,7 +427,7 @@ const eNPSSurvey = (locale: string): TXMTemplate => {
{
id: createId(),
type: TSurveyQuestionTypeEnum.OpenText,
headline: { default: translate("enps_survey_question_3_headline", locale) },
headline: { default: t("templates.enps_survey_question_3_headline") },
required: false,
inputType: "text",
charLimit: {
@@ -445,18 +438,15 @@ const eNPSSurvey = (locale: string): TXMTemplate => {
};
};
export const getXMTemplates = (locale: string): TXMTemplate[] => {
export const getXMTemplates = (t: TFnType): TXMTemplate[] => {
try {
if (!validateLocale(locale)) {
throw new Error("Invalid locale");
}
return [
NPSSurvey(locale),
StarRatingSurvey(locale),
CSATSurvey(locale),
CESSurvey(locale),
SmileysRatingSurvey(locale),
eNPSSurvey(locale),
npsSurvey(t),
starRatingSurvey(t),
csatSurvey(t),
cessSurvey(t),
smileysRatingSurvey(t),
enpsSurvey(t),
];
} catch (error) {
logError(error, "getXMTemplates");

View File

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

View File

@@ -17,9 +17,9 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { useTranslate } from "@tolgee/react";
import { ArrowUpRightIcon, ChevronRightIcon, LogOutIcon, PlusIcon } from "lucide-react";
import { signOut } from "next-auth/react";
import { useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -44,7 +44,7 @@ export const LandingSidebar = ({
}: LandingSidebarProps) => {
const [openCreateOrganizationModal, setOpenCreateOrganizationModal] = useState<boolean>(false);
const t = useTranslations();
const { t } = useTranslate();
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 { getEnterpriseLicense } from "@/modules/ee/license-check/lib/utils";
import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound, redirect } from "next/navigation";
import { getOrganization, getOrganizationsByUserId } from "@formbricks/lib/organization/service";
import { getUser } from "@formbricks/lib/user/service";
const Page = async (props) => {
const params = await props.params;
const t = await getTranslations();
const t = await getTranslate();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);

View File

@@ -1,8 +1,8 @@
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { ToasterClient } from "@/modules/ui/components/toaster-client";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { redirect } from "next/navigation";
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
import { getOrganization } from "@formbricks/lib/organization/service";
@@ -14,7 +14,7 @@ const ProjectOnboardingLayout = async (props) => {
const { children } = props;
const t = await getTranslations();
const t = await getTranslate();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
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 { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { PictureInPicture2Icon, SendIcon, XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { redirect } from "next/navigation";
import { getUserProjects } from "@formbricks/lib/project/service";
@@ -22,7 +22,7 @@ const Page = async (props: ChannelPageProps) => {
return redirect(`/auth/login`);
}
const t = await getTranslations();
const t = await getTranslate();
const channelOptions = [
{
title: t("organizations.projects.new.channel.link_and_email_surveys"),

View File

@@ -1,7 +1,7 @@
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getOrganizationProjectsLimit } from "@/modules/ee/license-check/lib/utils";
import { getTranslate } from "@/tolgee/server";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import { notFound, redirect } from "next/navigation";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
@@ -12,7 +12,7 @@ const OnboardingLayout = async (props) => {
const params = await props.params;
const { children } = props;
const t = await getTranslations();
const t = await getTranslate();
const session = await getServerSession(authOptions);
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 { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { redirect } from "next/navigation";
import { getUserProjects } from "@formbricks/lib/project/service";
@@ -22,7 +22,7 @@ const Page = async (props: ModePageProps) => {
return redirect(`/auth/login`);
}
const t = await getTranslations();
const t = await getTranslate();
const channelOptions = [
{
title: t("organizations.projects.new.mode.formbricks_surveys"),

View File

@@ -1,6 +1,7 @@
"use client";
import { createProjectAction } from "@/app/(app)/environments/[environmentId]/actions";
import { previewSurvey } from "@/app/lib/templates";
import { getFormattedErrorMessage } from "@/lib/utils/helper";
import { TOrganizationTeam } from "@/modules/ee/teams/project-teams/types/team";
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 { SurveyInline } from "@/modules/ui/components/survey";
import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslations } from "next-intl";
import { useTranslate } from "@tolgee/react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
import { getPreviewSurvey } from "@formbricks/lib/styling/constants";
import {
TProjectConfigChannel,
TProjectConfigIndustry,
@@ -43,7 +43,6 @@ interface ProjectSettingsProps {
defaultBrandColor: string;
organizationTeams: TOrganizationTeam[];
canDoRoleManagement: boolean;
locale: string;
userProjectsCount: number;
}
@@ -55,13 +54,12 @@ export const ProjectSettings = ({
defaultBrandColor,
organizationTeams,
canDoRoleManagement = false,
locale,
userProjectsCount,
}: ProjectSettingsProps) => {
const [createTeamModalOpen, setCreateTeamModalOpen] = useState(false);
const router = useRouter();
const t = useTranslations();
const { t } = useTranslate();
const addProject = async (data: TProjectUpdateInput) => {
try {
const createProjectResponse = await createProjectAction({
@@ -233,7 +231,7 @@ export const ProjectSettings = ({
<p className="text-sm text-slate-400">{t("common.preview")}</p>
<div className="z-0 h-3/4 w-3/4">
<SurveyInline
survey={getPreviewSurvey(locale, projectName || "my Product")}
survey={previewSurvey(projectName || "my Product", t)}
styling={{ brandColor: { light: brandColor } }}
isBrandingEnabled={false}
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 { Button } from "@/modules/ui/components/button";
import { Header } from "@/modules/ui/components/header";
import { getTranslate } from "@/tolgee/server";
import { XIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { getTranslations } from "next-intl/server";
import Link from "next/link";
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 { getUserProjects } from "@formbricks/lib/project/service";
import { getUserLocale } from "@formbricks/lib/user/service";
import { TProjectConfigChannel, TProjectConfigIndustry, TProjectMode } from "@formbricks/types/project";
interface ProjectSettingsPageProps {
@@ -29,7 +28,7 @@ interface ProjectSettingsPageProps {
const Page = async (props: ProjectSettingsPageProps) => {
const searchParams = await props.searchParams;
const params = await props.params;
const t = await getTranslations();
const t = await getTranslate();
const session = await getServerSession(authOptions);
if (!session || !session.user) {
@@ -39,7 +38,6 @@ const Page = async (props: ProjectSettingsPageProps) => {
const channel = searchParams.channel || null;
const industry = searchParams.industry || null;
const mode = searchParams.mode || "surveys";
const locale = session?.user.id ? await getUserLocale(session.user.id) : undefined;
const projects = await getUserProjects(session.user.id, params.organizationId);
const organizationTeams = await getTeamsByOrganizationId(params.organizationId);
@@ -70,7 +68,6 @@ const Page = async (props: ProjectSettingsPageProps) => {
defaultBrandColor={DEFAULT_BRAND_COLOR}
organizationTeams={organizationTeams}
canDoRoleManagement={canDoRoleManagement}
locale={locale ?? DEFAULT_LOCALE}
userProjectsCount={projects.length}
/>
{projects.length >= 1 && (

View File

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

View File

@@ -1,7 +1,7 @@
"use client";
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 { TSurvey } from "@formbricks/types/surveys/types";
import { CreateNewActionTab } from "./CreateNewActionTab";
@@ -28,7 +28,7 @@ export const AddActionModal = ({
isReadOnly,
environmentId,
}: AddActionModalProps) => {
const t = useTranslations();
const { t } = useTranslate();
const tabs = [
{
title: t("environments.surveys.edit.select_saved_action"),

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,12 @@
"use client";
import { QuestionFormInput } from "@/modules/surveys/components/QuestionFormInput";
import { AdvancedOptionToggle } from "@/modules/ui/components/advanced-option-toggle";
import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { Label } from "@/modules/ui/components/label";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { type JSX, useEffect, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys/types";
@@ -34,7 +36,7 @@ export const CalQuestionForm = ({
}: CalQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const [isCalHostEnabled, setIsCalHostEnabled] = useState(!!question.calHost);
const t = useTranslations();
const { t } = useTranslate();
useEffect(() => {
if (!isCalHostEnabled) {
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 { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import React from "react";
import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
@@ -36,7 +36,7 @@ export const CardStylingSettings = ({
setOpen,
form,
}: CardStylingSettingsProps) => {
const t = useTranslations();
const { t } = useTranslate();
const isAppSurvey = surveyType === "app";
const surveyTypeDerived = isAppSurvey ? "App" : "Link";
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 {
getDefaultOperatorForQuestion,
@@ -13,6 +15,7 @@ import {
import { Label } from "@/modules/ui/components/label";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { useTranslate } from "@tolgee/react";
import {
ArrowDownIcon,
ArrowUpIcon,
@@ -22,7 +25,6 @@ import {
SplitIcon,
TrashIcon,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { duplicateLogicItem } from "@formbricks/lib/surveyLogic/utils";
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
@@ -41,7 +43,7 @@ export function ConditionalLogic({
questionIdx,
updateQuestion,
}: ConditionalLogicProps) {
const t = useTranslations();
const { t } = useTranslate();
const transformedSurvey = useMemo(() => {
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default");
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,8 +6,8 @@ import { Button } from "@/modules/ui/components/button";
import { Input } from "@/modules/ui/components/input";
import { useGetBillingInfo } from "@/modules/utils/hooks/useGetBillingInfo";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslate } from "@tolgee/react";
import { PlusIcon, XCircleIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { type JSX, useMemo, useState } from "react";
import { toast } from "react-hot-toast";
@@ -45,7 +45,7 @@ export const FileUploadQuestionForm = ({
locale,
}: FileUploadFormProps): JSX.Element => {
const [extension, setExtension] = useState("");
const t = useTranslations();
const { t } = useTranslate();
const [isMaxSizeError, setMaxSizeError] = useState(false);
const {
billingInfo,
@@ -227,7 +227,7 @@ export const FileUploadQuestionForm = ({
className="underline"
target="_blank"
href={`/environments/${localSurvey.environmentId}/settings/billing`}>
{t("environments.surveys.edit.upgrade_your_plan")}
{t("common.please_upgrade_your_plan")}
</Link>
</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 { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { CheckIcon, SparklesIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import React from "react";
import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
@@ -30,7 +30,7 @@ export const FormStylingSettings = ({
setOpen,
form,
}: FormStylingSettingsProps) => {
const t = useTranslations();
const { t } = useTranslate();
const brandColor = form.watch("brandColor.light") || COLOR_DEFAULTS.brandColor;
const background = form.watch("background");
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 { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { EyeOff } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
@@ -32,7 +32,7 @@ export const HiddenFieldsCard = ({
}: HiddenFieldsCardProps) => {
const open = activeQuestionId == "hidden";
const [hiddenField, setHiddenField] = useState<string>("");
const t = useTranslations();
const { t } = useTranslate();
const setOpen = (open: boolean) => {
if (open) {
setActiveQuestionId("hidden");

View File

@@ -1,16 +1,16 @@
"use client";
import { getDefaultEndingCard } from "@/app/lib/templates";
import { Badge } from "@/modules/ui/components/badge";
import { Label } from "@/modules/ui/components/label";
import { RadioGroup, RadioGroupItem } from "@/modules/ui/components/radio-group";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useTranslate } from "@tolgee/react";
import { AlertCircleIcon, CheckIcon, LinkIcon, MonitorIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { getDefaultEndingCard } from "@formbricks/lib/templates";
import { TEnvironment } from "@formbricks/types/environment";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyType } from "@formbricks/types/surveys/types";
@@ -19,13 +19,12 @@ interface HowToSendCardProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey | ((TSurvey: TSurvey) => TSurvey)) => void;
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 [appSetupCompleted, setAppSetupCompleted] = useState(false);
const t = useTranslations();
const { t } = useTranslate();
useEffect(() => {
if (environment) {
setAppSetupCompleted(environment.appSetupCompleted);
@@ -35,7 +34,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, locale
const setSurveyType = (type: TSurveyType) => {
const endingsTemp = localSurvey.endings;
if (type === "link" && localSurvey.endings.length === 0) {
endingsTemp.push(getDefaultEndingCard(localSurvey.languages, locale));
endingsTemp.push(getDefaultEndingCard(localSurvey.languages, t));
}
setLocalSurvey((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 { LogicEditorConditions } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditorConditions";
import {
@@ -7,11 +9,11 @@ import {
SelectTrigger,
SelectValue,
} from "@/modules/ui/components/select";
import { useTranslate } from "@tolgee/react";
import { ArrowRightIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { ReactElement, useMemo } from "react";
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";
interface LogicEditorProps {
@@ -33,8 +35,8 @@ export function LogicEditor({
logicIdx,
isLast,
}: LogicEditorProps) {
const t = useTranslations();
const { t } = useTranslate();
const QUESTIONS_ICON_MAP = getQuestionIconMap(t);
const fallbackOptions = useMemo(() => {
let options: {
icon?: ReactElement;

View File

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

View File

@@ -1,3 +1,5 @@
"use client";
import {
getConditionOperatorOptions,
getConditionValueOptions,
@@ -13,8 +15,8 @@ import {
import { InputCombobox, TComboboxOption } from "@/modules/ui/components/input-combo-box";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { useTranslate } from "@tolgee/react";
import { CopyIcon, EllipsisVerticalIcon, PlusIcon, TrashIcon, WorkflowIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { cn } from "@formbricks/lib/cn";
import {
addConditionBelow,
@@ -54,7 +56,7 @@ export function LogicEditorConditions({
updateQuestion,
depth = 0,
}: LogicEditorConditionsProps) {
const t = useTranslations();
const { t } = useTranslate();
const [parent] = useAutoAnimate();
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 { TooltipRenderer } from "@/modules/ui/components/tooltip";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslate } from "@tolgee/react";
import { PlusIcon, TrashIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys/types";
@@ -37,7 +37,7 @@ export const MatrixQuestionForm = ({
locale,
}: MatrixQuestionFormProps): JSX.Element => {
const languageCodes = extractLanguageCodes(localSurvey.languages);
const t = useTranslations();
const { t } = useTranslate();
// Function to add a new Label input field
const handleAddLabel = (type: "row" | "column") => {
if (type === "row") {

View File

@@ -9,8 +9,8 @@ import { DndContext } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { createId } from "@paralleldrive/cuid2";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { type JSX, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
@@ -46,7 +46,7 @@ export const MultipleChoiceQuestionForm = ({
setSelectedLanguageCode,
locale,
}: MultipleChoiceQuestionFormProps): JSX.Element => {
const t = useTranslations();
const { t } = useTranslate();
const lastChoiceRef = useRef<HTMLInputElement>(null);
const [isNew, setIsNew] = useState(true);
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 { Button } from "@/modules/ui/components/button";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useTranslate } from "@tolgee/react";
import { PlusIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import type { JSX } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TSurvey, TSurveyNPSQuestion } from "@formbricks/types/surveys/types";
@@ -34,7 +34,7 @@ export const NPSQuestionForm = ({
setSelectedLanguageCode,
locale,
}: NPSQuestionFormProps): JSX.Element => {
const t = useTranslations();
const { t } = useTranslate();
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
// Auto animate
const [parent] = useAutoAnimate();

View File

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

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