Compare commits
37 Commits
feature/fi
...
codeql2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95b73ee0de | ||
|
|
bee0ab07b4 | ||
|
|
0c37956943 | ||
|
|
74bd40e0ff | ||
|
|
31c3f9730e | ||
|
|
da8e7c1870 | ||
|
|
ac0ec1fbcd | ||
|
|
18be650561 | ||
|
|
5977fa6f80 | ||
|
|
1e2833b742 | ||
|
|
f18b030ebf | ||
|
|
ef454d8140 | ||
|
|
1ea391e45b | ||
|
|
b3e6e8d5d0 | ||
|
|
705f55176f | ||
|
|
297f349b45 | ||
|
|
91b6a9e008 | ||
|
|
189dc52ee9 | ||
|
|
280a9a439b | ||
|
|
bc844bbb1f | ||
|
|
ea2d6de9a7 | ||
|
|
e09ab1dcbe | ||
|
|
e1e04517a9 | ||
|
|
06026b6922 | ||
|
|
19a3faadce | ||
|
|
33543f59f8 | ||
|
|
47826a45aa | ||
|
|
6f043ec16e | ||
|
|
c2703788ae | ||
|
|
ef7df0fc77 | ||
|
|
cdb8199199 | ||
|
|
b0ded570ff | ||
|
|
5c0b29eed4 | ||
|
|
8e16d8daf6 | ||
|
|
deea760a17 | ||
|
|
f56f08e3c1 | ||
|
|
5daeab6554 |
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
100
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||
|
||||
22
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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}}"
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
4
.vscode/settings.json
vendored
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"extends": "expo/tsconfig.base"
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ module.exports = {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ All you need to do is copy a `<script>` tag to your HTML head, and that’s 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
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 16 KiB |
BIN
apps/docs/app/best-practices/contact-form/images/embed.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -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.
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.0 KiB |
@@ -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 doesn’t 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 doesn’t 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”:
|
||||
|
||||
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 462 B |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 16 KiB |
@@ -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! You’re ready to publish your survey 💃
|
||||
|
||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 19 KiB |
BIN
apps/docs/app/best-practices/quiz-time/conditional-logic.webp
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
apps/docs/app/best-practices/quiz-time/ending-logic.webp
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
178
apps/docs/app/best-practices/quiz-time/page.mdx
Normal 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 we’re 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, let’s 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, it’s 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! You’ve 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).
|
||||
|
||||
|
||||
BIN
apps/docs/app/best-practices/quiz-time/pass-fail.webp
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
apps/docs/app/best-practices/quiz-time/quiz.webp
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
apps/docs/app/best-practices/quiz-time/score.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
apps/docs/app/best-practices/quiz-time/single-select.webp
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
apps/docs/app/best-practices/quiz-time/when-then.webp
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
@@ -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
|
||||
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
45
apps/docs/app/global/add-image-or-video-question/page.mdx
Normal 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>
|
||||
@@ -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.
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" }]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 <head> 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,7 +46,8 @@ export const inviteOrganizationMemberAction = authenticatedActionClient
|
||||
ctx.user.name ?? "",
|
||||
"",
|
||||
true, // is onboarding invite
|
||||
parsedInput.inviteMessage
|
||||
parsedInput.inviteMessage,
|
||||
ctx.user.locale
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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") => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||