Compare commits

...

37 Commits

Author SHA1 Message Date
Matti Nannt
95b73ee0de chore: update codeql.yml 2024-11-06 05:24:38 +01:00
Dhruwang Jariwala
bee0ab07b4 fix: person response page and copy link transalation issue (#4239)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-11-05 22:55:20 +00:00
Gourav Thakur
0c37956943 fix: Delete account to signup page (#4162)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-11-05 07:27:32 +00:00
Johannes
74bd40e0ff feat: Update Insight category, document sentiment and archive insights (#4038)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-11-05 07:07:27 +00:00
Matti Nannt
31c3f9730e fix: github issue template linting issues (#4234) 2024-11-04 16:49:55 +01:00
Matti Nannt
da8e7c1870 chore: Add CodeQL (#4233) 2024-11-04 16:27:13 +01:00
Matti Nannt
ac0ec1fbcd chore: update issue templates (#4230) 2024-11-04 16:25:54 +01:00
Johannes
18be650561 fix: onboarding template issue (#4221)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-11-03 04:40:40 +00:00
Johannes
5977fa6f80 fix: DE translations (#4216) 2024-11-02 19:14:14 +00:00
Matti Nannt
1e2833b742 docs: remove links to discontinued examples repo (#4212) 2024-11-01 17:13:50 +01:00
Sai Suhas Sawant
f18b030ebf fix: date select highlight fix (#4208)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-11-01 02:28:47 +00:00
Aditya Deshlahre
ef454d8140 docs: replacing old UI image to New on Best Practices (#4192)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-10-31 23:12:50 +00:00
Harsh Shrikant Bhat
1ea391e45b docs: Add quiz guide in the docs (#4180) 2024-10-31 23:02:36 +00:00
Dhruwang Jariwala
b3e6e8d5d0 feat: multi language UI (#3133)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-10-31 08:23:57 +00:00
Sai Suhas Sawant
705f55176f fix: date picker ui-fix (#3382)
Co-authored-by: Jonas Höbenreich <64426524+jonas-hoebenreich@users.noreply.github.com>
Co-authored-by: Vinay Kumar Maheshwaram <54681400+sudovinay01@users.noreply.github.com>
Co-authored-by: Sharad Kushwaha <123795629+SharadK10@users.noreply.github.com>
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
Co-authored-by: Chase Nelson <Chase.a.nelson@gmail.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2024-10-31 04:07:42 +00:00
Raj Gupta
297f349b45 fix: shiki package update from version 0.14.7 to 1.22.0 (#3542)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-10-30 20:49:34 -07:00
DivyanshuLohani
91b6a9e008 fix: Stop creation of empty name segments on people page (#4028)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-10-31 03:34:12 +00:00
Aleksey
189dc52ee9 fix: mobile delay before showing survey doesn't work on react native (#3961)
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-10-31 03:23:08 +00:00
Jonas Höbenreich
280a9a439b feat: add 4 new survey templates (#4018)
Co-authored-by: Bhavesh Mishra <69065938+thefool76@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2024-10-31 03:11:44 +00:00
Ikko Eltociear Ashimine
bc844bbb1f chore: update SurveyDropdownMenu.tsx (#4076)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-10-31 02:38:31 +00:00
Teddy ASSIH
ea2d6de9a7 feat: oss.gg: add new background video (#4123)
Co-authored-by: Johannes <johannes@formbricks.com>
2024-10-31 02:33:44 +00:00
Aditya Deshlahre
e09ab1dcbe feat: career & professionl development survey (#4092)
Co-authored-by: Johannes <johannes@formbricks.com>
2024-10-31 02:28:19 +00:00
Bhavesh Mishra
e1e04517a9 fix: Fixed the animation bug on the site (#4110)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2024-10-31 01:48:57 +00:00
Piyush Gupta
06026b6922 fix: error handling in insight generation (#4075)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2024-10-29 14:14:05 +00:00
Dishant Yadav
19a3faadce fix: Direct Link in Docs don't open Navigation Submenu (#3561)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-10-29 10:55:37 +00:00
Bhavesh Mishra
33543f59f8 feat: Added longterm retention check in template (#4061)
Co-authored-by: Johannes <johannes@formbricks.com>
2024-10-28 03:56:04 +00:00
DivyanshuLohani
47826a45aa fix: open specific nav group on docs page on first page load (#4034)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-10-26 23:16:04 +00:00
Aditya Deshlahre
6f043ec16e docs: add image or video to question (#4041)
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-10-26 23:15:44 +00:00
Bhavesh Mishra
c2703788ae feat: oss.gg: add new background video (#4051)
Co-authored-by: Johannes <johannes@formbricks.com>
2024-10-26 22:13:36 +00:00
Aditya Deshlahre
ef7df0fc77 fix: added webhook url test for a valid URL (#4007)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-10-25 21:18:38 +00:00
Jonas Höbenreich
cdb8199199 feat: oss.gg: add new background video (#4017) 2024-10-25 21:08:20 +00:00
Piyush Gupta
b0ded570ff fix: inconsistencies in survey summary (#4032)
Co-authored-by: Johannes <johannes@formbricks.com>
2024-10-25 16:53:55 +00:00
Piyush Gupta
5c0b29eed4 fix: summary calculation in multi choice questions (#4022)
Co-authored-by: Johannes <johannes@formbricks.com>
2024-10-25 09:17:06 +00:00
Piyush Gupta
8e16d8daf6 fix: adds CSP header (#3754) 2024-10-25 08:58:59 +00:00
Johannes
deea760a17 fix: animate tweaks (#4015) 2024-10-24 20:02:18 -07:00
Matti Nannt
f56f08e3c1 docs: fix last occurence of old sdk call from docs (#4016) 2024-10-24 22:04:03 +02:00
Matti Nannt
5daeab6554 chore: remove attributes from identify call (#4012)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2024-10-24 17:16:56 +00:00
634 changed files with 19824 additions and 6046 deletions

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions
url: https://formbricks.com/discord
about: Ask a general question about the project on our Discord server
url: https://github.com/formbricks/formbricks/discussions
about: Need help selfhosting or ask a general question about the project? Open a discussion

View File

@@ -1,8 +1,7 @@
name: Feature request
description: "Suggest an idea for this project \U0001F680"
title: "[FEATURE]"
labels: enhancement
assignees: []
labels:
- enhancement
body:
- type: textarea
id: problem-description
@@ -18,13 +17,6 @@ body:
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternate-solution-description
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: textarea
id: additional-context
attributes:
@@ -33,15 +25,9 @@ body:
validations:
required: false
- type: markdown
id: formbricks-info
attributes:
value: |
### How we code at Formbricks 🤓
### Additional resources 🤓
- Follow Best Practices lined out in our [Contributor Docs](https://formbricks.com/docs/contributing/how-we-code)
- First time: Please read our [introductory blog post](https://formbricks.com/blog/join-the-formtribe)
- All UI components are in the package `formbricks/ui`
- Run `pnpm go` to find a demo app to test in-app surveys at `localhost:3002`
- Everything is type-safe.
- We use **chatGPT** to help refactor code.
- Check out our [Contributor Docs](https://formbricks.com/docs/developer-docs/contributing/get-started)
- Anything unclear? [Ask in Discord](https://formbricks.com/discord)

View File

@@ -1,33 +0,0 @@
name: oss.gg hack submission 🕹️
description: "Submit your contribution for the for the oss.gg hackathon"
title: "[🕹️]"
labels: 🕹️ oss.gg, player submission, hacktoberfest
assignees: []
body:
- type: textarea
id: contribution-name
attributes:
label: What side quest or challenge are you solving?
description: Add the name of the side quest or challenge.
validations:
required: true
- type: textarea
id: points
attributes:
label: Points
description: How many points are assigned to this contribution?
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: What's the task your performed?
validations:
- type: textarea
id: proof
attributes:
label: Provide proof that you've completed the task
description: Screenshots, loom recordings, links to the content you shared or interacted with.
validations:
required: true

View File

@@ -4,7 +4,7 @@
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
Fixes # (issue)
Fixes #(issue)
<!-- Please provide a screenshots or a loom video for visual changes to speed up reviews
Loom Video: https://www.loom.com/

92
.github/workflows/codeql.yml vendored Normal file
View File

@@ -0,0 +1,92 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '17 1 * * 1'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

92
.github/workflows/codeql2.yml vendored Normal file
View File

@@ -0,0 +1,92 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
schedule:
- cron: '17 1 * * 1'
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

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

18
.vscode/launch.json vendored
View File

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

View File

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

View File

@@ -51,13 +51,13 @@ In the interest of responsibly managing vulnerabilities, please adhere to the fo
> Do not reveal the problem to others until it has been resolved.
1. **Send a Detailed Report**:
- Address emails to [security@formbricks.com](mailto:security@formbricks.com).
- Raise a security report on [Github](https://github.com/formbricks/formbricks/issues/new/choose) or send an email to [security@formbricks.com](mailto:security@formbricks.com).
- Include:
- Problem description.
- Detailed, reproducible steps, with screenshots where possible.
- Affected version(s).
- Known possible mitigations.
- Your Discord username or preferred contact method.
- Your preferred contact method.
2. **Acknowledgement of Receipt**:
- Our security team will acknowledge receipt and provide an initial response within 48 hours.
- Following verification of the vulnerability and the fix, a release plan will be formulated, with the fix deployed between 7 to 28 days, depending on the severity and complexity.

View File

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

View File

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

View File

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

View File

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

View File

@@ -60,7 +60,7 @@ All you need to do is copy a `<script>` tag to your HTML head, and thats abou
</Property>
</Properties>
Refer to our [Example HTML project](https://github.com/formbricks/examples/tree/main/html) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
---
@@ -118,7 +118,7 @@ export default App;
</Property>
</Properties>
Refer to our [Example ReactJs project](https://github.com/formbricks/examples/tree/main/reactjs) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
---
@@ -200,8 +200,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</CodeGroup>
</Col>
Refer to our [Example NextJS App Directory project](https://github.com/formbricks/examples/tree/main/nextjs-app) for more help!
### Pages Directory
<Col>
@@ -239,7 +237,6 @@ export default function App({ Component, pageProps }: AppProps) {
</CodeGroup>
</Col>
Refer to our [Example NextJS Pages Directory project](https://github.com/formbricks/examples/tree/main/nextjs-pages) for more help!
### Required customizations to be made
@@ -332,7 +329,7 @@ router.afterEach((to, from) => {
</Property>
</Properties>
Refer to our [Example VueJs project](https://github.com/formbricks/examples/tree/main/vuejs) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
## React Native

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,7 +1,7 @@
import { MdxImage } from "@/components/MdxImage";
import AddQuestion from "./images/add-question.webp";
import EmailField from "./images/email-field.webp";
import EmbedImage from "./images/embed.png";
import EmbedImage from "./images/embed.webp";
import MessageField from "./images/message-field.webp";
import NameField from "./images/name-field.webp";
import QueryImage from "./images/query-form.webp";
@@ -159,6 +159,8 @@ After publishing the form, follow these steps to integrate it into your site:
2. **Embed the Code**
- Copy the provided code and paste it into your website where you want the form to appear.
<Note>Note: There is an options toggle button called "Embed Mode." When enabled, it updates the `src` to `"?embed=true"` and displays your survey in a minimalist design, removing padding and background for a cleaner look.</Note>
3. **Test the Integration**
- Check if the form displays correctly on your site.
- Submit a test entry to ensure everything works and notifications are received.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -39,13 +39,13 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
1. To get started, create an account for the [Formbricks Cloud](https://app.formbricks.com/auth/signup).
2. In the Menu (top right) you see that you can switch between a “Development” and a “Production” environment. These are two separate environments so your test data doesnt mess up the insights from prod. Switch to “Development”:
2. In the Menu (top right) you see that you can toggle switch between a “Development” and a “Production” environment. These are two separate environments so your test data doesnt mess up the insights from prod. Switch to “Development”:
<MdxImage
src={SwitchToDev}
alt="switch to dev environment"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
className="max-w-full rounded-lg sm:max-w-xs"
/>
3. Then, create a survey using the template “Docs Feedback”:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -115,7 +115,7 @@ Lastly, scroll down to “Recontact Options”. Here you have full freedom to de
src={RecontactOptions}
alt="Set recontact options"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
className="max-w-full rounded-lg sm:max-w-2xl"
/>
### 7. Congrats! Youre ready to publish your survey 💃

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,178 @@
import { MdxImage } from "@/components/MdxImage";
import SingleSelect from "./single-select.webp";
import Quiz from "./quiz.webp";
import Score from "./score.webp"
import AddLogic from "./conditional-logic.webp";
import WhenThen from "./when-then.webp";
import EndingLogic from "./ending-logic.webp";
import PassFail from "./pass-fail.webp";
export const metadata = {
title: "How to Create a Quiz Using Formbricks - Step-by-Step Guide",
description:
"Learn to leverage Formbricks to create Quizzes. Follow our detailed step-by-step guide to build quizzes with custom logic and multiple endings.",
};
# Creating a quiz with Formbricks - Step-by-step Guide
Welcome to this guide on creating engaging quizzes with Formbricks! Quizzes help you capture customer insights, explore user personalities, or simply add fun for your team. With Formbricks, you can personalize quizzes in minutes add scores, customize backgrounds, and more, all without any technical skills!
## What we'll build
By the end of this tutorial, you'll have created a simple trivia Quiz featuring:
1. Score calculations.
2. Multiple endings depending on the score.
## Setting up the form
First, make sure you have a Formbricks account. If not, you can create one [here](https://app.formbricks.com):
1. Head to the Surveys page and click on **New Survey**.
2. Select Start from Scratch to create a new form.
3. Go to the settings and select form type as **Link Survey**
4. In the form editor, click the three dots next to a question, then select Change Question Type and choose **Single-Select**.
<MdxImage
src={SingleSelect}
alt="Change Question type to Single-Select"
quality="100"
className="max-w-full rounded-lg sm:max-w-2xl"
/>
5. Add a welcoming statement to greet your users and explain the Quiz's purpose.
6. Personalize the greeting to make it inviting and encourage engagement.
**Note:** While were creating a Link Survey here, the process is similar for Web and App surveys.
## Adding the questions
Next, let's create a question for example with multiple options:
What country has the longest coastline in the world?
A) Canada
B) Japan
C) India
D) Nepal
<MdxImage
src={Quiz}
alt="Sample Question"
quality="100"
className="max-w-full rounded-lg sm:max-w-xl"
/>
## Calculate Score
Now that we have our question ready, lets add the logic to calculate scores.
1. Scroll down in the editor and click on variables.
2. Create a new variable named `score` with a default value of 0
<MdxImage
src={Score}
alt="Create Variable named Score image"
quality="100"
className="max-w-full rounded-lg sm:max-w-xl"
/>
3. Now go back to the question and expand the advanced settings by clicking on `> Show Advanced Settings`.
4. Under the conditional logic you should see the option to `Add Logic`. Click on that.
<MdxImage
src={AddLogic}
alt="Add Logic Button"
quality="100"
className="max-w-full rounded-lg sm:max-w-xl"
/>
5. Now you should see conditional logic. Customize the logic to match your needs for example:
**When** `question` equals `YOUR_ANSWER_HERE` **Then** `Calculate` `score` `Add +` `01`. So it should look something like this.
<MdxImage
src={WhenThen}
alt="When-Then Conditional Logic"
quality="100"
className="max-w-full rounded-lg sm:max-w-xl"
/>
6. Let's duplicate and customize these questions. Click on the duplicate icon at the top of the question.
7. Now edit the questions, options, and answers in the **conditional logic**
## Creating Multiple Endings Based on Scores
Once you have all the questions and the calculation logic in place, its time to customize the endings. Scroll down to the Ending Card section. We will create two cards for this quiz: one for when the user fails the quiz and another for when the user passes.
1. Customize the ending card.
2. Display the score by typing `@score`. ( You can address all the variables or questions by just typing @ ).
3. Add logic to the last question. ( this is necessary to redirect the user based on the score ). Kind of like this:
**When** `score` >= `03` **Then** `Jump to` `Pass`. So it should look something like this.
<MdxImage
src={EndingLogic}
alt="Conditional Logic for ending card"
quality="100"
className="max-w-full rounded-lg sm:max-w-xl"
/>
4. Ensure that the Fail card is positioned above the Pass card. This allows any condition that does not meet the criteria of being greater than or equal to 3 to jump to the Fail card.
<MdxImage
src={PassFail}
alt="Pass or Fail ending Cards"
quality="100"
className="max-w-full rounded-lg sm:max-w-xl"
/>
5. That's it! Now you can save and publish the quiz.
# Wrapping Up
Congratulations! Youve successfully created a Quiz with Formbricks. You can play around with the quiz that we just created [here](https://app.formbricks.com/s/cm2wwt3vu0001ir8o7ys0bezz).
A great quiz can serve as an excellent lead generator, a job fit checker, or just a fun icebreaker for your team. You now have the skills to build that! If you want to read more about building quizzes and how you can create a Job Fit Quiz check this article [here](https://www.harshbhat.me/blog/formbricks-quiz).

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -48,7 +48,7 @@ Initialize the Formbricks JS Client for surveys. When used in a web app, pass a
<CodeGroup title="Initialize Formbricks">
```javascript
import formbricks from "@formbricks/js/app";
import formbricks from "@formbricks/js";
formbricks.init({
environmentId: "<your-environment-id>", // required

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -0,0 +1,45 @@
import { MdxImage } from "@/components/MdxImage";
import SurveyEmbed from "@/components/SurveyEmbed";
import AddImageOrVideoToQuestionImage from "./images/add-image-or-video-to-question-image.webp";
import AddImageOrVideoToQuestionVideo from "./images/add-image-or-video-to-question-video.webp";
import AddImageOrVideoToQuestion from "./images/add-image-or-video-to-question.webp";
#### Add Image or Video to a Question
Enhance your questions by adding images or videos. This makes instructions clearer and the survey more engaging.
## How to Add Images
Click the icon on the right side of the question to add an image or video:
<MdxImage
src={AddImageOrVideoToQuestion}
alt="Overview of adding image or video to question"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Upload an image by clicking the upload icon or dragging the file:
<MdxImage
src={AddImageOrVideoToQuestionImage}
alt="Overview of adding image to question"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## How to Add Videos
Toggle to add a video via link:
<MdxImage
src={AddImageOrVideoToQuestionVideo}
alt="Overview of adding video to question"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
### Supported Video Platforms
We support YouTube, Vimeo, and Loom URLs.
<Note>**YouTube Privacy Mode**: This option reduces tracking by converting YouTube URLs to no-cookie URLs. It only works with YouTube.</Note>

View File

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

View File

@@ -160,6 +160,33 @@ const NavigationGroup = ({
const pathname = usePathname();
const [isActiveGroup, setIsActiveGroup] = useState<boolean>(false);
// We need to expand the group with the current link so we loop over all links
// Until we find the one and then expand the groups
useEffect(() => {
const findMatchingGroup = () => {
for (const group of navigation) {
for (const link of group.links) {
if (!link.children) continue;
const matchingChild = link.children.find((child) => pathname && child.href.startsWith(pathname));
if (matchingChild) {
setOpenGroups([`${group.title}-${link.title}`]);
setActiveGroup(group);
return;
}
}
}
};
findMatchingGroup();
return () => {
setOpenGroups([]);
setActiveGroup(null);
};
}, [pathname, setActiveGroup, setOpenGroups]);
useEffect(() => {
setIsActiveGroup(activeGroup?.title === group.title);
}, [activeGroup?.title, group.title]);
@@ -262,6 +289,25 @@ interface NavigationProps extends React.ComponentPropsWithoutRef<"nav"> {
export const Navigation = ({ isMobile, ...props }: NavigationProps) => {
const [activeGroup, setActiveGroup] = useState<NavGroup | null>(navigation[0]);
const [openGroups, setOpenGroups] = useState<string[]>([]);
const pathname = usePathname();
useEffect(() => {
// Check the current pathname and set the active group
navigation.forEach((group) => {
group.links.forEach((link) => {
if (link.href && pathname.startsWith(link.href)) {
setActiveGroup(group);
} else if (link.children) {
link.children.forEach((child) => {
if (pathname.startsWith(child.href)) {
setActiveGroup(group);
setOpenGroups([`${group.title}-${link.title}`]); // Ensure parent is open
}
});
}
});
});
}, [pathname]);
return (
<nav {...props}>

View File

@@ -19,6 +19,7 @@ export const navigation: Array<NavGroup> = [
{ title: "Docs Feedback", href: "/best-practices/docs-feedback" },
{ title: "Improve Email Content", href: "/best-practices/improve-email-content" },
{ title: "Contact Form", href: "/best-practices/contact-form" },
{ title: "Quiz Time", href: "/best-practices/quiz-time" },
],
},
],
@@ -49,6 +50,10 @@ export const navigation: Array<NavGroup> = [
{ title: "Verify Email before Survey", href: "/link-surveys/verify-email-before-survey" },
{ title: "PIN Protected Surveys", href: "/link-surveys/pin-protected-surveys" },
{ title: "Partial Submissions", href: "/global/partial-submissions" },
{
title: "Add Image/Video to Question",
href: "/global/add-image-or-video-question",
},
],
},
],
@@ -76,6 +81,10 @@ export const navigation: Array<NavGroup> = [
{ title: "Recall Functionality", href: "/global/recall" }, // global
{ title: "Partial Submissions", href: "/global/partial-submissions" }, // global
{ title: "Shareable Dashboards", href: "/global/shareable-dashboards" },
{
title: "Add Image/Video to Question",
href: "/global/add-image-or-video-question",
},
],
},
],

View File

@@ -2,9 +2,19 @@ import { slugifyWithCounter } from "@sindresorhus/slugify";
import * as acorn from "acorn";
import { toString } from "mdast-util-to-string";
import { mdxAnnotations } from "mdx-annotations";
import { getHighlighter, renderToHtml } from "shiki";
import { createCssVariablesTheme, createHighlighter } from "shiki";
import { visit } from "unist-util-visit";
let highlighterPromise;
const supportedLanguages = ["javascript", "html", "shell", "tsx", "json", "yml", "ts"];
const myTheme = createCssVariablesTheme({
name: "css-variables",
variablePrefix: "--shiki-",
variableDefaults: {},
fontStyle: true,
});
const rehypeParseCodeBlocks = () => {
return (tree) => {
visit(tree, "element", (node, _nodeIndex, parentNode) => {
@@ -15,29 +25,31 @@ const rehypeParseCodeBlocks = () => {
};
};
let highlighter;
const getHighlighter = async () => {
if (!highlighterPromise) {
highlighterPromise = createHighlighter({
langs: supportedLanguages,
themes: [myTheme],
})
}
return highlighterPromise;
}
const rehypeShiki = () => {
return async (tree) => {
highlighter = highlighter ?? (await getHighlighter({ theme: "css-variables" }));
const highlighter = await getHighlighter();
visit(tree, "element", (node) => {
if (node.tagName === "pre" && node.children[0]?.tagName === "code") {
let codeNode = node.children[0];
let textNode = codeNode.children[0];
if (!codeNode || !textNode) return;
node.properties.code = textNode.value;
if (node.properties.language) {
let tokens = highlighter.codeToThemedTokens(textNode.value, node.properties.language);
textNode.value = renderToHtml(tokens, {
elements: {
pre: ({ children }) => children,
code: ({ children }) => children,
line: ({ children }) => `<span>${children}</span>`,
},
});
if (codeNode.properties.className && codeNode.properties.className.length > 0) {
let lang = codeNode.properties.className[0].replace("language-", "");
const code = highlighter.codeToHtml(textNode.value, { lang, theme: "css-variables" });
textNode.value = code;
}
}
});

View File

@@ -57,7 +57,7 @@
"remark-mdx": "3.0.1",
"schema-dts": "1.1.2",
"sharp": "0.33.5",
"shiki": "0.14.7",
"shiki": "1.22.0",
"simple-functional-loader": "1.2.1",
"tailwindcss": "3.4.13",
"unist-util-filter": "5.0.1",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,6 +48,9 @@ export const AnimatedSurveyBg = ({ handleBgChange, background }: AnimatedSurveyB
"/animated-bgs/Thumbnails/36_Thumb.mp4": "/animated-bgs/4K/36_4k.mp4",
"/animated-bgs/Thumbnails/37_Thumb.mp4": "/animated-bgs/4K/37_4k.mp4",
"/animated-bgs/Thumbnails/38_Thumb.mp4": "/animated-bgs/4K/38_4k.mp4",
"/animated-bgs/Thumbnails/39_Thumb.mp4": "/animated-bgs/4K/39_4k.mp4",
"/animated-bgs/Thumbnails/40_Thumb.mp4": "/animated-bgs/4K/40_4k.mp4",
"/animated-bgs/Thumbnails/41_Thumb.mp4": "/animated-bgs/4K/41_4k.mp4",
};
const togglePlayback = (index: number, type: "play" | "pause") => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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