Compare commits
45 Commits
v3.1.2
...
fix/discor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
323d39580a | ||
|
|
bae9226326 | ||
|
|
1f80f8f396 | ||
|
|
cb120c56f6 | ||
|
|
d73b497f52 | ||
|
|
22e8a137ef | ||
|
|
a9fe05d64a | ||
|
|
5219065b8e | ||
|
|
cb8497229d | ||
|
|
25b8920d20 | ||
|
|
9203db88ab | ||
|
|
36378e9c23 | ||
|
|
9c33e77755 | ||
|
|
88cb4c742f | ||
|
|
475cce8253 | ||
|
|
a86c1738d1 | ||
|
|
96a4d02c80 | ||
|
|
bb4f8f1df3 | ||
|
|
bb6df783ab | ||
|
|
26cca5c2f8 | ||
|
|
7e3dd7d624 | ||
|
|
db9a53f923 | ||
|
|
92ae4786f0 | ||
|
|
b35cf14d32 | ||
|
|
14374b55d2 | ||
|
|
5a919018c5 | ||
|
|
6ac73d3f25 | ||
|
|
510fe3902e | ||
|
|
2bc23594ad | ||
|
|
06e00f3066 | ||
|
|
9b3d409695 | ||
|
|
f7f5737abf | ||
|
|
5b8c548d84 | ||
|
|
458f135ee1 | ||
|
|
8e116bf62d | ||
|
|
f1d697a83f | ||
|
|
69a7a57f41 | ||
|
|
24de1559a5 | ||
|
|
ec29abfcaf | ||
|
|
c51cbda31a | ||
|
|
c773ddd117 | ||
|
|
eac97db665 | ||
|
|
d8386328e7 | ||
|
|
d28f321aa2 | ||
|
|
e691c076a1 |
@@ -167,9 +167,9 @@ ENTERPRISE_LICENSE_KEY=
|
||||
# DEFAULT_ORGANIZATION_ID=
|
||||
# DEFAULT_ORGANIZATION_ROLE=owner
|
||||
|
||||
# Send new users to customer.io
|
||||
# CUSTOMER_IO_API_KEY=
|
||||
# CUSTOMER_IO_SITE_ID=
|
||||
# Send new users to Brevo
|
||||
# BREVO_API_KEY=
|
||||
# BREVO_LIST_ID=
|
||||
|
||||
# Ignore Rate Limiting across the Formbricks app
|
||||
# RATE_LIMITING_DISABLED=1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Draft release
|
||||
run-name: Draft release ${{ inputs.next_version }}
|
||||
name: Prepare release
|
||||
run-name: Prepare release ${{ inputs.next_version }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -11,9 +11,10 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
draft_release:
|
||||
prepare_release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -42,14 +43,20 @@ jobs:
|
||||
cd apps/web
|
||||
pnpm version ${{ inputs.next_version }} --no-workspaces-update
|
||||
|
||||
- name: Commit changes
|
||||
- name: Commit changes and create a branch
|
||||
run: |
|
||||
branch_name="release-v${{ inputs.next_version }}"
|
||||
git checkout -b "$branch_name"
|
||||
git add .
|
||||
git commit -m "chore: release v${{ inputs.next_version }}"
|
||||
git push
|
||||
git push origin "$branch_name"
|
||||
|
||||
- name: Draft release
|
||||
run: gh release create v${{ inputs.next_version }} --generate-notes --draft
|
||||
- name: Create pull request
|
||||
run: |
|
||||
gh pr create \
|
||||
--base main \
|
||||
--head "release-v${{ inputs.next_version }}" \
|
||||
--title "chore: bump version to v${{ inputs.next_version }}" \
|
||||
--body "This PR contains the changes for the v${{ inputs.next_version }} release."
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ inputs.next_version }}
|
||||
39
.github/workflows/sonarqube.yml
vendored
@@ -1,11 +1,11 @@
|
||||
name: SonarQube
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
# pull_request:
|
||||
# types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
@@ -16,6 +16,35 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
|
||||
- name: Setup Node.js 20.x
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --config.platform=linux --config.architecture=x64
|
||||
|
||||
- name: create .env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Generate Random ENCRYPTION_KEY, CRON_SECRET & NEXTAUTH_SECRET and fill in .env
|
||||
run: |
|
||||
RANDOM_KEY=$(openssl rand -hex 32)
|
||||
sed -i "s/ENCRYPTION_KEY=.*/ENCRYPTION_KEY=${RANDOM_KEY}/" .env
|
||||
sed -i "s/CRON_SECRET=.*/CRON_SECRET=${RANDOM_KEY}/" .env
|
||||
sed -i "s/NEXTAUTH_SECRET=.*/NEXTAUTH_SECRET=${RANDOM_KEY}/" .env
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
cd apps/web
|
||||
pnpm test:coverage
|
||||
cd ../../
|
||||
# The Vitest coverage config is in your vite.config.mts
|
||||
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203
|
||||
env:
|
||||
|
||||
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
|
||||
@@ -3,7 +3,7 @@ name: "Welcome new contributors"
|
||||
on:
|
||||
issues:
|
||||
types: opened
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: opened
|
||||
|
||||
permissions:
|
||||
|
||||
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" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -35,33 +35,41 @@ tags:
|
||||
sessions, and responses within the Formbricks platform. This API is ideal
|
||||
for client-side interactions, as it doesn't expose sensitive information.
|
||||
|
||||
|
||||
- [Actions
|
||||
API](https://go.postman.co/workspace/90ba379a-0de2-47d2-a94c-8eb541e47082/documentation/11026000-927c954f-85a9-4f8f-b0ec-14191b903737?entity=folder-b8f3a10e-1642-4d82-a629-fef0a8c6c86c)
|
||||
- Create actions for a person
|
||||
|
||||
- [Displays API](https://formbricks.com/docs/api/client/displays) - Mark
|
||||
- Displays API - Mark
|
||||
Survey as Displayed or Responded for a Person
|
||||
|
||||
- [People API](https://formbricks.com/docs/api/client/people) - Create &
|
||||
update people (e.g. attributes)
|
||||
- Responses API - Create & update responses for a survey
|
||||
|
||||
- Environment API - Get the environment state to be used in Formbricks SDKs
|
||||
|
||||
- Contacts API - Identify & update contacts (e.g. attributes)
|
||||
|
||||
- User API - Identify & track users based on their attributes, device type, etc.
|
||||
|
||||
- [Responses API](https://formbricks.com/docs/api/client/responses) -
|
||||
Create & update responses for a survey
|
||||
- name: Client API > Display
|
||||
description: >-
|
||||
Displays are metrics used to measure the number of times a survey was
|
||||
viewed both by unidentified or identified users.
|
||||
- name: Client API > People
|
||||
description: >-
|
||||
Persons are the identified users on Formbricks app that get initated when
|
||||
you pass a userId and have user activation enabled. This now allows you to
|
||||
track & show them targeted surveys based on their actions, attributes,
|
||||
etc.
|
||||
- name: Client API > Response
|
||||
description: >-
|
||||
Responses are captured whenever a user fills in your survey either
|
||||
partially or completely.
|
||||
- name: Client API > Environment
|
||||
description: >-
|
||||
Get the environment state to be used in Formbricks SDKs
|
||||
- name: Client API > Contacts
|
||||
description: >-
|
||||
Contacts are the identified users on Formbricks app that get initated when
|
||||
you pass a userId and have user activation enabled. This now allows you to
|
||||
track & show them targeted surveys based on their attributes, device type,
|
||||
etc.
|
||||
- name: Client API > User
|
||||
description: >-
|
||||
Users are the identified users on Formbricks app that get initated when
|
||||
you pass a userId and have user activation enabled. This now allows you to
|
||||
track & show them targeted surveys based on their attributes, device type,
|
||||
etc. Currently, this api is only being used in the react-native sdk.
|
||||
|
||||
- name: Management API
|
||||
description: "The Management API provides access to all data and settings that are visible in the Formbricks App. This API requires a personal API Key for authentication, which can be generated in the Settings section of the Formbricks App. With the Management API, you can manage your Formbricks account programmatically, accessing and modifying data and settings as needed.\n\n> **For Auth:**\_we use the `x-api-key` header \n \n\nAPI requests made to the Management API are authorized using a personal API key. This key grants the same rights and access as if you were logged in at formbricks.com. It's essential to keep your API key secure and not share it with others.\n\nTo generate, store, or delete an API key, follow the instructions provided on the following page\_[API Key](https://formbricks.com/docs/api/management/api-key-setup)."
|
||||
- name: Management API > Action Class
|
||||
@@ -436,139 +444,424 @@ paths:
|
||||
code: internal_server_error
|
||||
message: Person with ID 2 not found
|
||||
details: {}
|
||||
/api/v1/client/{environmentId}/people:
|
||||
post:
|
||||
/api/v1/client/{environmentId}/environment:
|
||||
get:
|
||||
tags:
|
||||
- Client API > People
|
||||
summary: Create Person
|
||||
description: Creates a person in Formbricks with the given userId, it must be unique.
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
example:
|
||||
userId: "{{userId}}"
|
||||
- Client API > Environment
|
||||
summary: Get Environment State
|
||||
description: >-
|
||||
Retrieves the environment state to be used in Formbricks SDKs
|
||||
parameters:
|
||||
- name: environmentId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The ID of the environment
|
||||
responses:
|
||||
"200":
|
||||
description: "HTTP Status 200"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
surveys:
|
||||
type: array
|
||||
description: List of surveys in the environment
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: "cm6orr901000g19wjwwa690eo"
|
||||
welcomeCard:
|
||||
type: object
|
||||
properties:
|
||||
html:
|
||||
type: string
|
||||
example: "Thanks for providing your feedback - let's go!"
|
||||
enabled:
|
||||
type: boolean
|
||||
example: false
|
||||
headline:
|
||||
type: string
|
||||
example: "Welcome!"
|
||||
buttonLabel:
|
||||
type: string
|
||||
example: "Next"
|
||||
timeToFinish:
|
||||
type: boolean
|
||||
example: false
|
||||
showResponseCount:
|
||||
type: boolean
|
||||
example: false
|
||||
name:
|
||||
type: string
|
||||
example: "Start from scratch"
|
||||
questions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: "dd5c8w2a4ttkbnjb9nwhtb17"
|
||||
type:
|
||||
type: string
|
||||
example: "openText"
|
||||
headline:
|
||||
type: string
|
||||
example: "What would you like to know?"
|
||||
required:
|
||||
type: boolean
|
||||
example: true
|
||||
charLimit:
|
||||
type: boolean
|
||||
example: false
|
||||
inputType:
|
||||
type: string
|
||||
example: "text"
|
||||
buttonLabel:
|
||||
type: string
|
||||
example: "Next"
|
||||
placeholder:
|
||||
type: string
|
||||
example: "Type your answer here..."
|
||||
variables:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
example: "app"
|
||||
showLanguageSwitch:
|
||||
type: boolean
|
||||
example: null
|
||||
languages:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
endings:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: "o729tod5klhix62njmk262dk"
|
||||
type:
|
||||
type: string
|
||||
example: "endScreen"
|
||||
headline:
|
||||
type: string
|
||||
example: "Thank you!"
|
||||
subheader:
|
||||
type: string
|
||||
example: "We appreciate your feedback."
|
||||
buttonLink:
|
||||
type: string
|
||||
example: "https://formbricks.com"
|
||||
buttonLabel:
|
||||
type: string
|
||||
example: "Create your own Survey"
|
||||
autoClose:
|
||||
type: boolean
|
||||
example: null
|
||||
styling:
|
||||
type: object
|
||||
example: null
|
||||
status:
|
||||
type: string
|
||||
example: "inProgress"
|
||||
segment:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: "cm6orr90h000h19wj1lnwoxwg"
|
||||
createdAt:
|
||||
type: string
|
||||
example: "2025-02-03T08:08:33.377Z"
|
||||
updatedAt:
|
||||
type: string
|
||||
example: "2025-02-03T08:08:33.377Z"
|
||||
title:
|
||||
type: string
|
||||
example: "cm6orr901000g19wjwwa690eo"
|
||||
description:
|
||||
type: string
|
||||
example: null
|
||||
isPrivate:
|
||||
type: boolean
|
||||
example: true
|
||||
filters:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
recontactDays:
|
||||
type: integer
|
||||
example: 0
|
||||
displayLimit:
|
||||
type: integer
|
||||
example: 5
|
||||
displayOption:
|
||||
type: string
|
||||
example: "respondMultiple"
|
||||
hiddenFields:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
example: true
|
||||
fieldIds:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
triggers:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
actionClass:
|
||||
type: string
|
||||
example: "code action"
|
||||
displayPercentage:
|
||||
type: integer
|
||||
example: null
|
||||
delay:
|
||||
type: integer
|
||||
example: 0
|
||||
actionClasses:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: "cm6orqtdd000b19wjec82bpp2"
|
||||
type:
|
||||
type: string
|
||||
example: "automatic"
|
||||
name:
|
||||
type: string
|
||||
example: "New Session"
|
||||
key:
|
||||
type: string
|
||||
nullable: true
|
||||
example: null
|
||||
noCodeConfig:
|
||||
type: object
|
||||
nullable: true
|
||||
example: null
|
||||
example:
|
||||
- id: "cm6orqtdd000b19wjec82bpp2"
|
||||
type: "automatic"
|
||||
name: "New Session"
|
||||
key: null
|
||||
noCodeConfig: null
|
||||
- id: "cm6oryki3000i19wj860utcnn"
|
||||
type: "code"
|
||||
name: "code action"
|
||||
key: "code"
|
||||
noCodeConfig: null
|
||||
project:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
example: "cm6orqtcl000319wj9wb7dltl"
|
||||
recontactDays:
|
||||
type: integer
|
||||
example: 7
|
||||
clickOutsideClose:
|
||||
type: boolean
|
||||
example: true
|
||||
darkOverlay:
|
||||
type: boolean
|
||||
example: false
|
||||
placement:
|
||||
type: string
|
||||
example: "bottomRight"
|
||||
inAppSurveyBranding:
|
||||
type: boolean
|
||||
example: true
|
||||
styling:
|
||||
type: object
|
||||
properties:
|
||||
brandColor:
|
||||
type: object
|
||||
properties:
|
||||
light:
|
||||
type: string
|
||||
example: "#64748b"
|
||||
allowStyleOverwrite:
|
||||
type: boolean
|
||||
example: true
|
||||
"404":
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
example: "not_found"
|
||||
message:
|
||||
type: string
|
||||
example: "Environment not found"
|
||||
details:
|
||||
type: object
|
||||
properties:
|
||||
resource_id:
|
||||
type: string
|
||||
example: "tpywklouw2p7tebdu4zv01an"
|
||||
resource_type:
|
||||
type: string
|
||||
example: "environment"
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
example: "internal_server_error"
|
||||
message:
|
||||
type: string
|
||||
example: "Unable to complete request: Expected property name or '}' in JSON at position 29"
|
||||
details:
|
||||
type: object
|
||||
/api/v1/client/{environmentId}/identify/contacts/{userId}:
|
||||
get:
|
||||
tags:
|
||||
- Client API > Contacts
|
||||
summary: Get Contact State
|
||||
description: >-
|
||||
Retrieves a contact's state including their segments, displays, responses
|
||||
and other tracking information. If the contact doesn't exist, it will be created.
|
||||
parameters:
|
||||
- name: environmentId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The ID of the environment
|
||||
- name: userId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: The user ID to identify the contact
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
schema:
|
||||
type: boolean
|
||||
example: "true"
|
||||
Access-Control-Allow-Origin:
|
||||
schema:
|
||||
type: string
|
||||
example: "*"
|
||||
access-control-allow-methods:
|
||||
schema:
|
||||
type: string
|
||||
example: GET, POST, PUT, DELETE, OPTIONS
|
||||
access-control-allow-headers:
|
||||
schema:
|
||||
type: string
|
||||
example: Content-Type, Authorization
|
||||
vary:
|
||||
schema:
|
||||
type: string
|
||||
example: RSC, Next-Router-State-Tree, Next-Router-Prefetch
|
||||
cache-control:
|
||||
schema:
|
||||
type: string
|
||||
example: private, no-store
|
||||
content-type:
|
||||
schema:
|
||||
type: string
|
||||
example: application/json
|
||||
Date:
|
||||
schema:
|
||||
type: string
|
||||
example: Tue, 23 Apr 2024 07:35:47 GMT
|
||||
Connection:
|
||||
schema:
|
||||
type: string
|
||||
example: keep-alive
|
||||
Keep-Alive:
|
||||
schema:
|
||||
type: string
|
||||
example: timeout=5
|
||||
Transfer-Encoding:
|
||||
schema:
|
||||
type: string
|
||||
example: chunked
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
example:
|
||||
data:
|
||||
userId: Shubham
|
||||
"400":
|
||||
description: Bad Request
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
schema:
|
||||
type: boolean
|
||||
example: "true"
|
||||
Access-Control-Allow-Origin:
|
||||
schema:
|
||||
type: string
|
||||
example: "*"
|
||||
access-control-allow-methods:
|
||||
schema:
|
||||
type: string
|
||||
example: GET, POST, PUT, DELETE, OPTIONS
|
||||
access-control-allow-headers:
|
||||
schema:
|
||||
type: string
|
||||
example: Content-Type, Authorization
|
||||
vary:
|
||||
schema:
|
||||
type: string
|
||||
example: RSC, Next-Router-State-Tree, Next-Router-Prefetch
|
||||
content-type:
|
||||
schema:
|
||||
type: string
|
||||
example: application/json
|
||||
Date:
|
||||
schema:
|
||||
type: string
|
||||
example: Tue, 23 Apr 2024 07:36:43 GMT
|
||||
Connection:
|
||||
schema:
|
||||
type: string
|
||||
example: keep-alive
|
||||
Keep-Alive:
|
||||
schema:
|
||||
type: string
|
||||
example: timeout=5
|
||||
Transfer-Encoding:
|
||||
schema:
|
||||
type: string
|
||||
example: chunked
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
userId:
|
||||
type: string
|
||||
description: The user ID of the contact
|
||||
segments:
|
||||
type: array
|
||||
description: List of segment IDs the contact belongs to
|
||||
items:
|
||||
type: string
|
||||
displays:
|
||||
type: array
|
||||
description: List of survey displays for this contact
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
surveyId:
|
||||
type: string
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
responses:
|
||||
type: array
|
||||
description: List of survey IDs the contact has responded to
|
||||
items:
|
||||
type: string
|
||||
lastDisplayAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
description: Timestamp of the last survey display
|
||||
example:
|
||||
userId: "user-123"
|
||||
segments: ["fi8f9oekza95wwszrptidivq", "zgwrv8eg7vfavdhzv1s0po1w"]
|
||||
displays: [{ surveyId: "pjogp5a1wyxon6umplmf49b8", createdAt: "2024-04-23T08:59:37.550Z" }]
|
||||
responses: ["pjogp5a1wyxon6umplmf49b8"]
|
||||
lastDisplayAt: "2024-04-23T08:59:37.550Z"
|
||||
"403":
|
||||
description: Forbidden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
example:
|
||||
code: bad_request
|
||||
message: userId is required
|
||||
details:
|
||||
environmentId: clurwouax000azffxt7n5unn3
|
||||
/api/v1/client/{environmentId}/people/{userId}/attributes:
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
example: "forbidden"
|
||||
message:
|
||||
type: string
|
||||
example: "User identification is only available for enterprise users."
|
||||
"404":
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
example: "not_found"
|
||||
message:
|
||||
type: string
|
||||
example: "Environment not found"
|
||||
details:
|
||||
type: object
|
||||
properties:
|
||||
resource_id:
|
||||
type: string
|
||||
example: "tpywklouw2p7tebdu4zv01an"
|
||||
resource_type:
|
||||
type: string
|
||||
example: "environment"
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: string
|
||||
example: "internal_server_error"
|
||||
message:
|
||||
type: string
|
||||
example: "Unable to complete request: Expected property name or '}' in JSON at position 29"
|
||||
details:
|
||||
type: object
|
||||
/api/v1/client/{environmentId}/contacts/{userId}/attributes:
|
||||
put:
|
||||
tags:
|
||||
- Client API > People
|
||||
summary: Update Person
|
||||
- Client API > Contacts
|
||||
summary: Update Contact (Attributes)
|
||||
description: >-
|
||||
Update a person's attributes in Formbricks to keep them in sync with
|
||||
Update a contact's attributes in Formbricks to keep them in sync with
|
||||
your app or when you want to set a custom attribute in Formbricks.
|
||||
requestBody:
|
||||
content:
|
||||
@@ -713,6 +1006,280 @@ paths:
|
||||
Unable to complete request: Expected property name or '}' in
|
||||
JSON at position 29
|
||||
details: {}
|
||||
/api/v1/client/{environmentId}/user:
|
||||
post:
|
||||
tags:
|
||||
- Client API > User
|
||||
summary: Create or Identify User
|
||||
description: >
|
||||
Endpoint for creating or identifying a user within the specified environment.
|
||||
If the user already exists, this will identify them and potentially
|
||||
update user attributes. If they don't exist, it will create a new user.
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
example:
|
||||
userId: "hello-user"
|
||||
attributes:
|
||||
plan: "free"
|
||||
parameters:
|
||||
- name: environmentId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
schema:
|
||||
type: boolean
|
||||
example: "true"
|
||||
Access-Control-Allow-Origin:
|
||||
schema:
|
||||
type: string
|
||||
example: "*"
|
||||
access-control-allow-methods:
|
||||
schema:
|
||||
type: string
|
||||
example: GET, POST, PUT, DELETE, OPTIONS
|
||||
access-control-allow-headers:
|
||||
schema:
|
||||
type: string
|
||||
example: Content-Type, Authorization
|
||||
vary:
|
||||
schema:
|
||||
type: string
|
||||
example: RSC, Next-Router-State-Tree, Next-Router-Prefetch
|
||||
content-type:
|
||||
schema:
|
||||
type: string
|
||||
example: application/json
|
||||
Date:
|
||||
schema:
|
||||
type: string
|
||||
example: Tue, 23 Apr 2024 07:36:43 GMT
|
||||
Connection:
|
||||
schema:
|
||||
type: string
|
||||
example: keep-alive
|
||||
Keep-Alive:
|
||||
schema:
|
||||
type: string
|
||||
example: timeout=5
|
||||
Transfer-Encoding:
|
||||
schema:
|
||||
type: string
|
||||
example: chunked
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
state:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
properties:
|
||||
userId:
|
||||
type: string
|
||||
example: "hello-user"
|
||||
segments:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: "cm6onrezn000hw2ahcokiz41v"
|
||||
displays:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
surveyId:
|
||||
type: string
|
||||
example: "cm6orqtdd000a19wjhnbces5s"
|
||||
createdAt:
|
||||
type: string
|
||||
example: "2025-02-03T11:23:13.050Z"
|
||||
responses:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: "cm6orqtdd000a19wjhnbces5s"
|
||||
lastDisplayAt:
|
||||
type: string
|
||||
example: "2025-02-03T11:23:13.050Z"
|
||||
expiresAt:
|
||||
type: string
|
||||
example: "2025-02-03T11:23:13.050Z"
|
||||
"400":
|
||||
description: Bad Request
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
schema:
|
||||
type: boolean
|
||||
example: "true"
|
||||
Access-Control-Allow-Origin:
|
||||
schema:
|
||||
type: string
|
||||
example: "*"
|
||||
access-control-allow-methods:
|
||||
schema:
|
||||
type: string
|
||||
example: GET, POST, PUT, DELETE, OPTIONS
|
||||
access-control-allow-headers:
|
||||
schema:
|
||||
type: string
|
||||
example: Content-Type, Authorization
|
||||
vary:
|
||||
schema:
|
||||
type: string
|
||||
example: RSC, Next-Router-State-Tree, Next-Router-Prefetch
|
||||
content-type:
|
||||
schema:
|
||||
type: string
|
||||
example: application/json
|
||||
Date:
|
||||
schema:
|
||||
type: string
|
||||
example: Tue, 23 Apr 2024 07:36:43 GMT
|
||||
Connection:
|
||||
schema:
|
||||
type: string
|
||||
example: keep-alive
|
||||
Keep-Alive:
|
||||
schema:
|
||||
type: string
|
||||
example: timeout=5
|
||||
Transfer-Encoding:
|
||||
schema:
|
||||
type: string
|
||||
example: chunked
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
example:
|
||||
code: bad_request
|
||||
message: userId is required
|
||||
"404":
|
||||
description: Not Found
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
schema:
|
||||
type: boolean
|
||||
example: "true"
|
||||
Access-Control-Allow-Origin:
|
||||
schema:
|
||||
type: string
|
||||
example: "*"
|
||||
access-control-allow-methods:
|
||||
schema:
|
||||
type: string
|
||||
example: GET, POST, PUT, DELETE, OPTIONS
|
||||
access-control-allow-headers:
|
||||
schema:
|
||||
type: string
|
||||
example: Content-Type, Authorization
|
||||
vary:
|
||||
schema:
|
||||
type: string
|
||||
example: RSC, Next-Router-State-Tree, Next-Router-Prefetch
|
||||
cache-control:
|
||||
schema:
|
||||
type: string
|
||||
example: private, no-store
|
||||
content-type:
|
||||
schema:
|
||||
type: string
|
||||
example: application/json
|
||||
Date:
|
||||
schema:
|
||||
type: string
|
||||
example: Tue, 23 Apr 2024 07:56:29 GMT
|
||||
Connection:
|
||||
schema:
|
||||
type: string
|
||||
example: keep-alive
|
||||
Keep-Alive:
|
||||
schema:
|
||||
type: string
|
||||
example: timeout=5
|
||||
Transfer-Encoding:
|
||||
schema:
|
||||
type: string
|
||||
example: chunked
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
example:
|
||||
code: not_found
|
||||
message: Environment not found
|
||||
details:
|
||||
resource_id: f16ttdvtkx85k5m4s561ruqj
|
||||
resource_type: Environment
|
||||
"500":
|
||||
description: Internal Server Error
|
||||
headers:
|
||||
Access-Control-Allow-Credentials:
|
||||
schema:
|
||||
type: boolean
|
||||
example: "true"
|
||||
Access-Control-Allow-Origin:
|
||||
schema:
|
||||
type: string
|
||||
example: "*"
|
||||
access-control-allow-methods:
|
||||
schema:
|
||||
type: string
|
||||
example: GET, POST, PUT, DELETE, OPTIONS
|
||||
access-control-allow-headers:
|
||||
schema:
|
||||
type: string
|
||||
example: Content-Type, Authorization
|
||||
vary:
|
||||
schema:
|
||||
type: string
|
||||
example: RSC, Next-Router-State-Tree, Next-Router-Prefetch
|
||||
cache-control:
|
||||
schema:
|
||||
type: string
|
||||
example: private, no-store
|
||||
content-type:
|
||||
schema:
|
||||
type: string
|
||||
example: application/json
|
||||
Date:
|
||||
schema:
|
||||
type: string
|
||||
example: Tue, 23 Apr 2024 07:56:29 GMT
|
||||
Connection:
|
||||
schema:
|
||||
type: string
|
||||
example: keep-alive
|
||||
Keep-Alive:
|
||||
schema:
|
||||
type: string
|
||||
example: timeout=5
|
||||
Transfer-Encoding:
|
||||
schema:
|
||||
type: string
|
||||
example: chunked
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
example:
|
||||
code: internal_server_error
|
||||
message: An unexpected error occurred
|
||||
details: {}
|
||||
/api/v1/client/{environmentId}/responses:
|
||||
post:
|
||||
tags:
|
||||
|
||||
@@ -18,6 +18,7 @@ FROM node:22-alpine3.20 AS base
|
||||
FROM base AS installer
|
||||
|
||||
# Enable corepack and prepare pnpm
|
||||
RUN npm install -g corepack@latest
|
||||
RUN corepack enable
|
||||
|
||||
# Install necessary build tools and compilers
|
||||
@@ -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();
|
||||
|
||||
@@ -127,7 +127,7 @@ export const LandingSidebar = ({
|
||||
await signOut({ callbackUrl: "/auth/login" });
|
||||
await formbricksLogout();
|
||||
}}
|
||||
icon={<LogOutIcon className="h-4 w-4" strokeWidth={1.5} />}>
|
||||
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
||||
{t("common.logout")}
|
||||
</DropdownMenuItem>
|
||||
|
||||
|
||||
@@ -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,11 +4,10 @@ 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 { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -21,7 +20,6 @@ interface AddressQuestionFormProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -33,11 +31,10 @@ export const AddressQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: AddressQuestionFormProps): JSX.Element => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslate();
|
||||
const fields = [
|
||||
{
|
||||
id: "addressLine1",
|
||||
@@ -107,7 +104,6 @@ export const AddressQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -125,7 +121,6 @@ export const AddressQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ConditionalLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { UpdateQuestionId } from "./UpdateQuestionId";
|
||||
|
||||
@@ -8,7 +7,6 @@ interface AdvancedSettingsProps {
|
||||
questionIdx: number;
|
||||
localSurvey: TSurvey;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
}
|
||||
|
||||
export const AdvancedSettings = ({
|
||||
@@ -16,7 +14,6 @@ export const AdvancedSettings = ({
|
||||
questionIdx,
|
||||
localSurvey,
|
||||
updateQuestion,
|
||||
contactAttributeKeys,
|
||||
}: AdvancedSettingsProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -25,7 +22,6 @@ export const AdvancedSettings = ({
|
||||
updateQuestion={updateQuestion}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
/>
|
||||
|
||||
<UpdateQuestionId
|
||||
|
||||
@@ -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,9 +6,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 { useTranslations } from "next-intl";
|
||||
import { useTranslate } from "@tolgee/react";
|
||||
import { type JSX, useState } from "react";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -21,7 +20,6 @@ interface CTAQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -34,10 +32,9 @@ export const CTAQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: CTAQuestionFormProps): JSX.Element => {
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslate();
|
||||
const options = [
|
||||
{
|
||||
value: "internal",
|
||||
@@ -59,7 +56,6 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -103,7 +99,6 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -120,7 +115,6 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
)}
|
||||
@@ -155,7 +149,6 @@ export const CTAQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
"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 { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -20,7 +21,6 @@ interface CalQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -32,12 +32,11 @@ export const CalQuestionForm = ({
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
isInvalid,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: 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 });
|
||||
@@ -60,7 +59,6 @@ export const CalQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<div>
|
||||
@@ -77,7 +75,6 @@ export const CalQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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,11 +25,9 @@ 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";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
|
||||
interface ConditionalLogicProps {
|
||||
@@ -34,23 +35,21 @@ interface ConditionalLogicProps {
|
||||
questionIdx: number;
|
||||
question: TSurveyQuestion;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
}
|
||||
|
||||
export function ConditionalLogic({
|
||||
contactAttributeKeys,
|
||||
localSurvey,
|
||||
question,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
}: ConditionalLogicProps) {
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslate();
|
||||
const transformedSurvey = useMemo(() => {
|
||||
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default", contactAttributeKeys);
|
||||
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default", contactAttributeKeys);
|
||||
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default");
|
||||
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default");
|
||||
|
||||
return modifiedSurvey;
|
||||
}, [localSurvey, contactAttributeKeys]);
|
||||
}, [localSurvey]);
|
||||
|
||||
const addLogic = () => {
|
||||
const operator = getDefaultOperatorForQuestion(question, t);
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
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 { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -17,7 +16,6 @@ interface ConsentQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -29,11 +27,10 @@ export const ConsentQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: ConsentQuestionFormProps): JSX.Element => {
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslate();
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -46,7 +43,6 @@ export const ConsentQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -80,7 +76,6 @@ export const ConsentQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</form>
|
||||
|
||||
@@ -4,11 +4,10 @@ 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 { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyContactInfoQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -21,7 +20,6 @@ interface ContactInfoQuestionFormProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -33,10 +31,9 @@ export const ContactInfoQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: ContactInfoQuestionFormProps): JSX.Element => {
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslate();
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
|
||||
|
||||
const fields = [
|
||||
@@ -98,7 +95,6 @@ export const ContactInfoQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -116,7 +112,6 @@ export const ContactInfoQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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,13 +1,14 @@
|
||||
"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 { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -20,7 +21,6 @@ interface IDateQuestionFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -47,11 +47,10 @@ export const DateQuestionForm = ({
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: IDateQuestionFormProps): JSX.Element => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslate();
|
||||
const [parent] = useAutoAnimate();
|
||||
return (
|
||||
<form>
|
||||
@@ -65,7 +64,6 @@ export const DateQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<div ref={parent}>
|
||||
@@ -82,7 +80,6 @@ export const DateQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,13 +14,12 @@ 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";
|
||||
import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
|
||||
import {
|
||||
TSurvey,
|
||||
@@ -39,7 +38,6 @@ interface EditEndingCardProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
plan: TOrganizationBillingPlan;
|
||||
addEndingCard: (index: number) => void;
|
||||
isFormbricksCloud: boolean;
|
||||
@@ -55,14 +53,13 @@ export const EditEndingCard = ({
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
plan,
|
||||
addEndingCard,
|
||||
isFormbricksCloud,
|
||||
locale,
|
||||
}: EditEndingCardProps) => {
|
||||
const endingCard = localSurvey.endings[endingCardIndex];
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslate();
|
||||
const isRedirectToUrlDisabled = isFormbricksCloud
|
||||
? plan === "free" && endingCard.type !== "redirectToUrl"
|
||||
: false;
|
||||
@@ -200,21 +197,13 @@ export const EditEndingCard = ({
|
||||
<p className="text-sm font-semibold">
|
||||
{endingCard.type === "endScreen" &&
|
||||
(endingCard.headline &&
|
||||
recallToHeadline(
|
||||
endingCard.headline,
|
||||
localSurvey,
|
||||
true,
|
||||
selectedLanguageCode,
|
||||
contactAttributeKeys
|
||||
)[selectedLanguageCode]
|
||||
recallToHeadline(endingCard.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
]
|
||||
? formatTextWithSlashes(
|
||||
recallToHeadline(
|
||||
endingCard.headline,
|
||||
localSurvey,
|
||||
true,
|
||||
selectedLanguageCode,
|
||||
contactAttributeKeys
|
||||
)[selectedLanguageCode]
|
||||
recallToHeadline(endingCard.headline, localSurvey, true, selectedLanguageCode)[
|
||||
selectedLanguageCode
|
||||
]
|
||||
)
|
||||
: t("environments.surveys.edit.ending_card"))}
|
||||
{endingCard.type === "redirectToUrl" &&
|
||||
@@ -242,7 +231,6 @@ export const EditEndingCard = ({
|
||||
updateCard={() => {}}
|
||||
addCard={addEndingCard}
|
||||
cardType="ending"
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -274,19 +262,13 @@ export const EditEndingCard = ({
|
||||
isInvalid={isInvalid}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
updateSurvey={updateSurvey}
|
||||
endingCard={endingCard}
|
||||
locale={locale}
|
||||
/>
|
||||
)}
|
||||
{endingCard.type === "redirectToUrl" && (
|
||||
<RedirectUrlForm
|
||||
localSurvey={localSurvey}
|
||||
endingCard={endingCard}
|
||||
updateSurvey={updateSurvey}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
/>
|
||||
<RedirectUrlForm localSurvey={localSurvey} endingCard={endingCard} updateSurvey={updateSurvey} />
|
||||
)}
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -6,12 +6,11 @@ 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";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyQuestionId, TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -23,7 +22,6 @@ interface EditWelcomeCardProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
locale: TUserLocale;
|
||||
}
|
||||
|
||||
@@ -35,10 +33,9 @@ export const EditWelcomeCard = ({
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
locale,
|
||||
}: EditWelcomeCardProps) => {
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslate();
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
const path = usePathname();
|
||||
const environmentId = path?.split("/environments/")[1]?.split("/")[0];
|
||||
@@ -136,7 +133,6 @@ export const EditWelcomeCard = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -173,7 +169,6 @@ export const EditWelcomeCard = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
label={t("environments.surveys.edit.next_button_label")}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -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,12 +5,11 @@ 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";
|
||||
import { headlineToRecall, recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TSurvey, TSurveyEndScreenCard } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
|
||||
@@ -20,7 +19,6 @@ interface EndScreenFormProps {
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
updateSurvey: (input: Partial<TSurveyEndScreenCard>) => void;
|
||||
endingCard: TSurveyEndScreenCard;
|
||||
locale: TUserLocale;
|
||||
@@ -32,12 +30,11 @@ export const EndScreenForm = ({
|
||||
isInvalid,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
updateSurvey,
|
||||
endingCard,
|
||||
locale,
|
||||
}: EndScreenFormProps) => {
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslate();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [showEndingCardCTA, setshowEndingCardCTA] = useState<boolean>(
|
||||
endingCard.type === "endScreen" &&
|
||||
@@ -55,7 +52,6 @@ export const EndScreenForm = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
|
||||
@@ -69,7 +65,6 @@ export const EndScreenForm = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
@@ -115,7 +110,6 @@ export const EndScreenForm = ({
|
||||
updateSurvey={updateSurvey}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -137,7 +131,6 @@ export const EndScreenForm = ({
|
||||
onAddFallback={() => {
|
||||
inputRef.current?.focus();
|
||||
}}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
isRecallAllowed
|
||||
localSurvey={localSurvey}
|
||||
usedLanguageCode={"default"}
|
||||
@@ -164,8 +157,7 @@ export const EndScreenForm = ({
|
||||
},
|
||||
localSurvey,
|
||||
false,
|
||||
"default",
|
||||
contactAttributeKeys
|
||||
"default"
|
||||
)[selectedLanguageCode]
|
||||
}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
|
||||
@@ -6,15 +6,14 @@ 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";
|
||||
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { createI18nString } from "@formbricks/lib/i18n/utils";
|
||||
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
|
||||
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||
import { TProject } from "@formbricks/types/project";
|
||||
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys/types";
|
||||
import { TUserLocale } from "@formbricks/types/user";
|
||||
@@ -29,7 +28,6 @@ interface FileUploadFormProps {
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (languageCode: string) => void;
|
||||
isInvalid: boolean;
|
||||
contactAttributeKeys: TContactAttributeKey[];
|
||||
isFormbricksCloud: boolean;
|
||||
locale: TUserLocale;
|
||||
}
|
||||
@@ -43,12 +41,11 @@ export const FileUploadQuestionForm = ({
|
||||
project,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
contactAttributeKeys,
|
||||
isFormbricksCloud,
|
||||
locale,
|
||||
}: FileUploadFormProps): JSX.Element => {
|
||||
const [extension, setExtension] = useState("");
|
||||
const t = useTranslations();
|
||||
const { t } = useTranslate();
|
||||
const [isMaxSizeError, setMaxSizeError] = useState(false);
|
||||
const {
|
||||
billingInfo,
|
||||
@@ -141,7 +138,6 @@ export const FileUploadQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
<div ref={parent}>
|
||||
@@ -158,7 +154,6 @@ export const FileUploadQuestionForm = ({
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
contactAttributeKeys={contactAttributeKeys}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
@@ -232,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;
|
||||
|
||||