Compare commits
15 Commits
v3.1.4
...
fix/enviro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f803022d7c | ||
|
|
22e8a137ef | ||
|
|
a9fe05d64a | ||
|
|
f476693f0d | ||
|
|
5219065b8e | ||
|
|
cb8497229d | ||
|
|
25b8920d20 | ||
|
|
9203db88ab | ||
|
|
36378e9c23 | ||
|
|
9c33e77755 | ||
|
|
88cb4c742f | ||
|
|
475cce8253 | ||
|
|
a86c1738d1 | ||
|
|
96a4d02c80 | ||
|
|
bb6df783ab |
39
.github/workflows/tolgee-missing-key-check.yml
vendored
Normal 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
@@ -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
@@ -58,4 +58,5 @@ packages/lib/uploads
|
||||
# js compiled assets
|
||||
apps/web/public/js
|
||||
|
||||
|
||||
packages/database/migrations
|
||||
1
.husky/post-checkout
Normal file
@@ -0,0 +1 @@
|
||||
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ../branch.json
|
||||
1
.husky/post-commit
Normal file
@@ -0,0 +1 @@
|
||||
echo "{\"branchName\": \"$(git rev-parse --abbrev-ref HEAD)\"}" > ../branch.json
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -18,6 +18,7 @@
|
||||
},
|
||||
"jsEngine": "hermes",
|
||||
"name": "react-native-demo",
|
||||
"newArchEnabled": true,
|
||||
"orientation": "portrait",
|
||||
"slug": "react-native-demo",
|
||||
"splash": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 154 KiB |
155
apps/docs/app/developer-docs/integrations/activepieces/page.mdx
Normal 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"
|
||||
/>
|
||||
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
@@ -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>
|
||||
|
||||
158
apps/docs/app/self-hosting/kubernetes/page.mdx
Normal 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`
|
||||
@@ -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],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,4 +7,3 @@ export function LoadingSpinner(props: React.ComponentPropsWithoutRef<"div">): Re
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -22,4 +22,3 @@ export default function SurveyEmbed({ surveyUrl }: SurveyEmbedProps): React.JSX.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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`);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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" &&
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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" /> },
|
||||
|
||||