Compare commits

..

51 Commits

Author SHA1 Message Date
Johannes
38d21be5fa remove SurveyBg and PreviewSurveyDesktop 2023-11-27 13:30:57 +01:00
Neil Chauhan
5c47bd00d7 fix: merge conflict issue 2023-11-23 15:31:51 +05:30
Neil Chauhan
6091413d82 chore: refactor surveyBg and previewDesktopSurveyBg code into a single component 2023-11-23 15:06:47 +05:30
Anjy Gupta
d8e1e103ee Merge branch 'main' into surveyBg 2023-11-23 13:05:42 +05:30
Anjy Gupta
a063f920be Merge branch 'main' into surveyBg 2023-11-21 14:19:43 +05:30
Johannes
ab4c8838f9 Merge branch 'main' into surveyBg 2023-11-20 11:38:05 +01:00
Johannes
7d2fffbdab Merge main again 2023-11-19 16:34:22 +01:00
Johannes
b14d4e4235 Merge main 2023-11-19 16:33:24 +01:00
Johannes
52659b14ac Merge branch 'main' of github.com:formbricks/formbricks into surveyBg 2023-11-19 16:28:43 +01:00
Johannes
cbe610aa90 remove shadow from card 2023-11-19 16:27:23 +01:00
Johannes
d4a302fb20 refactor survey bg tabs 2023-11-19 14:18:56 +01:00
Johannes
28a33ee818 Merge branch 'main' into surveyBg 2023-11-19 14:00:10 +01:00
Johannes
870ba09abe fix merge conflicts 2023-11-19 13:58:39 +01:00
Johannes
39b11f9dba Merge branch 'main' of github.com:formbricks/formbricks into surveyBg 2023-11-19 13:56:00 +01:00
Anjy Gupta
71cfd31b7a Merge branch 'main' into surveyBg 2023-11-19 13:40:06 +05:30
Anjy Gupta
b47eeb4673 Update constants.ts 2023-11-17 01:47:48 +05:30
Anjy Gupta
708bfd8810 Merge branch 'main' into surveyBg 2023-11-17 01:46:46 +05:30
Anjy Gupta
e6e41b81a3 fix: tweaks 2023-11-15 19:21:02 +00:00
Anjy Gupta
e7d8a675c9 Merge branch 'main' into surveyBg 2023-11-15 19:05:21 +00:00
Johannes
12e4cc110c remove black bars 2023-11-15 10:29:44 +01:00
anjy7
fa7f7c497c fix: ux tweaks 2023-11-14 17:19:31 +05:30
anjy7
4066fbf7d6 fix: image bg file upload 2023-11-14 16:54:46 +05:30
Anjy Gupta
16245fe355 Merge branch 'main' into surveyBg 2023-11-14 14:19:53 +05:30
anjy7
057d2ec446 fix: tweaks 2023-11-14 02:33:41 +05:30
Anjy Gupta
9f80ed6ec2 fix: tweaks in ui 2023-11-13 20:29:12 +00:00
Anjy Gupta
90b0734cd2 fix: made ui changes 2023-11-13 20:02:05 +00:00
Anjy Gupta
6711543cdf fix: fixed overlay 2023-11-13 17:27:18 +00:00
Anjy Gupta
862e72ca8f Merge branch 'formbricks:main' into surveyBg 2023-11-13 20:40:14 +05:30
Anjy Gupta
14c249cbde Merge branch 'main' into surveyBg 2023-11-11 01:05:38 +05:30
Anjy Gupta
1bded4cce1 Merge branch 'main' into surveyBg 2023-11-09 15:07:05 +05:30
Anjy Gupta
bbc6dfbc97 Merge branch 'main' into surveyBg 2023-11-08 15:22:48 +05:30
Anjy Gupta
351ade6448 fix: made requested changes 2023-11-07 17:27:12 +00:00
Johannes
a530017c3b Merge branch 'main' into surveyBg 2023-11-07 09:49:35 +01:00
Anjy Gupta
0479c34be9 Merge branch 'main' into surveyBg 2023-11-04 00:01:27 +05:30
Anjy Gupta
e0e5ca93b6 Merge branch 'main' into surveyBg 2023-11-01 12:06:22 +05:30
Anjy Gupta
c80dde1ba1 Merge branch 'main' into surveyBg 2023-10-31 20:39:18 +05:30
anjy7
c9ba9e8e1b fix: fixed box overflow and width 2023-10-31 20:38:51 +05:30
Johannes
fd5284025f Merge branch 'main' into surveyBg 2023-10-30 21:45:20 +00:00
anjy7
2213adb24a fix: added overlay 2023-10-31 01:34:09 +05:30
Anjy Gupta
8d028cdf45 Merge branch 'main' into surveyBg 2023-10-30 23:03:58 +05:30
Anjy Gupta
e705fcf763 Merge branch 'main' into surveyBg 2023-10-30 21:30:09 +05:30
anjy7
7f65b41b59 fix: removed unnecessary changes 2023-10-30 19:17:42 +05:30
anjy7
0809055247 fix: added props interface to new components 2023-10-30 19:12:07 +05:30
anjy7
8546ce918c fix: added id to file input 2023-10-30 19:03:01 +05:30
anjy7
3cf1e7ad3b fix: added zod validation 2023-10-30 19:00:05 +05:30
anjy7
c7e2e08026 fix: made changes 2023-10-30 19:00:05 +05:30
Anjy Gupta
f39b15790b Merge branch 'main' into surveyBg 2023-10-30 18:44:41 +05:30
Anjy Gupta
aa9142b3c0 Merge branch 'main' into surveyBg 2023-10-30 14:33:40 +05:30
anjy7
ca634cd798 fix: added components 2023-10-30 14:30:10 +05:30
anjy7
f8518fcd80 fix: added animation 2023-10-29 20:24:27 +05:30
anjy7
2e2ad05367 feat: added change background feature 2023-10-29 12:34:40 +05:30
159 changed files with 1590 additions and 2456 deletions

View File

@@ -6,7 +6,6 @@ name: Docker
# documentation.
on:
workflow_dispatch:
push:
tags:
- "v*"
@@ -16,9 +15,6 @@ env:
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
jobs:
build:
@@ -31,16 +27,6 @@ jobs:
id-token: write
steps:
- name: Generate Random NEXTAUTH_SECRET
run: |
SECRET=$(openssl rand -hex 32)
echo "NEXTAUTH_SECRET=$SECRET" >> $GITHUB_ENV
- name: Generate Random ENCRYPTION_KEY
run: |
SECRET=$(openssl rand -hex 32)
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v3

View File

@@ -13,7 +13,7 @@
"dependencies": {
"@formbricks/js": "workspace:*",
"@heroicons/react": "^2.0.18",
"next": "14.0.3",
"next": "14.0.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},

View File

@@ -45,17 +45,14 @@ Adds an Actions for a given User by their User ID
curl --location --request POST 'https://app.formbricks.com/api/v1/client/<environment-id>/actions' \
--data-raw '{
"userId": "1",
"name": "new_action_v2",
"properties":{}
"name": "new_action_v2"
}'
```
```json {{ title: 'Example Request Body' }}
{
"userId": "1",
"name": "new_action_v3",
"properties":{}
"name": "new_action_v2"
}
```

View File

@@ -65,7 +65,12 @@ This set of API can be used to
```json {{title:'200 Success'}}
{
"data": {
"id": "clphzz6oo00083zdmc7e0nwzi"
"id": "clp83r8uy000ceyqcbld2ebwj",
"createdAt": "2023-11-21T08:57:23.866Z",
"updatedAt": "2023-11-21T08:57:23.866Z",
"surveyId": "cloqzeuu70000z8khcirufo60",
"responseId": null,
"personId": "cloo25v3e0000z8ptskh030jd"
}
}
```
@@ -76,7 +81,7 @@ This set of API can be used to
"message": "Fields are missing or incorrectly formatted",
"details": {
"surveyId": "Required"
}
}
}
```
</CodeGroup>
@@ -124,7 +129,14 @@ This set of API can be used to
```json {{title:'200 Success'}}
{
"data": {}
"data": {
"id": "clp83r8uy000ceyqcbld2ebwj",
"createdAt": "2023-11-21T08:57:23.866Z",
"updatedAt": "2023-11-21T09:05:27.285Z",
"surveyId": "cloqzeuu70000z8khcirufo60",
"responseId": null,
"personId": "cloo25v3e0000z8ptskh030jd"
}
}
```

View File

@@ -10,7 +10,7 @@ export const metadata = {
Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has different authentication requirements, and provides access to different data and settings.
Checkout the [API Key Setup](/docs/api/management/api-key-setup) - to generate, store, or delete API Keys.
Checkout the [API Key Setup](/docs/api/api-key-setup) - to generate, store, or delete API Keys.
## Public Client API
@@ -27,7 +27,7 @@ The Management API provides access to all data and settings that are visible in
API requests made to the Management API are authorized using a personal API key. This key grants the same rights and access as if you were logged in at formbricks.com. It's essential to keep your API key secure and not share it with others.
To generate, store, or delete an API key, follow the instructions provided on the following page [API Key](/docs/api/management/api-key-setup).
To generate, store, or delete an API key, follow the instructions provided on the following page [API Key](/docs/api/api-key-setup).
- [Action Class API](/docs/api/management/action-classes) - Create, Update, and Delete Action Classes
- [Attribute Class API](/docs/api/management/attribute-classes) - Create, Update, and Delete Attribute Classes

View File

@@ -26,6 +26,12 @@ This set of API can be used to
Create User with your own User ID
### Mandatory Request Body JSON Keys
<Properties>
<Property name="environmentId" type="string">
Environment to create a person in
</Property>
</Properties>
<Properties>
<Property name="userId" type="string">
User ID which you would like to identify the person with
@@ -42,6 +48,7 @@ This set of API can be used to
'https://app.formbricks.com/api/v1/client/<environment-id>/people' \
-H 'Content-Type: application/json' \
-d '{
"environmentId":"clonzr6vc0009z8md7y06hipl",
"userId":"docs_user"
}'
```
@@ -53,7 +60,15 @@ This set of API can be used to
```json {{title:'200 Success'}}
{
"data": {
"userId": "docs_user"
"status": "success",
"person": {
"id": "clp861gvu0000v7wzr950fqad",
"userId": "docs_user",
"attributes": {},
"environmentId": "clonzr6vc0009z8md7y06hipl",
"createdAt": "2023-11-21T10:01:20.058Z",
"updatedAt": "2023-11-21T10:01:20.058Z"
}
}
}
```
@@ -111,7 +126,16 @@ This set of API can be used to
```json {{title:'200 Success'}}
{
"data": {}
"data": {
"id": "clp868htl0003v7wzmy977m15",
"userId": "new_docs_user",
"attributes": {
"welcome_to": "formbricks"
},
"environmentId": "clonzr6vc0009z8md7y06hipl",
"createdAt": "2023-11-21T10:06:47.865Z",
"updatedAt": "2023-11-21T10:06:47.865Z"
}
}
```

View File

@@ -6,19 +6,15 @@ export const metadata = {
"Unlock the full potential of Formbricks' Responses API. From fetching to updating survey responses, our comprehensive guide helps you integrate and manage survey data efficiently without compromising security. Ideal for client-side interactions.",
};
#### Client API
#### Management API
# Responses API
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
This set of API can be used to
- [Create Response](#create-response)
- [Update Response](#update-response)
---
## Create Response {{ tag: 'POST', label: '/api/v1/client/<environment-id>/responses' }}
## Create a response {{ tag: 'POST', label: '/api/v1/client/responses' }}
Add a new response to a survey.
@@ -43,8 +39,8 @@ Add a new response to a survey.
### Optional Body Fields
<Properties>
<Property name="userId" type="string" required>
Pre-existing User ID to identify the user sending the response
<Property name="personId" type="string" required>
Internal Formbricks id to identify the user sending the response
</Property>
</Properties>
@@ -53,20 +49,20 @@ Add a new response to a survey.
| field name | required | default | description |
| ---------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| data | yes | - | The response data object (answers to the survey). In this object the key is the questionId, the value the answer of the user to this question. |
| userId | no | - | The person this response is connected to. |
| personId | no | - | The person this response is connected to. |
| surveyId | yes | - | The survey this response is connected to. |
| finished | yes | false | Mark a response as complete to be able to filter accordingly. |
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/client/<environment-id>/responses">
<CodeGroup title="Request" tag="POST" label="/api/v1/client/responses">
```bash {{ title: 'cURL' }}
curl --location --request POST 'https://app.formbricks.com/api/v1/client/<environment-id>/responses' \
curl --location --request POST 'https://app.formbricks.com/api/v1/client/responses' \
--data-raw '{
"surveyId":"cloqzeuu70000z8khcirufo60",
"userId": "1",
"surveyId":"clfqz1esd0000yzah51trddn8",
"personId": "clfqjny0v000ayzgsycx54a2c",
"finished": true,
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
@@ -77,8 +73,8 @@ Add a new response to a survey.
```json {{ title: 'Example Request Body' }}
{
"userId": "1",
"surveyId": "cloqzeuu70000z8khcirufo60",
"personId": "clfqjny0v000ayzgsycx54a2c",
"surveyId": "clfqz1esd0000yzah51trddn8",
"finished": true,
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
@@ -94,7 +90,19 @@ Add a new response to a survey.
```json {{ title: '200 Success' }}
{
"data": {
"id": "clp84xdld0002px36fkgue5ka",
"id": "clisyqeoi000219t52m5gopke",
"surveyId": "clfqz1esd0000yzah51trddn8",
"finished": true,
"person": {
"id": "clfqjny0v000ayzgsycx54a2c",
"attributes": {
"email": "me@johndoe.com"
}
},
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
}
}
}
```
@@ -116,7 +124,7 @@ Add a new response to a survey.
---
## Update Response {{ tag: 'PUT', label: '/api/v1/client/<environment-id>/responses/<response-id>' }}
## Update a response {{ tag: 'POST', label: '/api/v1/client/responses/<response-id>' }}
Update an existing response in a survey.
@@ -126,9 +134,6 @@ Update an existing response in a survey.
### Mandatory Body Fields
<Properties>
<Property name="finished" type="boolean">
Marks whether the response is complete or not.
</Property>
<Property name="data" type="string">
The data of the response as JSON object (key: questionId, value: answer).
</Property>
@@ -144,25 +149,27 @@ Update an existing response in a survey.
</Col>
<Col sticky>
<CodeGroup title="Request" tag="PUT" label="/api/v1/client/<environment-id>/responses/<response-id>">
<CodeGroup title="Request" tag="POST" label="/api/v1/client/responses/<response-id>">
```bash {{ title: 'cURL' }}
curl --location --request PUT 'https://app.formbricks.com/api/v1/client/<environment-id>/responses/<response-id>' \
curl --location --request POST 'https://app.formbricks.com/api/v1/client/responses/<response-id>' \
--data-raw '{
"finished":false,
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
"personId": "clfqjny0v000ayzgsycx54a2c",
"surveyId": "clfqz1esd0000yzah51trddn8",
"finished": true,
"data": {
"clggpvpvu0009n40g8ikawby8": 5,
}
}'
```
```json {{ title: 'Example Request Body' }}
{
"finished":false,
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
"personId": "clfqjny0v000ayzgsycx54a2c",
"surveyId": "clfqz1esd0000yzah51trddn8",
"finished": true,
"data": {
"clggpvpvu0009n40g8ikawby8": 5,
}
}
```
@@ -173,7 +180,22 @@ Update an existing response in a survey.
```json {{ title: '200 Success' }}
{
"data": {}
"data": {
"id": "clisyqeoi000219t52m5gopke",
"surveyId": "clfqz1esd0000yzah51trddn8",
"finished": true,
"person": {
"id": "clfqjny0v000ayzgsycx54a2c",
"attributes": {
"email": "me@johndoe.com"
}
},
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks",
"clggpvpvu0009n40g8ikawby8": 5
}
}
}
```

View File

@@ -40,7 +40,7 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
3. Connect to API
4. Test
## 1. Setting up Formbricks Cloud
### 1. Setting up Formbricks Cloud
1. To get started, create an account for the [Formbricks Cloud](https://app.formbricks.com/auth/signup).
@@ -74,7 +74,7 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
5. In the same way, you can change the Internal Question ID of the _Please elaborate_ question to **“additionalFeedback”** and the one of the _Page URL_ question to **“pageUrl”**.
<Note>
Answers need to be identical If you want different answers than “Yes 👍” and “No 👎” you need to update the
## Answers need to be identical If you want different answers than “Yes 👍” and “No 👎” you need to update the
choices accordingly. They have to be identical to the frontend we're building in the next step.
</Note>
@@ -108,10 +108,10 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
**Youre all setup in Formbricks Cloud for now 👍**
## 2. Build the frontend
### 2. Build the frontend
<Note>
Your frontend might work differently Your frontend likely looks and works differently. This is an example
## Your frontend might work differently Your frontend likely looks and works differently. This is an example
specific to our tech stack. We want to illustrate what you should consider building yours 😊
</Note>
@@ -311,7 +311,7 @@ return (
</Col>
## 3. Connecting to the Formbricks API
The last step is to hook up your sparkling new frontend to the Formbricks API. To do so, we followed the “[Create Response](/docs/api/client/responses#create-a-response)” and “[Update Response](/docs/api/client/responses#update-a-response)” pages in our docs.
The last step is to hook up your sparkling new frontend to the Formbricks API. To do so, we followed the “[Create Response](/docs/client-api/create-response)” and “[Update Response](/docs/client-api/update-response)” pages in our docs.
Here is the code for the `handleFeedbackSubmit` function with comments:
<Col>

View File

@@ -96,7 +96,7 @@ Click "Create a webhook":
className="rounded-lg max-w-full sm:max-w-3xl"
/>
Enter the Formbricks API key. Learn how to get one from the [API Key tutorial](/docs/api/management/api-key-setup).
Enter the Formbricks API key. Learn how to get one from the [API Key tutorial](/docs/api/api-key-setup).
<Image src={EnterApiKey} alt="Enter API Key" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />

View File

@@ -71,7 +71,7 @@ Click on Create New Credentail button to add your host and API Key
<Image src={AddApiKey} alt="Add host and api key" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
Now you need an API key. Please refer to the [API Key Setup](/docs/api/management/api-key-setup) page to learn how to create one.
Now you need an API key. Please refer to the [API Key Setup](/docs/api/api-key-setup) page to learn how to create one.
Once you copied it in the API Key field, hit Save button to test the connection and save the credentials.

View File

@@ -94,7 +94,7 @@ Now, you have to connect Zapier with Formbricks via an API Key:
className="rounded-lg max-w-full sm:max-w-3xl"
/>
Now you need an API key. Please refer to the [API Key Setup](/docs/api/management/api-key-setup) page to learn how to create one.
Now you need an API key. Please refer to the [API Key Setup](/docs/api/api-key-setup) page to learn how to create one.
Once you copied it in the newly opened Zapier window, you will be connected:

View File

@@ -3,14 +3,14 @@ import Image from "next/image";
import PeopleView from "./people-view.webp";
export const metadata = {
title: "Effective Identify Users in Formbricks Link Surveys",
title: "Effective User Identification in Formbricks Link Surveys",
description:
"Discover how to seamlessly connect responses from Formbricks link surveys to existing users in your database. Learn the intricacies of the userId URL parameter to enhance user tracking, profiling, and segmentation, ensuring more personalized interactions and data-driven decisions.",
};
#### Link Surveys
# Identify Users in Link Surveys
# User Identification in Link Surveys
Identifying users in link features lets you connect responses from link surveys with existing users in your Formbricks database.

View File

@@ -6,7 +6,7 @@ export const metadata = {
#### Self-Hosting
# Advanced Setup
# Running Formbricks with Docker
Quickly set up and start using Formbricks without getting into the technicalities of the build process. You'll be using an image that we've already built for you. The pre-built image is ready-to-run, and it only requires minimal configuration on your part. This approach is perfect for getting a functional instance of Formbricks up and running with minimal hassle. It's as easy as downloading the Docker image and firing up the container.

View File

@@ -1,13 +1,9 @@
export const metadata = {
title: "Configure Formbricks with External auth providers",
title: "External auth providers",
description:
"Set up and integrate multiple external authentication providers with Formbricks. Our step-by-step guide covers Google OAuth and more, ensuring a seamless login experience for your users.",
};
#### Self-Hosting
# Configure
## Google OAuth Authentication
Integrating Google OAuth with your Formbricks instance allows users to log in using their Google credentials, ensuring a secure and streamlined user experience. This guide will walk you through the process of setting up Google OAuth for your Formbricks instance.

View File

@@ -6,7 +6,7 @@ export const metadata = {
#### Self-Hosting
# One-Click Setup
# Deploying Formbricks to Production
If you want to quickly set up a production instance of Formbricks on a server running Ubuntu, we've got you covered! This method utilizes a convenient shell script that takes care of everything, including Docker, Postgres DB, and SSL certificate configuration. The shell script will automatically install all the required dependencies and configure your server, making the process a breeze.
@@ -29,7 +29,7 @@ Copy and paste the following command into your terminal:
<CodeGroup title="Single Command to deploy Formbricks">
```bash
curl -fsSL https://raw.githubusercontent.com/formbricks/formbricks/main/docker/production.sh -o formbricks.sh && chmod +x formbricks.sh && ./formbricks.sh install
/bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/formbricks/formbricks/main/docker/production.sh)"
```
</CodeGroup>
@@ -154,69 +154,6 @@ my.hosted.url.com
</CodeGroup>
</Col>
## Update Formbricks
To update Formbricks, simply run the following command:
<Col>
<CodeGroup title="Update Formbricks">
```bash
./formbricks.sh update
```
</CodeGroup>
</Col>
The script will automatically pull the latest version of Formbricks from Dockerhub and restart the containers.
## Stop Formbricks Instance
To stop Formbricks, simply run the following command:
<Col>
<CodeGroup title="Stop Formbricks">
```bash
./formbricks.sh stop
```
</CodeGroup>
</Col>
The script will automatically stop all the Formbricks related containers and brings the entire stack down.
## Restart Formbricks Instance
To restart Formbricks, simply run the following command:
<Col>
<CodeGroup title="Restart Formbricks">
```bash
./formbricks.sh restart
```
</CodeGroup>
</Col>
The script will automatically restart all the Formbricks related containers and brings the entire stack up with the previous configuration.
## Uninstall Formbricks
To uninstall Formbricks, simply run the following command, but keep in mind that this will delete all your data!
<Col>
<CodeGroup title="Uninstall Formbricks">
```bash
./formbricks.sh uninstall
```
</CodeGroup>
</Col>
The script will automatically stop all the Formbricks related containers, remove the Formbricks directory, and delete the Docker network.
## Debugging
If you encounter any issues, you can check the logs of the container with:
@@ -231,7 +168,6 @@ cd formbricks && docker compose logs -f
</Col>
You can close the logs again with `CTRL + C`.
## Troubleshooting
If you encounter any issues, consider the following steps:

View File

@@ -212,14 +212,6 @@ export const navigation: Array<NavGroup> = [
{ title: "Code Actions", href: "/docs/actions/code" },
],
},
{
title: "Link Surveys",
links: [
{ title: "Data Prefilling", href: "/docs/link-surveys/data-prefilling" },
{ title: "Identify Users", href: "/docs/link-surveys/user-identification" },
{ title: "Single Use Links", href: "/docs/link-surveys/single-use-links" },
],
},
{
title: "Best Practices",
links: [
@@ -242,14 +234,22 @@ export const navigation: Array<NavGroup> = [
{ title: "Zapier", href: "/docs/integrations/zapier" },
],
},
{
title: "Link Surveys",
links: [
{ title: "Data Prefilling", href: "/docs/link-surveys/data-prefilling" },
{ title: "User Identification", href: "/docs/link-surveys/user-identification" },
{ title: "Single Use Links", href: "/docs/link-surveys/single-use-links" },
],
},
{
title: "Self-hosting",
links: [
{ title: "Introduction", href: "/docs/self-hosting/deployment" },
{ title: "One-Click Setup", href: "/docs/self-hosting/production" },
{ title: "Advanced Setup", href: "/docs/self-hosting/docker" },
{ title: "Configure", href: "/docs/self-hosting/external-auth-providers" },
{ title: "Deployment", href: "/docs/self-hosting/deployment" },
{ title: "Production", href: "/docs/self-hosting/production" },
{ title: "Docker", href: "/docs/self-hosting/docker" },
{ title: "Migration Guide", href: "/docs/self-hosting/migration-guide" },
{ title: "External auth providers", href: "/docs/self-hosting/external-auth-providers" },
],
},
{

View File

@@ -1271,14 +1271,6 @@ export const templates: TTemplate[] = [
buttonUrl: "https://app.formbricks.com/auth/signup",
buttonExternal: true,
},
{
id: createId(),
type: TSurveyQuestionType.FileUpload,
headline: "Upload file",
required: false,
allowMultipleFiles: false,
maxSizeInMB: 10,
},
],
thankYouCard: thankYouCardDefault,
welcomeCard: {

View File

@@ -1,13 +1,6 @@
# Installer stage: Building the application
FROM node:18-alpine AS installer
RUN corepack enable && corepack prepare pnpm@latest --activate
# Install Supercronic (cron for containers without super user privileges)
RUN apk add --no-cache curl \
&& curl -fsSLo /tmp/supercronic \
"https://github.com/aptible/supercronic/releases/download/v0.2.27/supercronic-linux-amd64" \
&& chmod +x /tmp/supercronic
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL
@@ -22,42 +15,41 @@ WORKDIR /app
COPY . .
RUN touch /app/apps/web/.env
RUN pnpm install
# Build the project
RUN pnpm post-install --filter=web...
RUN pnpm turbo run build --filter=web...
# Runner stage: Setting up the runtime environment
FROM node:18-alpine AS runner
RUN corepack enable && corepack prepare pnpm@latest --activate
RUN apk add --no-cache curl \
# && addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
WORKDIR /home/nextjs
COPY --from=installer /tmp/supercronic /usr/local/bin/supercronic
COPY --from=installer /app/apps/web/next.config.mjs .
COPY --from=installer /app/apps/web/package.json .
# Leverage output traces to reduce image size
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/standalone ./
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migrations ./packages/database/migrations
COPY --from=installer /app/docker/cronjobs /app/docker/cronjobs
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
COPY --from=installer --chown=nextjs:nodejs /app/packages/database/schema.prisma ./packages/database/schema.prisma
COPY --from=installer --chown=nextjs:nodejs /app/packages/database/migrations ./packages/database/migrations
EXPOSE 3000
ENV HOSTNAME "0.0.0.0"
USER nextjs
CMD supercronic -quiet /app/docker/cronjobs & \
if [ "$NEXTAUTH_SECRET" != "RANDOM_STRING" ]; then \
pnpm dlx prisma migrate deploy && \
exec node apps/web/server.js; \
ENV HOSTNAME "0.0.0.0"
CMD if [ "$NEXTAUTH_SECRET" != "RANDOM_STRING" ]; then \
pnpm dlx prisma migrate deploy && node apps/web/server.js; \
else \
echo "ERROR: Please set a value for NEXTAUTH_SECRET in your docker compose variables!" >&2; \
echo "ERROR: Please set a value for NEXTAUTH_SECRET in your docker compose variables!"; \
exit 1; \
fi
fi

View File

@@ -8,7 +8,6 @@ import { createMembership } from "@formbricks/lib/membership/service";
import { createProduct } from "@formbricks/lib/product/service";
import { createShortUrl } from "@formbricks/lib/shortUrl/service";
import { canUserAccessSurvey, verifyUserRoleAccess } from "@formbricks/lib/survey/auth";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { deleteSurvey, duplicateSurvey, getSurvey } from "@formbricks/lib/survey/service";
import { createTeam, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { AuthenticationError, AuthorizationError, ResourceNotFoundError } from "@formbricks/types/errors";
@@ -206,11 +205,6 @@ export async function copyToOtherEnvironmentAction(
verifyEmail: existingSurvey.verifyEmail ?? prismaClient.JsonNull,
},
});
surveyCache.revalidate({
id: newSurvey.id,
environmentId: targetEnvironmentId,
});
return newSurvey;
}

View File

@@ -7,7 +7,7 @@ import WebhookLogo from "@/images/webhook.png";
import ZapierLogo from "@/images/zapier-small.png";
import { Card } from "@formbricks/ui/Card";
import Image from "next/image";
import { getWebhookCountBySource } from "@formbricks/lib/webhook/service";
import { getCountOfWebhooksBasedOnSource } from "@formbricks/lib/webhook/service";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
@@ -20,24 +20,13 @@ import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
export default async function IntegrationsPage({ params }) {
const environmentId = params.environmentId;
const [
environment,
integrations,
team,
session,
userWebhookCount,
zapierWebhookCount,
makeWebhookCount,
n8nwebhookCount,
] = await Promise.all([
const [environment, integrations, userWebhooks, zapierWebhooks, team, session] = await Promise.all([
getEnvironment(environmentId),
getIntegrations(environmentId),
getCountOfWebhooksBasedOnSource(environmentId, "user"),
getCountOfWebhooksBasedOnSource(environmentId, "zapier"),
getTeamByEnvironmentId(params.environmentId),
getServerSession(authOptions),
getWebhookCountBySource(environmentId, "user"),
getWebhookCountBySource(environmentId, "zapier"),
getWebhookCountBySource(environmentId, "make"),
getWebhookCountBySource(environmentId, "n8n"),
]);
if (!session) {
@@ -78,13 +67,9 @@ export default async function IntegrationsPage({ params }) {
label: "Zapier",
description: "Integrate Formbricks with 5000+ apps via Zapier",
icon: <Image src={ZapierLogo} alt="Zapier Logo" />,
connected: zapierWebhookCount > 0,
connected: zapierWebhooks > 0,
statusText:
zapierWebhookCount === 1
? "1 zap"
: zapierWebhookCount === 0
? "Not Connected"
: `${zapierWebhookCount} zaps`,
zapierWebhooks === 1 ? "1 zap" : zapierWebhooks === 0 ? "Not Connected" : `${zapierWebhooks} zaps`,
},
{
connectHref: `/environments/${params.environmentId}/integrations/webhooks`,
@@ -96,13 +81,9 @@ export default async function IntegrationsPage({ params }) {
label: "Webhooks",
description: "Trigger Webhooks based on actions in your surveys",
icon: <Image src={WebhookLogo} alt="Webhook Logo" />,
connected: userWebhookCount > 0,
connected: userWebhooks > 0,
statusText:
userWebhookCount === 1
? "1 webhook"
: userWebhookCount === 0
? "Not Connected"
: `${userWebhookCount} webhooks`,
userWebhooks === 1 ? "1 webhook" : userWebhooks === 0 ? "Not Connected" : `${userWebhooks} webhooks`,
},
{
connectHref: `/environments/${params.environmentId}/integrations/google-sheets`,
@@ -140,13 +121,6 @@ export default async function IntegrationsPage({ params }) {
label: "n8n",
description: "Integrate Formbricks with 350+ apps via n8n",
icon: <Image src={n8nLogo} alt="n8n Logo" />,
connected: n8nwebhookCount > 0,
statusText:
n8nwebhookCount === 1
? "1 integration"
: n8nwebhookCount === 0
? "Not Connected"
: `${n8nwebhookCount} integrations`,
},
{
docsHref: "https://formbricks.com/docs/integrations/make",
@@ -158,13 +132,6 @@ export default async function IntegrationsPage({ params }) {
label: "Make.com",
description: "Integrate Formbricks with 1000+ apps via Make",
icon: <Image src={MakeLogo} alt="Make Logo" />,
connected: makeWebhookCount > 0,
statusText:
makeWebhookCount === 1
? "1 integration"
: makeWebhookCount === 0
? "Not Connected"
: `${makeWebhookCount} integration`,
},
];

View File

@@ -10,7 +10,6 @@ import { useState } from "react";
import toast from "react-hot-toast";
import AddAPIKeyModal from "./AddApiKeyModal";
import { createApiKeyAction, deleteApiKeyAction } from "../actions";
import { FilesIcon } from "lucide-react";
export default function EditAPIKeys({
environmentTypeId,
@@ -60,24 +59,6 @@ export default function EditAPIKeys({
}
};
const ApiKeyDisplay = ({ apiKey }) => {
const copyToClipboard = () => {
navigator.clipboard.writeText(apiKey);
toast.success("API Key copied to clipboard");
};
if (!apiKey) {
return <span className="italic">secret</span>;
}
return (
<div className="flex items-center">
<span>{apiKey}</span>
<FilesIcon className="mx-2 h-4 w-4 cursor-pointer" onClick={copyToClipboard} />
</div>
);
};
return (
<>
<div className="mb-6 text-right">
@@ -91,9 +72,10 @@ export default function EditAPIKeys({
</Button>
</div>
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-10 content-center rounded-t-lg bg-slate-100 px-6 text-left text-sm font-semibold text-slate-900">
<div className="grid h-12 grid-cols-9 content-center rounded-t-lg bg-slate-100 px-6 text-left text-sm font-semibold text-slate-900">
<div className="col-span-4 sm:col-span-2">Label</div>
<div className="col-span-4 hidden sm:col-span-5 sm:block">API Key</div>
<div className="col-span-4 hidden sm:col-span-2 sm:block">API Key</div>
<div className="col-span-4 hidden sm:col-span-2 sm:block">Last used</div>
<div className="col-span-4 sm:col-span-2">Created at</div>
<div className=""></div>
</div>
@@ -106,11 +88,14 @@ export default function EditAPIKeys({
apiKeysLocal &&
apiKeysLocal.map((apiKey) => (
<div
className="grid h-12 w-full grid-cols-10 content-center rounded-lg px-6 text-left text-sm text-slate-900"
className="grid h-12 w-full grid-cols-9 content-center rounded-lg px-6 text-left text-sm text-slate-900"
key={apiKey.hashedKey}>
<div className="col-span-4 font-semibold sm:col-span-2">{apiKey.label}</div>
<div className="col-span-4 hidden sm:col-span-5 sm:block">
<ApiKeyDisplay apiKey={apiKey.apiKey} />
<div className="col-span-4 hidden sm:col-span-2 sm:block">
{apiKey.apiKey || <span className="italic">secret</span>}
</div>
<div className="col-span-4 hidden sm:col-span-2 sm:block">
{apiKey.lastUsedAt && timeSince(apiKey.lastUsedAt.toString())}
</div>
<div className="col-span-4 sm:col-span-2">{timeSince(apiKey.createdAt.toString())}</div>
<div className="col-span-1 text-center">

View File

@@ -34,7 +34,7 @@ export default async function ProfileSettingsPage({ params }) {
return !isViewer ? (
<div>
<SettingsTitle title="API Keys" />
<EnvironmentNotice environmentId={environment.id} subPageUrl="/settings/api-keys" />
<EnvironmentNotice environmentId={environment.id} />
{environment.type === "development" ? (
<SettingsCard
title="Development Env Keys"

View File

@@ -41,7 +41,7 @@ export default async function SettingsLayout({ children, params }) {
membershipRole={currentUserMembership?.role}
/>
<div className="w-full md:ml-64">
<div className="max-w-4xl px-20 pb-6 pt-14 md:pt-6">
<div className="max-w-4xl px-6 pb-6 pt-14 md:pt-6">
<div>{children}</div>
</div>
</div>

View File

@@ -51,12 +51,12 @@ export const handleFileUpload = async (
const { signature, timestamp, uuid } = signingData;
requestHeaders = {
"X-File-Type": file.type,
"X-File-Name": encodeURIComponent(file.name),
"X-Environment-ID": environmentId ?? "",
"X-Signature": signature,
"X-Timestamp": String(timestamp),
"X-UUID": uuid,
fileType: file.type,
fileName: file.name,
environmentId: environmentId ?? "",
signature,
timestamp,
uuid,
};
}

View File

@@ -10,7 +10,7 @@ export default async function ProfileSettingsPage({ params }) {
<>
<div className="space-y-4">
<SettingsTitle title="Setup Checklist" />
<EnvironmentNotice environmentId={params.environmentId} subPageUrl="/settings/setup" />
<EnvironmentNotice environmentId={params.environmentId} />
<SettingsCard
title="Widget Status"
description="Check if the Formbricks widget is alive and kicking.">

View File

@@ -1,112 +0,0 @@
import Headline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/Headline";
import { questionTypes } from "@/app/lib/questions";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { timeSince } from "@formbricks/lib/time";
import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
import { TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
import { PersonAvatar } from "@formbricks/ui/Avatars";
import { InboxStackIcon } from "@heroicons/react/24/solid";
import { DownloadIcon, FileIcon } from "lucide-react";
import Link from "next/link";
interface FileUploadSummaryProps {
questionSummary: TSurveyQuestionSummary<TSurveyFileUploadQuestion>;
environmentId: string;
}
export default function FileUploadSummary({ questionSummary, environmentId }: FileUploadSummaryProps) {
const questionTypeInfo = questionTypes.find((type) => type.id === questionSummary.question.type);
return (
<div className="rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
<div className="space-y-2 px-4 pb-5 pt-6 md:px-6">
<Headline headline={questionSummary.question.headline} required={questionSummary.question.required} />
<div className="flex space-x-2 text-xs font-semibold text-slate-600 md:text-sm">
<div className="flex items-center rounded-lg bg-slate-100 p-2 ">
{questionTypeInfo && <questionTypeInfo.icon className="mr-2 h-4 w-4 " />}
{questionTypeInfo ? questionTypeInfo.label : "Unknown Question Type"} Question
</div>
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
<InboxStackIcon className="mr-2 h-4 w-4" />
{questionSummary.responses.length} Responses
</div>
</div>
</div>
<div className="rounded-b-lg bg-white ">
<div className="grid h-10 grid-cols-4 items-center border-y border-slate-200 bg-slate-100 text-sm font-bold text-slate-600">
<div className="pl-4 md:pl-6">User</div>
<div className="col-span-2 pl-4 md:pl-6">Response</div>
<div className="px-4 md:px-6">Time</div>
</div>
{questionSummary.responses.map((response) => {
const displayIdentifier = response.person ? getPersonIdentifier(response.person) : null;
return (
<div
key={response.id}
className="grid grid-cols-4 items-center border-b border-slate-100 py-2 text-sm text-slate-800 md:text-base">
<div className="pl-4 md:pl-6">
{response.person ? (
<Link
className="ph-no-capture group flex items-center"
href={`/environments/${environmentId}/people/${response.person.id}`}>
<div className="hidden md:flex">
<PersonAvatar personId={response.person.id} />
</div>
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{displayIdentifier}
</p>
</Link>
) : (
<div className="group flex items-center">
<div className="hidden md:flex">
<PersonAvatar personId="anonymous" />
</div>
<p className="break-all text-slate-600 md:ml-2">Anonymous</p>
</div>
)}
</div>
<div className="col-span-2 grid">
{response.value === "skipped" && (
<div className="flex w-full flex-col items-center justify-center p-2">
<p className="mt-2 text-sm font-semibold text-slate-500 dark:text-slate-400">skipped</p>
</div>
)}
{Array.isArray(response.value) &&
(response.value.length > 0 ? (
response.value.map((fileUrl, index) => (
<div className="relative m-2 rounded-lg bg-slate-200">
<a href={fileUrl as string} key={index} download target="_blank">
<div className="absolute right-0 top-0 m-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
<DownloadIcon className="h-6 text-slate-500" />
</div>
</div>
</a>
<div className="flex flex-col items-center justify-center p-2">
<FileIcon className="h-6 text-slate-500" />
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
{fileUrl.split("/").pop()}
</p>
</div>
</div>
))
) : (
<div className="flex w-full flex-col items-center justify-center p-2">
<p className="mt-2 text-sm font-semibold text-slate-500 dark:text-slate-400">skipped</p>
</div>
))}
</div>
<div className="px-4 text-slate-500 md:px-6">{timeSince(response.updatedAt.toISOString())}</div>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -3,11 +3,7 @@ import ConsentSummary from "@/app/(app)/environments/[environmentId]/surveys/[su
import HiddenFieldsSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import type {
TSurveyFileUploadQuestion,
TSurveyPictureSelectionQuestion,
TSurveyQuestionSummary,
} from "@formbricks/types/surveys";
import type { TSurveyPictureSelectionQuestion, TSurveyQuestionSummary } from "@formbricks/types/surveys";
import { TEnvironment } from "@formbricks/types/environment";
import { TResponse } from "@formbricks/types/responses";
import {
@@ -26,8 +22,7 @@ import MultipleChoiceSummary from "./MultipleChoiceSummary";
import NPSSummary from "./NPSSummary";
import OpenTextSummary from "./OpenTextSummary";
import RatingSummary from "./RatingSummary";
import FileUploadSummary from "./FileUploadSummary";
import PictureChoiceSummary from "./PictureChoiceSummary";
import PictureChoiceSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary";
interface SummaryListProps {
environment: TEnvironment;
@@ -127,15 +122,6 @@ export default function SummaryList({ environment, survey, responses, responsesP
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.FileUpload) {
return (
<FileUploadSummary
key={questionSummary.question.id}
questionSummary={questionSummary as TSurveyQuestionSummary<TSurveyFileUploadQuestion>}
environmentId={environment.id}
/>
);
}
if (questionSummary.question.type === TSurveyQuestionType.PictureSelection) {
return (
<PictureChoiceSummary

View File

@@ -3,11 +3,11 @@
import { AuthenticationError } from "@formbricks/types/errors";
import { Button } from "@formbricks/ui/Button";
import CodeBlock from "@formbricks/ui/CodeBlock";
import LoadingSpinner from "@formbricks/ui/LoadingSpinner";
import { CodeBracketIcon, DocumentDuplicateIcon, EnvelopeIcon } from "@heroicons/react/24/solid";
import { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { getEmailHtmlAction, sendEmailAction } from "../../actions";
import LoadingSpinner from "@formbricks/ui/LoadingSpinner";
interface EmailTabProps {
surveyId: string;

View File

@@ -60,7 +60,6 @@ export default function LinkTab({ surveyUrl, survey, brandColor }: EmailTabProps
autoFocus={false}
isRedirectDisabled={false}
key={survey.id}
onFileUpload={async () => ""}
/>
<Button

View File

@@ -0,0 +1,107 @@
import { TSurvey } from "@formbricks/types/surveys";
import { useState } from "react";
interface AnimatedSurveyBgProps {
localSurvey?: TSurvey;
handleBgChange: (bg: string, bgType: string) => void;
}
export default function AnimatedSurveyBg({ localSurvey, handleBgChange }: AnimatedSurveyBgProps) {
const [color, setColor] = useState(localSurvey?.surveyBackground?.bg || "#ffff");
const [hoveredVideo, setHoveredVideo] = useState<number | null>(null);
const animationFiles = {
"/animated-bgs/Thumbnails/1_Thumb.mp4": "/animated-bgs/4K/1_4k.mp4",
"/animated-bgs/Thumbnails/2_Thumb.mp4": "/animated-bgs/4K/2_4k.mp4",
"/animated-bgs/Thumbnails/3_Thumb.mp4": "/animated-bgs/4K/3_4k.mp4",
"/animated-bgs/Thumbnails/4_Thumb.mp4": "/animated-bgs/4K/4_4k.mp4",
"/animated-bgs/Thumbnails/5_Thumb.mp4": "/animated-bgs/4K/5_4k.mp4",
"/animated-bgs/Thumbnails/6_Thumb.mp4": "/animated-bgs/4K/6_4k.mp4",
"/animated-bgs/Thumbnails/7_Thumb.mp4": "/animated-bgs/4K/7_4k.mp4",
"/animated-bgs/Thumbnails/8_Thumb.mp4": "/animated-bgs/4K/8_4k.mp4",
"/animated-bgs/Thumbnails/9_Thumb.mp4": "/animated-bgs/4K/9_4k.mp4",
"/animated-bgs/Thumbnails/10_Thumb.mp4": "/animated-bgs/4K/10_4k.mp4",
"/animated-bgs/Thumbnails/11_Thumb.mp4": "/animated-bgs/4K/11_4k.mp4",
"/animated-bgs/Thumbnails/12_Thumb.mp4": "/animated-bgs/4K/12_4k.mp4",
"/animated-bgs/Thumbnails/13_Thumb.mp4": "/animated-bgs/4K/13_4k.mp4",
"/animated-bgs/Thumbnails/14_Thumb.mp4": "/animated-bgs/4K/14_4k.mp4",
"/animated-bgs/Thumbnails/15_Thumb.mp4": "/animated-bgs/4K/15_4k.mp4",
"/animated-bgs/Thumbnails/16_Thumb.mp4": "/animated-bgs/4K/16_4k.mp4",
"/animated-bgs/Thumbnails/17_Thumb.mp4": "/animated-bgs/4K/17_4k.mp4",
"/animated-bgs/Thumbnails/18_Thumb.mp4": "/animated-bgs/4K/18_4k.mp4",
"/animated-bgs/Thumbnails/19_Thumb.mp4": "/animated-bgs/4K/19_4k.mp4",
"/animated-bgs/Thumbnails/20_Thumb.mp4": "/animated-bgs/4K/20_4k.mp4",
"/animated-bgs/Thumbnails/21_Thumb.mp4": "/animated-bgs/4K/21_4k.mp4",
"/animated-bgs/Thumbnails/22_Thumb.mp4": "/animated-bgs/4K/22_4k.mp4",
"/animated-bgs/Thumbnails/23_Thumb.mp4": "/animated-bgs/4K/23_4k.mp4",
"/animated-bgs/Thumbnails/24_Thumb.mp4": "/animated-bgs/4K/24_4k.mp4",
"/animated-bgs/Thumbnails/25_Thumb.mp4": "/animated-bgs/4K/25_4k.mp4",
"/animated-bgs/Thumbnails/26_Thumb.mp4": "/animated-bgs/4K/26_4k.mp4",
"/animated-bgs/Thumbnails/27_Thumb.mp4": "/animated-bgs/4K/27_4k.mp4",
"/animated-bgs/Thumbnails/28_Thumb.mp4": "/animated-bgs/4K/28_4k.mp4",
"/animated-bgs/Thumbnails/29_Thumb.mp4": "/animated-bgs/4K/29_4k.mp4",
"/animated-bgs/Thumbnails/30_Thumb.mp4": "/animated-bgs/4K/30_4k.mp4",
};
const handleMouseEnter = (index: number) => {
setHoveredVideo(index);
playVideo(index);
};
const handleMouseLeave = (index: number) => {
setHoveredVideo(null);
pauseVideo(index);
};
// Function to play the video
const playVideo = (index: number) => {
const video = document.getElementById(`video-${index}`) as HTMLVideoElement;
if (video) {
video.play();
}
};
// Function to pause the video
const pauseVideo = (index: number) => {
const video = document.getElementById(`video-${index}`) as HTMLVideoElement;
if (video) {
video.pause();
}
};
const handleBg = (x: string) => {
setColor(x);
handleBgChange(x, "animation");
};
return (
<div>
<div className="mt-4 grid grid-cols-6 gap-4">
{Object.keys(animationFiles).map((key, index) => {
const value = animationFiles[key];
return (
<div
key={index}
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={() => handleMouseLeave(index)}
onClick={() => handleBg(value)}
className="relative cursor-pointer overflow-hidden rounded-lg">
<video
disablePictureInPicture
id={`video-${index}`}
autoPlay={hoveredVideo === index}
className="h-46 w-96 origin-center scale-105 transform">
<source src={`${key}`} type="video/mp4" />
</video>
<input
className="absolute right-2 top-2 h-4 w-4 rounded-sm bg-white "
type="checkbox"
checked={color === value}
onChange={() => handleBg(value)}
/>
</div>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import { TSurvey } from "@formbricks/types/surveys";
import { ColorPicker } from "@formbricks/ui/ColorPicker";
import { useState } from "react";
interface ColorSurveyBgBgProps {
localSurvey?: TSurvey;
handleBgChange: (bg: string, bgType: string) => void;
colours: string[];
}
export default function ColorSurveyBg({ localSurvey, handleBgChange, colours }: ColorSurveyBgBgProps) {
const [color, setColor] = useState(localSurvey?.surveyBackground?.bg || "#ffff");
const handleBg = (x: string) => {
setColor(x);
handleBgChange(x, "color");
};
return (
<div>
<div className="w-full max-w-xs py-2">
<ColorPicker color={color} onChange={handleBg} />
</div>
<div className="grid grid-cols-4 gap-4 md:grid-cols-5 xl:grid-cols-8 2xl:grid-cols-10">
{colours.map((x) => {
return (
<div
className={`h-16 w-16 cursor-pointer rounded-lg ${
color === x ? "border-4 border-slate-500" : ""
}`}
key={x}
style={{ backgroundColor: `${x}` }}
onClick={() => handleBg(x)}></div>
);
})}
</div>
</div>
);
}

View File

@@ -1,234 +0,0 @@
"use client";
import { useGetBillingInfo } from "@formbricks/lib/team/hooks/useGetBillingInfo";
import { TAllowedFileExtension, ZAllowedFileExtension } from "@formbricks/types/common";
import { TProduct } from "@formbricks/types/product";
import { TSurvey, TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { PlusIcon, TrashIcon, XCircleIcon } from "@heroicons/react/24/solid";
import { useMemo, useState } from "react";
import { toast } from "react-hot-toast";
interface FileUploadFormProps {
localSurvey: TSurvey;
product?: TProduct;
question: TSurveyFileUploadQuestion;
questionIdx: number;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
lastQuestion: boolean;
isInValid: boolean;
}
export default function FileUploadQuestionForm({
question,
questionIdx,
updateQuestion,
isInValid,
product,
}: FileUploadFormProps): JSX.Element {
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
const [extension, setExtension] = useState("");
const {
billingInfo,
error: billingInfoError,
isLoading: billingInfoLoading,
} = useGetBillingInfo(product?.teamId ?? "");
const handleInputChange = (event) => {
setExtension(event.target.value);
};
const addExtension = (event) => {
event.preventDefault();
event.stopPropagation();
let modifiedExtension = extension.trim();
// Remove the dot at the start if it exists
if (modifiedExtension.startsWith(".")) {
modifiedExtension = modifiedExtension.substring(1);
}
if (!modifiedExtension) {
toast.error("Please enter a file extension.");
return;
}
const parsedExtensionResult = ZAllowedFileExtension.safeParse(modifiedExtension);
if (!parsedExtensionResult.success) {
toast.error("This file type is not supported.");
return;
}
if (question.allowedFileExtensions) {
if (!question.allowedFileExtensions.includes(modifiedExtension as TAllowedFileExtension)) {
updateQuestion(questionIdx, {
allowedFileExtensions: [...question.allowedFileExtensions, modifiedExtension],
});
setExtension("");
} else {
toast.error("This extension is already added.");
}
} else {
updateQuestion(questionIdx, { allowedFileExtensions: [modifiedExtension] });
setExtension("");
}
};
const removeExtension = (event, index: number) => {
event.preventDefault();
if (question.allowedFileExtensions) {
const updatedExtensions = [...question?.allowedFileExtensions];
updatedExtensions.splice(index, 1);
updateQuestion(questionIdx, { allowedFileExtensions: updatedExtensions });
}
};
const maxSizeInMBLimit = useMemo(() => {
if (billingInfoError || billingInfoLoading || !billingInfo) {
return 10;
}
if (billingInfo.features.linkSurvey.status === "active") {
// 1GB in MB
return 1024;
}
return 10;
}, [billingInfo, billingInfoError, billingInfoLoading]);
return (
<form>
<div className="mt-3">
<Label htmlFor="headline">Question</Label>
<div className="mt-2">
<Input
autoFocus
id="headline"
name="headline"
value={question.headline}
onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })}
isInvalid={isInValid && question.headline.trim() === ""}
/>
</div>
</div>
<div className="mt-3">
{showSubheader && (
<>
<Label htmlFor="subheader">Description</Label>
<div className="mt-2 inline-flex w-full items-center">
<Input
id="subheader"
name="subheader"
value={question.subheader}
onChange={(e) => updateQuestion(questionIdx, { subheader: e.target.value })}
/>
<TrashIcon
className="ml-2 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => {
setShowSubheader(false);
updateQuestion(questionIdx, { subheader: "" });
}}
/>
</div>
</>
)}
{!showSubheader && (
<Button size="sm" variant="minimal" type="button" onClick={() => setShowSubheader(true)}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
</Button>
)}
</div>
<div className="mb-8 mt-6 space-y-6">
<AdvancedOptionToggle
isChecked={question.allowMultipleFiles}
onToggle={() => updateQuestion(questionIdx, { allowMultipleFiles: !question.allowMultipleFiles })}
htmlId="allowMultipleFile"
title="Allow Multiple Files"
description="Let people upload up to 10 files at the same time."
childBorder
customContainerClass="p-0"></AdvancedOptionToggle>
<AdvancedOptionToggle
isChecked={!!question.maxSizeInMB}
onToggle={(checked) => updateQuestion(questionIdx, { maxSizeInMB: checked ? 10 : undefined })}
htmlId="maxFileSize"
title="Max file size"
description="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
<Input
autoFocus
type="number"
id="fileSizeLimit"
value={question.maxSizeInMB}
onChange={(e) => {
const parsedValue = parseInt(e.target.value, 10);
if (parsedValue > maxSizeInMBLimit) {
toast.error(`Max file size limit is ${maxSizeInMBLimit} MB`);
updateQuestion(questionIdx, { maxSizeInMB: maxSizeInMBLimit });
return;
}
updateQuestion(questionIdx, { maxSizeInMB: parseInt(e.target.value, 10) });
}}
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
/>
MB
</p>
</label>
</AdvancedOptionToggle>
<AdvancedOptionToggle
isChecked={!!question.allowedFileExtensions}
onToggle={(checked) =>
updateQuestion(questionIdx, { allowedFileExtensions: checked ? [] : undefined })
}
htmlId="limitFileType"
title="Limit file types"
description="Control which file types can be uploaded."
childBorder
customContainerClass="p-0">
<div className="p-4">
<div className="flex flex-row flex-wrap gap-2">
{question.allowedFileExtensions &&
question.allowedFileExtensions.map((item, index) => (
<div className="mb-2 flex h-8 items-center space-x-2 rounded-full bg-slate-200 px-2">
<p className="text-sm text-slate-800">{item}</p>
<Button
className="inline-flex px-0"
variant="minimal"
onClick={(e) => removeExtension(e, index)}>
<XCircleIcon className="h-4 w-4" />
</Button>
</div>
))}
</div>
<div className="flex items-center">
<Input
autoFocus
className="mr-2 w-20 rounded-md bg-white placeholder:text-sm"
placeholder=".pdf"
value={extension}
onChange={handleInputChange}
type="text"
/>
<Button size="sm" variant="secondary" onClick={(e) => addExtension(e)}>
Allow file type
</Button>
</div>
</div>
</AdvancedOptionToggle>
</div>
</form>
);
}

View File

@@ -162,10 +162,7 @@ const validateHiddenField = (
return "Question already exists";
}
// no key words -- userId & suid & existing question ids
if (
["userId", "source", "suid", "end", "start", "welcomeCard", "hidden"].includes(field) ||
existingQuestions.findIndex((q) => q.id === field) !== -1
) {
if (["userId", "suid"].includes(field) || existingQuestions.findIndex((q) => q.id === field) !== -1) {
return "Question not allowed";
}
// no spaced words --> should be valid query param on url

View File

@@ -0,0 +1,42 @@
import FileInput from "@formbricks/ui/FileInput";
import { TSurvey } from "@formbricks/types/surveys";
interface ImageSurveyBgBgProps {
localSurvey?: TSurvey;
handleBgChange: (url: string, bgType: string) => void;
}
export default function ImageSurveyBg({ localSurvey, handleBgChange }: ImageSurveyBgBgProps) {
const isUrl = (str: string) => {
try {
new URL(str);
return true;
} catch (error) {
return false;
}
};
const fileUrl = isUrl(localSurvey?.surveyBackground?.bg ?? "")
? localSurvey?.surveyBackground?.bg ?? ""
: "";
return (
<div className="mb-2 mt-4 w-full rounded-lg border bg-slate-50 p-4">
<div className="flex w-full items-center justify-center">
<FileInput
id="survey-bg-file-input"
allowedFileExtensions={["png", "jpeg", "jpg"]}
environmentId={localSurvey?.environmentId}
onFileUpload={(url: string[]) => {
if (url.length > 0) {
handleBgChange(url[0], "image");
} else {
handleBgChange("#ffff", "color");
}
}}
fileUrl={fileUrl}
/>
</div>
</div>
);
}

View File

@@ -81,7 +81,6 @@ export default function LogicEditor({
cta: ["clicked", "skipped"],
consent: ["skipped", "accepted"],
pictureSelection: ["submitted", "skipped"],
fileUpload: ["uploaded", "notUploaded"],
};
const logicConditions: LogicConditions = {
@@ -100,16 +99,6 @@ export default function LogicEditor({
values: null,
unique: true,
},
uploaded: {
label: "has uploaded file",
values: null,
unique: true,
},
notUploaded: {
label: "has not uploaded file",
values: null,
unique: true,
},
clicked: {
label: "is clicked",
values: null,
@@ -253,7 +242,7 @@ export default function LogicEditor({
<SelectContent>
{conditions[question.type].map(
(condition) =>
!(question.required && (condition === "skipped" || condition === "notUploaded")) && (
!(question.required && condition === "skipped") && (
<SelectItem
key={condition}
value={condition}

View File

@@ -18,13 +18,11 @@ import {
PresentationChartBarIcon,
QueueListIcon,
StarIcon,
ArrowUpTrayIcon,
PhotoIcon,
} from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
import { Draggable } from "react-beautiful-dnd";
import FileUploadQuestionForm from "./FileUploadQuestionForm";
import CTAQuestionForm from "./CTAQuestionForm";
import ConsentQuestionForm from "./ConsentQuestionForm";
import MultipleChoiceMultiForm from "./MultipleChoiceMultiForm";
@@ -34,11 +32,9 @@ import OpenQuestionForm from "./OpenQuestionForm";
import QuestionDropdown from "./QuestionMenu";
import RatingQuestionForm from "./RatingQuestionForm";
import PictureSelectionForm from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/PictureSelectionForm";
import { TProduct } from "@formbricks/types/product";
interface QuestionCardProps {
localSurvey: TSurvey;
product?: TProduct;
questionIdx: number;
moveQuestion: (questionIndex: number, up: boolean) => void;
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
@@ -78,7 +74,6 @@ export function BackButtonInput({
export default function QuestionCard({
localSurvey,
product,
questionIdx,
moveQuestion,
updateQuestion,
@@ -128,9 +123,7 @@ export default function QuestionCard({
<div>
<div className="inline-flex">
<div className="-ml-0.5 mr-3 h-6 w-6 text-slate-400">
{question.type === TSurveyQuestionType.FileUpload ? (
<ArrowUpTrayIcon />
) : question.type === TSurveyQuestionType.OpenText ? (
{question.type === TSurveyQuestionType.OpenText ? (
<ChatBubbleBottomCenterTextIcon />
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
<QueueListIcon />
@@ -243,16 +236,6 @@ export default function QuestionCard({
lastQuestion={lastQuestion}
isInValid={isInValid}
/>
) : question.type === TSurveyQuestionType.FileUpload ? (
<FileUploadQuestionForm
localSurvey={localSurvey}
product={product}
question={question}
questionIdx={questionIdx}
updateQuestion={updateQuestion}
lastQuestion={lastQuestion}
isInValid={isInValid}
/>
) : null}
<div className="mt-4">
<Collapsible.Root open={openAdvanced} onOpenChange={setOpenAdvanced} className="mt-5">

View File

@@ -202,7 +202,6 @@ export default function QuestionsView({
<QuestionCard
key={internalQuestionIdMap[question.id]}
localSurvey={localSurvey}
product={product}
questionIdx={questionIdx}
moveQuestion={moveQuestion}
updateQuestion={updateQuestion}

View File

@@ -18,6 +18,7 @@ interface SettingsViewProps {
attributeClasses: TAttributeClass[];
responseCount: number;
membershipRole?: TMembershipRole;
colours: string[];
}
export default function SettingsView({
@@ -28,6 +29,7 @@ export default function SettingsView({
attributeClasses,
responseCount,
membershipRole,
colours,
}: SettingsViewProps) {
return (
<div className="mt-12 space-y-3 p-5">
@@ -60,7 +62,7 @@ export default function SettingsView({
environmentId={environment.id}
/>
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} colours={colours} />
</div>
);
}

View File

@@ -9,18 +9,28 @@ import { CheckCircleIcon } from "@heroicons/react/24/solid";
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
import Placement from "./Placement";
import SurveyBgSelectorTab from "./SurveyBgSelectorTab";
interface StylingCardProps {
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
colours: string[];
}
export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCardProps) {
export default function StylingCard({ localSurvey, setLocalSurvey, colours }: StylingCardProps) {
const [open, setOpen] = useState(false);
const { type, productOverwrites } = localSurvey;
const { type, productOverwrites, surveyBackground } = localSurvey;
const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } =
productOverwrites ?? {};
const { bg, bgType, brightness } = surveyBackground ?? {};
const [inputValue, setInputValue] = useState(100);
const handleInputChange = (e) => {
setInputValue(e.target.value);
handleBrightnessChange(parseInt(e.target.value));
};
const togglePlacement = () => {
setLocalSurvey({
@@ -42,6 +52,28 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
});
};
const toggleBackgroundColor = () => {
setLocalSurvey({
...localSurvey,
surveyBackground: {
...localSurvey.surveyBackground,
bg: !!bg ? undefined : "#ffff",
bgType: !!bg ? undefined : "color",
},
});
};
const toggleBrightness = () => {
setLocalSurvey({
...localSurvey,
surveyBackground: {
...localSurvey.surveyBackground,
brightness: !!brightness ? undefined : 100,
},
});
setInputValue(100);
};
const toggleHighlightBorderColor = () => {
setLocalSurvey({
...localSurvey,
@@ -62,6 +94,29 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
});
};
const handleBgChange = (color: string, type: string) => {
setInputValue(100);
setLocalSurvey({
...localSurvey,
surveyBackground: {
...localSurvey.surveyBackground,
bg: color,
bgType: type,
brightness: brightness !== undefined ? 100 : brightness,
},
});
};
const handleBrightnessChange = (percent: number) => {
setLocalSurvey({
...localSurvey,
surveyBackground: {
...localSurvey.surveyBackground,
brightness: percent,
},
});
};
const handleBorderColorChange = (color: string) => {
setLocalSurvey({
...localSurvey,
@@ -143,6 +198,66 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
</div>
)}
</div>
{type == "link" && (
<>
{/* Background */}
<div className="p-3">
<div className="ml-2 flex items-center space-x-1">
<Switch id="autoCompleteBg" checked={!!bg} onCheckedChange={toggleBackgroundColor} />
<Label htmlFor="autoCompleteBg" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Change Background</h3>
<p className="text-xs font-normal text-slate-500">
Pick a background from our library or upload your own.
</p>
</div>
</Label>
</div>
{bg && (
<SurveyBgSelectorTab
localSurvey={localSurvey}
handleBgChange={handleBgChange}
colours={colours}
bgType={bgType}
/>
)}
</div>
{/* Overlay */}
<div className="p-3">
<div className="ml-2 flex items-center space-x-1">
<Switch
id="autoCompleteOverlay"
checked={!!brightness}
onCheckedChange={toggleBrightness}
/>
<Label htmlFor="autoCompleteOverlay" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Background Overlay</h3>
<p className="text-xs font-normal text-slate-500">
Darken or lighten background of your choice.
</p>
</div>
</Label>
</div>
{brightness && (
<div>
<div className="mt-4 flex flex-col justify-center rounded-lg border bg-slate-50 p-4 px-8">
<h3 className="mb-4 text-sm font-semibold text-slate-700">Transparency</h3>
<input
id="small-range"
type="range"
min="1"
max="200"
value={inputValue}
onChange={handleInputChange}
className="range-sm mb-6 h-1 w-full cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700"
/>
</div>
</div>
)}
</div>
</>
)}
{/* positioning */}
{type !== "link" && (
<div className="p-3 ">
@@ -184,9 +299,9 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
/>
<Label htmlFor="autoComplete" className="cursor-pointer">
<div className="ml-2">
<h3 className="text-sm font-semibold text-slate-700">Overwrite Highlight Border</h3>
<h3 className="text-sm font-semibold text-slate-700">Overwrite border highlight</h3>
<p className="text-xs font-normal text-slate-500">
Change the highlight border for this survey.
Change the border highlight for this survey.
</p>
</div>
</Label>

View File

@@ -0,0 +1,61 @@
import { TSurvey } from "@formbricks/types/surveys";
import { useState } from "react";
import AnimatedSurveyBg from "./AnimatedSurveyBg";
import ColorSurveyBg from "./ColorSurveyBg";
import ImageSurveyBg from "./ImageSurveyBg";
interface SurveyBgSelectorTabProps {
localSurvey: TSurvey;
handleBgChange: (bg: string, bgType: string) => void;
colours: string[];
bgType: string | null | undefined;
}
const TabButton = ({ isActive, onClick, children }) => (
<button
className={`w-1/4 rounded-md p-2 text-sm font-medium leading-none text-slate-800 ${
isActive ? "bg-white shadow-sm" : ""
}`}
onClick={onClick}>
{children}
</button>
);
export default function SurveyBgSelectorTab({
localSurvey,
handleBgChange,
colours,
bgType,
}: SurveyBgSelectorTabProps) {
const [tab, setTab] = useState(bgType || "image");
const renderContent = () => {
switch (tab) {
case "image":
return <ImageSurveyBg localSurvey={localSurvey} handleBgChange={handleBgChange} />;
case "animation":
return <AnimatedSurveyBg localSurvey={localSurvey} handleBgChange={handleBgChange} />;
case "color":
return <ColorSurveyBg localSurvey={localSurvey} handleBgChange={handleBgChange} colours={colours} />;
default:
return null;
}
};
return (
<div className="mt-4 flex flex-col items-center justify-center rounded-lg border bg-slate-50 p-4 px-8">
<div className="flex w-full items-center justify-between rounded-lg border border-slate-300 bg-slate-50 px-6 py-1.5">
<TabButton isActive={tab === "image"} onClick={() => setTab("image")}>
Image
</TabButton>
<TabButton isActive={tab === "animation"} onClick={() => setTab("animation")}>
Animation
</TabButton>
<TabButton isActive={tab === "color"} onClick={() => setTab("color")}>
Color
</TabButton>
</div>
{renderContent()}
</div>
);
}

View File

@@ -23,6 +23,7 @@ interface SurveyEditorProps {
attributeClasses: TAttributeClass[];
responseCount: number;
membershipRole?: TMembershipRole;
colours: string[];
}
export default function SurveyEditor({
@@ -33,6 +34,7 @@ export default function SurveyEditor({
attributeClasses,
responseCount,
membershipRole,
colours,
}: SurveyEditorProps): JSX.Element {
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
@@ -99,6 +101,7 @@ export default function SurveyEditor({
attributeClasses={attributeClasses}
responseCount={responseCount}
membershipRole={membershipRole}
colours={colours}
/>
)}
</main>
@@ -110,7 +113,6 @@ export default function SurveyEditor({
product={product}
environment={environment}
previewType={localSurvey.type === "web" ? "modal" : "fullwidth"}
onFileUpload={async (file) => file.name}
/>
</aside>
</div>

View File

@@ -40,10 +40,10 @@ export default function UpdateQuestionId({
updateQuestion(questionIdx, { id: prevValue });
toast.error("ID should not be empty.");
return;
} else if (["userId", "source", "suid", "end", "start", "welcomeCard", "hidden"].includes(currentValue)) {
} else if (currentValue === "source" || currentValue === "suID" || currentValue === "userId") {
setCurrentValue(prevValue);
updateQuestion(questionIdx, { id: prevValue });
toast.error("Reserved words cannot be used as question ID");
toast.error("ID cannot used reserved words.");
return;
} else {
setIsInputInvalid(false);

View File

@@ -12,6 +12,7 @@ import { getSurvey } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { getServerSession } from "next-auth";
import { colours } from "@formbricks/lib/constants";
import SurveyEditor from "./components/SurveyEditor";
export const generateMetadata = async ({ params }) => {
@@ -67,6 +68,7 @@ export default async function SurveysEditPage({ params }) {
attributeClasses={attributeClasses}
responseCount={responseCount}
membershipRole={currentUserMembership?.role}
colours={colours}
/>
</>
);

View File

@@ -56,7 +56,7 @@ export default function Modal({
ref={modalRef}
style={highlightBorderColorStyle}
className={cn(
"pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm overflow-y-auto rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out",
"pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm overflow-y-auto rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out ",
previewMode === "desktop" ? getPlacementStyle(placement) : "max-w-full ",
slidingAnimationClass
)}>

View File

@@ -1,13 +1,14 @@
"use client";
import Modal from "@/app/(app)/environments/[environmentId]/surveys/components/Modal";
import PreviewSurveyBgMobile from "@/app/(app)/environments/[environmentId]/surveys/components/PreviewSurveyBgMobile";
import TabOption from "@/app/(app)/environments/[environmentId]/surveys/components/TabOption";
import { SurveyInline } from "@formbricks/ui/Survey";
import MediaBackground from "@/app/s/[surveyId]/components/MediaBackground";
import type { TEnvironment } from "@formbricks/types/environment";
import type { TProduct } from "@formbricks/types/product";
import { TSurvey } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { SurveyInline } from "@formbricks/ui/Survey";
import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline";
import {
ArrowsPointingInIcon,
@@ -17,7 +18,6 @@ import {
} from "@heroicons/react/24/solid";
import { Variants, motion } from "framer-motion";
import { useEffect, useRef, useState } from "react";
import { TUploadFileConfig } from "@formbricks/types/storage";
type TPreviewType = "modal" | "fullwidth" | "email";
@@ -28,7 +28,6 @@ interface PreviewSurveyProps {
previewType?: TPreviewType;
product: TProduct;
environment: TEnvironment;
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
}
let surveyNameTemp;
@@ -66,7 +65,6 @@ export default function PreviewSurvey({
previewType,
product,
environment,
onFileUpload,
}: PreviewSurveyProps) {
const [isModalOpen, setIsModalOpen] = useState(true);
const [isFullScreenPreview, setIsFullScreenPreview] = useState(false);
@@ -155,6 +153,20 @@ export default function PreviewSurvey({
setActiveQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id);
}
function animationTrigger() {
let storePreviewMode = previewMode;
setPreviewMode("null");
setTimeout(() => {
setPreviewMode(storePreviewMode);
}, 10);
}
useEffect(() => {
if (survey.surveyBackground?.bgType === "animation") {
animationTrigger();
}
}, [survey.surveyBackground?.bg]);
useEffect(() => {
if (environment && environment.widgetSetupCompleted) {
setWidgetSetupCompleted(true);
@@ -194,7 +206,7 @@ export default function PreviewSurvey({
<div className="absolute right-0 top-0 m-2">
<ResetProgressButton resetQuestionProgress={resetQuestionProgress} />
</div>
<div className="relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-[3rem] border-8 border-slate-500 bg-slate-400">
<PreviewSurveyBgMobile survey={survey} ContentRef={ContentRef}>
{/* below element is use to create notch for the mobile device mockup */}
<div className="absolute left-1/2 right-1/2 top-0 z-20 h-4 w-1/2 -translate-x-1/2 transform rounded-b-md bg-slate-500"></div>
{previewType === "modal" ? (
@@ -210,28 +222,20 @@ export default function PreviewSurvey({
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
isRedirectDisabled={true}
onFileUpload={onFileUpload}
/>
</Modal>
) : (
<div
className="absolute top-0 z-10 flex h-full w-full flex-grow flex-col overflow-y-auto"
ref={ContentRef}>
<div className="flex w-full flex-grow flex-col items-center justify-center bg-white py-6">
<div className="w-full max-w-md px-4">
<SurveyInline
survey={survey}
brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
onFileUpload={onFileUpload}
/>
</div>
</div>
<div className="relative z-10 w-full max-w-md px-4">
<SurveyInline
survey={survey}
brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
/>
</div>
)}
</div>
</PreviewSurveyBgMobile>
</>
)}
{previewMode === "desktop" && (
@@ -282,25 +286,21 @@ export default function PreviewSurvey({
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
isRedirectDisabled={true}
onFileUpload={onFileUpload}
/>
</Modal>
) : (
<div className="flex flex-grow flex-col overflow-y-auto rounded-b-lg" ref={ContentRef}>
<div className="flex w-full flex-grow flex-col items-center justify-center bg-white p-4 py-6">
<div className="w-full max-w-md">
<SurveyInline
survey={survey}
brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
isRedirectDisabled={true}
onFileUpload={onFileUpload}
/>
</div>
<MediaBackground survey={survey} ContentRef={ContentRef} isEditorView>
<div className="z-0 w-full max-w-md rounded-lg p-4">
<SurveyInline
survey={survey}
brandColor={brandColor}
activeQuestionId={activeQuestionId || undefined}
isBrandingEnabled={product.linkSurveyBranding}
onActiveQuestionChange={setActiveQuestionId}
isRedirectDisabled={true}
/>
</div>
</div>
</MediaBackground>
)}
</div>
)}

View File

@@ -0,0 +1,75 @@
export default function PreviewSurveyBgMobile({ children, survey, ContentRef }) {
return survey.surveyBackground && survey.surveyBackground.bgType === "color" ? (
<div className="relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-[3rem] border-8 border-slate-500">
<div
className="absolute top-0 z-10 flex h-full w-full flex-grow flex-col"
ref={ContentRef}
style={{
backgroundColor: survey.surveyBackground.bg,
filter: survey.surveyBackground?.brightness
? `brightness(${survey.surveyBackground.brightness}%)`
: "none",
}}></div>
<div className="absolute flex h-full w-full items-center justify-center overflow-y-auto">
{children}
</div>
</div>
) : survey.surveyBackground && survey.surveyBackground.bgType === "animation" ? (
<div className="relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-[3rem] border-8 border-slate-500">
<div
className="absolute top-0 z-10 flex h-full w-full flex-grow flex-col overflow-y-auto"
ref={ContentRef}>
<div className="relative flex w-full flex-grow flex-col items-center justify-center">
<div className="absolute inset-0 h-full w-full object-cover">
<video
muted
loop
autoPlay
className="h-full w-full object-cover"
style={{
width: "100%",
height: "100%",
filter: survey.surveyBackground?.brightness
? `brightness(${survey.surveyBackground.brightness}%)`
: "none",
}}>
<source src={survey.surveyBackground.bg} type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
{children}
</div>
</div>
</div>
) : survey.surveyBackground && survey.surveyBackground.bgType === "image" ? (
<div className="relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-[3rem] border-8 border-slate-500">
<div
className="absolute top-0 z-10 flex h-full w-full flex-grow flex-col overflow-y-auto"
ref={ContentRef}
style={{
backgroundImage: `url(${survey.surveyBackground.bg})`,
filter: survey.surveyBackground?.brightness
? `brightness(${survey.surveyBackground.brightness}%)`
: "none",
}}></div>
<div className="absolute flex h-full w-full items-center justify-center overflow-y-auto">
{children}
</div>
</div>
) : (
<div className="relative h-[90%] max-h-[40rem] w-80 overflow-hidden rounded-[3rem] border-8 border-slate-500">
<div
className="absolute top-0 z-10 flex h-full w-full flex-grow flex-col"
ref={ContentRef}
style={{
backgroundColor: "#ffff",
filter: survey.surveyBackground?.brightness
? `brightness(${survey.surveyBackground.brightness}%)`
: "none",
}}></div>
<div className="absolute flex h-full w-full items-center justify-center overflow-y-auto">
{children}
</div>
</div>
);
}

View File

@@ -77,7 +77,6 @@ export default function TemplateContainerWithPreview({
product={product}
environment={environment}
setActiveQuestionId={setActiveQuestionId}
onFileUpload={async (file) => file.name}
/>
</div>
)}

View File

@@ -1,9 +1,5 @@
import {
TSurvey,
TSurveyHiddenFields,
TSurveyQuestionType,
TSurveyWelcomeCard,
} from "@formbricks/types/surveys";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import { TSurvey, TSurveyHiddenFields, TSurveyWelcomeCard } from "@formbricks/types/surveys";
import { TTemplate } from "@formbricks/types/templates";
import { createId } from "@paralleldrive/cuid2";
@@ -2482,7 +2478,7 @@ export const customSurvey: TTemplate = {
{
id: createId(),
type: TSurveyQuestionType.OpenText,
headline: "What would you like to know?",
headline: "Custom Survey",
subheader: "This is an example survey.",
placeholder: "Type your answer here...",
required: true,

View File

@@ -17,7 +17,7 @@ import Role from "./Role";
const MAX_STEPS = 6;
interface OnboardingProps {
session: Session;
session: Session | null;
environmentId: string;
profile: TProfile;
product: TProduct;
@@ -88,12 +88,7 @@ export default function Onboarding({ session, environmentId, profile, product }:
<Greeting next={next} skip={doLater} name={profile.name ? profile.name : ""} session={session} />
)}
{currentStep === 2 && (
<Role
next={next}
skip={skipStep}
setFormbricksResponseId={setFormbricksResponseId}
session={session}
/>
<Role next={next} skip={skipStep} setFormbricksResponseId={setFormbricksResponseId} />
)}
{currentStep === 3 && (
<Objective

View File

@@ -5,7 +5,6 @@ import { createResponse, formbricksEnabled } from "@/app/lib/formbricks";
import { cn } from "@formbricks/lib/cn";
import { env } from "@formbricks/lib/env.mjs";
import { Button } from "@formbricks/ui/Button";
import { Session } from "next-auth";
import { useEffect, useRef, useState } from "react";
import { toast } from "react-hot-toast";
import { handleTabNavigation } from "../utils";
@@ -14,7 +13,6 @@ type RoleProps = {
next: () => void;
skip: () => void;
setFormbricksResponseId: (id: string) => void;
session: Session;
};
type RoleChoice = {
@@ -22,7 +20,7 @@ type RoleChoice = {
id: "project_manager" | "engineer" | "founder" | "marketing_specialist" | "other";
};
const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, session }) => {
const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId }) => {
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
const [isUpdating, setIsUpdating] = useState(false);
const fieldsetRef = useRef<HTMLFieldSetElement>(null);
@@ -57,7 +55,7 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, sessio
console.error(e);
}
if (formbricksEnabled && env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID) {
const res = await createResponse(env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID, session.user.id, {
const res = await createResponse(env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID, {
role: selectedRole.label,
});
if (res.ok) {

View File

@@ -48,7 +48,7 @@ export const SigninForm = ({
try {
const signInResponse = await signIn("credentials", {
callbackUrl: searchParams?.get("callbackUrl") || "/",
email: data.email.toLowerCase(),
email: data.email,
password: data.password,
...(totpLogin && { totpCode: data.totpCode }),
...(totpBackup && { backupCode: data.backupCode }),

View File

@@ -7,7 +7,7 @@ const VerificationPageSchema = z.string().email();
export default function VerificationPage(params) {
const email = params.searchParams.email;
try {
const parsedEmail = VerificationPageSchema.parse(email).toLowerCase();
const parsedEmail = VerificationPageSchema.parse(email);
return (
<FormWrapper>
<>

View File

@@ -1,17 +0,0 @@
import { responses } from "@/app/lib/api/response";
import { CRON_SECRET } from "@formbricks/lib/constants";
import { captureTelemetry } from "@formbricks/lib/telemetry";
import { headers } from "next/headers";
export async function POST() {
const headersList = headers();
const apiKey = headersList.get("x-api-key");
if (!apiKey || apiKey !== CRON_SECRET) {
return responses.notAuthenticatedResponse();
}
captureTelemetry("ping");
return responses.successResponse({}, true);
}

View File

@@ -19,8 +19,10 @@ export async function POST(request: NextRequest) {
const ws = xlsx.utils.json_to_sheet(json, { header: fields });
xlsx.utils.book_append_sheet(wb, ws, "Sheet1");
const buffer = xlsx.write(wb, { type: "buffer", bookType: "xlsx" }) as Buffer;
const base64String = buffer.toString("base64");
const buffer = xlsx.write(wb, { type: "buffer", bookType: "xlsx" });
const binaryString = String.fromCharCode.apply(null, buffer);
const base64String = btoa(binaryString);
const headers = new Headers();
headers.set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

View File

@@ -7,7 +7,7 @@ import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { TJsStateSync, ZJsPeopleAttributeInput } from "@formbricks/types/js";
import { TJsState, ZJsPeopleAttributeInput } from "@formbricks/types/js";
import { NextResponse } from "next/server";
interface Context {
@@ -80,8 +80,8 @@ export async function POST(req: Request, context: Context): Promise<NextResponse
}
// return state
const state: TJsStateSync = {
person: { id: person.id, userId: person.userId },
const state: TJsState = {
person,
surveys,
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
product,

View File

@@ -14,8 +14,15 @@ export async function POST(_: Request, { params }: { params: { displayId: string
}
try {
await markDisplayRespondedLegacy(displayId);
return responses.successResponse({}, true);
const display = await markDisplayRespondedLegacy(displayId);
return responses.successResponse(
{
...display,
createdAt: display.createdAt.toISOString(),
updatedAt: display.updatedAt.toISOString(),
},
true
);
} catch (error) {
return responses.internalServerErrorResponse(error.message);
}

View File

@@ -72,5 +72,12 @@ export async function POST(request: Request): Promise<NextResponse> {
console.warn("Posthog capture not possible. No team owner found");
}
return responses.successResponse({ id: display.id }, true);
return responses.successResponse(
{
...display,
createdAt: display.createdAt.toISOString(),
updatedAt: display.updatedAt.toISOString(),
},
true
);
}

View File

@@ -7,7 +7,7 @@ import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { TJsStateSync, ZJsPeopleAttributeInput } from "@formbricks/types/js";
import { TJsState, ZJsPeopleAttributeInput } from "@formbricks/types/js";
import { NextResponse } from "next/server";
interface Context {
@@ -79,8 +79,8 @@ export async function POST(req: Request, context: Context): Promise<NextResponse
}
// return state
const state: TJsStateSync = {
person: { id: person.id, userId: person.userId },
const state: TJsState = {
person,
surveys,
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
product,

View File

@@ -83,5 +83,5 @@ export async function PUT(
response: response,
});
}
return responses.successResponse({}, true);
return responses.successResponse(response, true);
}

View File

@@ -105,5 +105,5 @@ export async function POST(request: Request): Promise<NextResponse> {
console.warn("Posthog capture not possible. No team owner found");
}
return responses.successResponse({ id: response.id }, true);
return responses.successResponse(response, true);
}

View File

@@ -6,7 +6,6 @@ import { personCache } from "@formbricks/lib/person/cache";
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { ZJsPeopleLegacyAttributeInput } from "@formbricks/types/js";
import { TPersonClient } from "@formbricks/types/people";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
@@ -67,15 +66,7 @@ export async function POST(req: Request, { params }): Promise<NextResponse> {
const state = await getUpdatedState(environmentId, personId);
let person: TPersonClient | null = null;
if (state.person && "id" in state.person && "userId" in state.person) {
person = {
id: state.person.id,
userId: state.person.userId,
};
}
return responses.successResponse({ ...state, person }, true);
return responses.successResponse({ ...state }, true);
} catch (error) {
console.error(error);
return responses.internalServerErrorResponse(`Unable to complete request: ${error.message}`, true);

View File

@@ -31,13 +31,8 @@ export async function POST(req: Request): Promise<NextResponse> {
person = await createPerson(environmentId, userId);
}
const personClient = {
id: person.id,
userId: person.userId,
};
const state = await getUpdatedState(environmentId, person.id);
return responses.successResponse({ ...state, person: personClient }, true);
return responses.successResponse({ ...state }, true);
} catch (error) {
console.error(error);
return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);

View File

@@ -23,9 +23,9 @@ export async function POST(req: NextRequest) {
}
try {
await createPerson(environmentId, userId);
const person = await createPerson(environmentId, userId);
return responses.successResponse({}, true);
return responses.successResponse({ status: "success", person }, true);
} catch (err) {
return responses.internalServerErrorResponse("Something went wrong", true);
}

View File

@@ -2,7 +2,6 @@ import { getUpdatedState } from "@/app/api/v1/(legacy)/js/sync/lib/sync";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { ZJsSyncLegacyInput } from "@formbricks/types/js";
import { TPersonClient } from "@formbricks/types/people";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
@@ -28,15 +27,7 @@ export async function POST(req: Request): Promise<NextResponse> {
const state = await getUpdatedState(environmentId, personId);
let person: TPersonClient | null = null;
if (state.person && "id" in state.person && "userId" in state.person) {
person = {
id: state.person.id,
userId: state.person.userId,
};
}
return responses.successResponse({ ...state, person }, true);
return responses.successResponse({ ...state }, true);
} catch (error) {
console.error(error);
return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);

View File

@@ -32,8 +32,8 @@ export async function PUT(request: Request, context: Context): Promise<NextRespo
}
try {
await updateDisplay(displayId, inputValidation.data);
return responses.successResponse({}, true);
const display = await updateDisplay(displayId, inputValidation.data);
return responses.successResponse(display, true);
} catch (error) {
console.error(error);
return responses.internalServerErrorResponse(error.message, true);

View File

@@ -3,7 +3,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { createDisplay } from "@formbricks/lib/display/service";
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
import { ZDisplayCreateInput } from "@formbricks/types/displays";
import { TDisplay, ZDisplayCreateInput } from "@formbricks/types/displays";
import { InvalidInputError } from "@formbricks/types/errors";
import { NextResponse } from "next/server";
@@ -34,12 +34,11 @@ export async function POST(request: Request, context: Context): Promise<NextResp
// find teamId & teamOwnerId from environmentId
const teamDetails = await getTeamDetails(inputValidation.data.environmentId);
let response = {};
// create display
let display: TDisplay;
try {
const { id } = await createDisplay(inputValidation.data);
response = { id };
display = await createDisplay(inputValidation.data);
} catch (error) {
if (error instanceof InvalidInputError) {
return responses.badRequestResponse(error.message);
@@ -55,5 +54,12 @@ export async function POST(request: Request, context: Context): Promise<NextResp
console.warn("Posthog capture not possible. No team owner found");
}
return responses.successResponse(response, true);
return responses.successResponse(
{
...display,
createdAt: display.createdAt.toISOString(),
updatedAt: display.updatedAt.toISOString(),
},
true
);
}

View File

@@ -9,7 +9,7 @@ import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { getMonthlyActiveTeamPeopleCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { TEnvironment } from "@formbricks/types/environment";
import { TJsStateSync, ZJsPeopleUserIdInput } from "@formbricks/types/js";
import { TJsState, ZJsPeopleUserIdInput } from "@formbricks/types/js";
import { NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
@@ -101,8 +101,8 @@ export async function GET(
}
// return state
const state: TJsStateSync = {
person: { id: person.id, userId: person.userId },
const state: TJsState = {
person,
surveys,
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
product,

View File

@@ -4,7 +4,7 @@ import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getSurveys } from "@formbricks/lib/survey/service";
import { TJsStateSync, ZJsPublicSyncInput } from "@formbricks/types/js";
import { TJsState, ZJsPublicSyncInput } from "@formbricks/types/js";
import { NextRequest, NextResponse } from "next/server";
export async function OPTIONS(): Promise<NextResponse> {
@@ -51,7 +51,7 @@ export async function GET(
throw new Error("Product not found");
}
const state: TJsStateSync = {
const state: TJsState = {
surveys: surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web"),
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
product,

View File

@@ -1,11 +1,6 @@
import { responses } from "@/app/lib/api/response";
import { createPerson } from "@formbricks/lib/person/service";
import { NextRequest } from "next/server";
interface Context {
params: {
environmentId: string;
};
}
export async function OPTIONS() {
// cors headers
@@ -13,11 +8,11 @@ export async function OPTIONS() {
return responses.successResponse({}, true);
}
export async function POST(req: NextRequest, context: Context) {
export async function POST(req: NextRequest) {
// we need to create a new person
// call the createPerson service from here
const environmentId = context.params.environmentId;
const { userId } = await req.json();
const { environmentId, userId } = await req.json();
if (!environmentId) {
return responses.badRequestResponse("environmentId is required", { environmentId }, true);
@@ -28,9 +23,9 @@ export async function POST(req: NextRequest, context: Context) {
}
try {
await createPerson(environmentId, userId);
const person = await createPerson(environmentId, userId);
return responses.successResponse({ userId }, true);
return responses.successResponse({ status: "success", person }, true);
} catch (err) {
return responses.internalServerErrorResponse("Something went wrong", true);
}

View File

@@ -91,5 +91,5 @@ export async function PUT(
response: response,
});
}
return responses.successResponse({}, true);
return responses.successResponse(response, true);
}

View File

@@ -122,5 +122,5 @@ export async function POST(request: Request, context: Context): Promise<NextResp
console.warn("Posthog capture not possible. No team owner found");
}
return responses.successResponse({ id: response.id }, true);
return responses.successResponse(response, true);
}

View File

@@ -17,39 +17,25 @@ interface Context {
};
}
export async function OPTIONS(): Promise<NextResponse> {
return NextResponse.json(
{},
{
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers":
"Content-Type, Authorization, X-File-Name, X-File-Type, X-Survey-ID, X-Signature, X-Timestamp, X-UUID",
},
}
);
}
export async function POST(req: NextRequest, context: Context): Promise<NextResponse> {
const environmentId = context.params.environmentId;
const accessType = "private"; // private files are accessible only by authorized users
const headersList = headers();
const fileType = headersList.get("X-File-Type");
const encodedFileName = headersList.get("X-File-Name");
const surveyId = headersList.get("X-Survey-ID");
const fileType = headersList.get("fileType");
const fileName = headersList.get("fileName");
const surveyId = headersList.get("surveyId");
const signedSignature = headersList.get("X-Signature");
const signedUuid = headersList.get("X-UUID");
const signedTimestamp = headersList.get("X-Timestamp");
const signedSignature = headersList.get("signature");
const signedUuid = headersList.get("uuid");
const signedTimestamp = headersList.get("timestamp");
if (!fileType) {
return responses.badRequestResponse("contentType is required");
}
if (!encodedFileName) {
if (!fileName) {
return responses.badRequestResponse("fileName is required");
}
@@ -79,8 +65,6 @@ export async function POST(req: NextRequest, context: Context): Promise<NextResp
return responses.notFoundResponse("TeamByEnvironmentId", environmentId);
}
const fileName = decodeURIComponent(encodedFileName);
// validate signature
const validated = validateLocalSignedUrl(

View File

@@ -10,10 +10,6 @@ interface Context {
};
}
export async function OPTIONS(): Promise<NextResponse> {
return responses.successResponse({}, true);
}
// api endpoint for uploading private files
// uploaded files will be private, only the user who has access to the environment can access the file
// uploading private files requires no authentication

View File

@@ -16,19 +16,19 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
const accessType = "public"; // public files are accessible by anyone
const headersList = headers();
const fileType = headersList.get("X-File-Type");
const encodedFileName = headersList.get("X-File-Name");
const environmentId = headersList.get("X-Environment-ID");
const fileType = headersList.get("fileType");
const fileName = headersList.get("fileName");
const environmentId = headersList.get("environmentId");
const signedSignature = headersList.get("X-Signature");
const signedUuid = headersList.get("X-UUID");
const signedTimestamp = headersList.get("X-Timestamp");
const signedSignature = headersList.get("signature");
const signedUuid = headersList.get("uuid");
const signedTimestamp = headersList.get("timestamp");
if (!fileType) {
return responses.badRequestResponse("fileType is required");
}
if (!encodedFileName) {
if (!fileName) {
return responses.badRequestResponse("fileName is required");
}
@@ -60,8 +60,6 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
return responses.unauthorizedResponse();
}
const fileName = decodeURIComponent(encodedFileName);
// validate signature
const validated = validateLocalSignedUrl(

View File

@@ -6,15 +6,15 @@ export const formbricksEnabled =
export const createResponse = async (
surveyId: string,
userId: string,
data: { [questionId: string]: any },
finished: boolean = false
): Promise<any> => {
const api = formbricks.getApi();
const userId = formbricks.getPerson()?.userId;
return await api.client.response.create({
surveyId,
userId,
userId: userId ?? "",
finished,
data,
});

View File

@@ -1,6 +1,5 @@
import { TSurveyQuestionType as QuestionId } from "@formbricks/types/surveys";
import {
ArrowUpTrayIcon,
ChatBubbleBottomCenterTextIcon,
CheckIcon,
CursorArrowRippleIcon,
@@ -25,7 +24,7 @@ export const questionTypes: TSurveyQuestionType[] = [
{
id: QuestionId.OpenText,
label: "Free text",
description: "Ask for a text-based answer",
description: "A single line of text",
icon: ChatBubbleBottomCenterTextIcon,
preset: {
headline: "Who let the dogs out?",
@@ -67,7 +66,7 @@ export const questionTypes: TSurveyQuestionType[] = [
{
id: QuestionId.PictureSelection,
label: "Picture Selection",
description: "Ask respondents to select one or more pictures",
description: "Select one or more pictures",
icon: PhotoIcon,
preset: {
headline: "Which is the cutest puppy?",
@@ -88,7 +87,7 @@ export const questionTypes: TSurveyQuestionType[] = [
{
id: QuestionId.Rating,
label: "Rating",
description: "Ask respondents for a rating",
description: "Ask your users to rate something",
icon: StarIcon,
preset: {
headline: "How would you rate {{productName}}",
@@ -113,7 +112,7 @@ export const questionTypes: TSurveyQuestionType[] = [
{
id: QuestionId.CTA,
label: "Call-to-Action",
description: "Prompt respondents to perform an action",
description: "Ask your users to perform an action",
icon: CursorArrowRippleIcon,
preset: {
headline: "You are one of our power users!",
@@ -125,7 +124,7 @@ export const questionTypes: TSurveyQuestionType[] = [
{
id: QuestionId.Consent,
label: "Consent",
description: "Ask respondents for consent",
description: "Ask your users to accept something",
icon: CheckIcon,
preset: {
headline: "Terms and Conditions",
@@ -133,16 +132,6 @@ export const questionTypes: TSurveyQuestionType[] = [
dismissButtonLabel: "Skip",
},
},
{
id: QuestionId.FileUpload,
label: "File Upload",
description: "Allow respondents to upload a file",
icon: ArrowUpTrayIcon,
preset: {
headline: "File Upload",
allowMultipleFiles: false,
},
},
];
export const universalQuestionPresets = {

View File

@@ -1,19 +1,28 @@
import { IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants";
import Link from "next/link";
export default function LegalFooter() {
interface LegalFooterProps {
bgColor?: string | null;
}
export default function LegalFooter({ bgColor }: LegalFooterProps) {
if (!IMPRINT_URL && !PRIVACY_URL) return null;
return (
<div className="h-10 w-full border-t border-slate-200">
<div className="mx-auto max-w-lg p-3 text-center text-sm text-slate-400">
<div
className={`fixed bottom-0 h-12 w-full`}
style={{
backgroundColor: `${bgColor}`,
}}>
<div className="mx-auto max-w-lg p-3 text-center text-xs text-slate-400">
{IMPRINT_URL && (
<Link href={IMPRINT_URL} target="_blank">
<Link href={IMPRINT_URL} target="_blank" className="hover:underline">
Imprint
</Link>
)}
{IMPRINT_URL && PRIVACY_URL && <span> | </span>}
{IMPRINT_URL && PRIVACY_URL && <span className="px-2">|</span>}
{PRIVACY_URL && (
<Link href={PRIVACY_URL} target="_blank">
<Link href={PRIVACY_URL} target="_blank" className="hover:underline">
Privacy Policy
</Link>
)}

View File

@@ -1,20 +1,19 @@
"use client";
import ContentWrapper from "@formbricks/ui/ContentWrapper";
import { SurveyInline } from "@formbricks/ui/Survey";
import SurveyLinkUsed from "@/app/s/[surveyId]/components/SurveyLinkUsed";
import VerifyEmail from "@/app/s/[surveyId]/components/VerifyEmail";
import { getPrefillResponseData } from "@/app/s/[surveyId]/lib/prefilling";
import { FormbricksAPI } from "@formbricks/api";
import { ResponseQueue } from "@formbricks/lib/responseQueue";
import { SurveyState } from "@formbricks/lib/surveyState";
import { TProduct } from "@formbricks/types/product";
import { TResponse, TResponseData, TResponseUpdate } from "@formbricks/types/responses";
import { TSurvey } from "@formbricks/types/surveys";
import ContentWrapper from "@formbricks/ui/ContentWrapper";
import { SurveyInline } from "@formbricks/ui/Survey";
import { ArrowPathIcon } from "@heroicons/react/24/solid";
import { useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { FormbricksAPI } from "@formbricks/api";
import { TUploadFileConfig } from "@formbricks/types/storage";
interface LinkSurveyProps {
survey: TSurvey;
@@ -117,7 +116,7 @@ export default function LinkSurvey({
return (
<>
<ContentWrapper className="h-full w-full p-0 md:max-w-lg">
<ContentWrapper className="h-full w-full p-0 md:max-w-md">
{isPreview && (
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
<div />
@@ -168,20 +167,6 @@ export default function LinkSurvey({
},
});
}}
onFileUpload={async (file: File, params: TUploadFileConfig) => {
const api = new FormbricksAPI({
apiHost: webAppUrl,
environmentId: survey.environmentId,
});
try {
const uploadedUrl = await api.client.storage.uploadFile(file, params);
return uploadedUrl;
} catch (err) {
console.error(err);
return "";
}
}}
onActiveQuestionChange={(questionId) => setActiveQuestionId(questionId)}
activeQuestionId={activeQuestionId}
autoFocus={autoFocus}

View File

@@ -0,0 +1,91 @@
"use client";
import { TSurvey } from "@formbricks/types/surveys";
import React from "react";
const MediaBackground = ({
children,
survey,
isEditorView,
ContentRef,
}: {
children: React.ReactNode;
survey: TSurvey;
isEditorView?: boolean;
ContentRef?: React.RefObject<HTMLDivElement>;
}) => {
const getFilterStyle = () => {
return survey.surveyBackground?.brightness
? { filter: `brightness(${survey.surveyBackground.brightness}%)` }
: {};
};
const renderBackground = () => {
const filterStyle = getFilterStyle();
switch (survey.surveyBackground?.bgType) {
case "color":
// Ensure backgroundColor is applied directly
return (
<div
style={{
...filterStyle,
backgroundColor: survey.surveyBackground.bg || "#ffff", // Default color
width: "100%",
height: "100%",
}}
className="absolute inset-0"
/>
);
case "animation":
return (
<video
muted
loop
autoPlay
style={filterStyle}
className="absolute inset-0 h-full w-full object-cover">
<source src={survey.surveyBackground.bg || ""} type="video/mp4" />
</video>
);
case "image":
return (
<div
style={{
...filterStyle,
backgroundImage: `url(${survey.surveyBackground.bg})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
className="absolute inset-0 h-full w-full"
/>
);
default:
return <div style={{ backgroundColor: "#ffff" }} className="absolute inset-0 h-full w-full" />;
}
};
const commonClasses = "relative flex w-full flex-grow flex-col items-center justify-center p-4 py-6";
const previewClasses = "flex flex-grow flex-col overflow-y-auto rounded-b-lg";
return (
<>
{isEditorView ? (
<div className={previewClasses} ref={ContentRef}>
<div className={commonClasses}>
{renderBackground()}
<div className="flex h-full w-full items-center justify-center">{children}</div>
</div>
</div>
) : (
<div className={`flex min-h-screen flex-col items-center justify-center px-2`}>
{renderBackground()}
<div className="relative w-full">{children}</div>
</div>
)}
</>
);
};
export default MediaBackground;

View File

@@ -1,15 +1,15 @@
"use client";
import type { NextPage } from "next";
import { validateSurveyPinAction } from "@/app/s/[surveyId]/actions";
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
import { cn } from "@formbricks/lib/cn";
import { TProduct } from "@formbricks/types/product";
import { TResponse } from "@formbricks/types/responses";
import { OTPInput } from "@formbricks/ui/OTPInput";
import { useCallback, useEffect, useState } from "react";
import { validateSurveyPinAction } from "@/app/s/[surveyId]/actions";
import { TSurvey } from "@formbricks/types/surveys";
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
import { cn } from "@formbricks/lib/cn";
import { OTPInput } from "@formbricks/ui/OTPInput";
import type { NextPage } from "next";
import { useCallback, useEffect, useState } from "react";
interface LinkSurveyPinScreenProps {
surveyId: string;

View File

@@ -1,12 +1,12 @@
"use client";
import { sendLinkSurveyEmailAction } from "@/app/s/[surveyId]/actions";
import { TSurvey } from "@formbricks/types/surveys";
import React, { useState } from "react";
import { EnvelopeIcon } from "@heroicons/react/24/solid";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { EnvelopeIcon } from "@heroicons/react/24/solid";
import { useState } from "react";
import { Toaster, toast } from "react-hot-toast";
import { sendLinkSurveyEmailAction } from "@/app/s/[surveyId]/actions";
import { TSurvey } from "@formbricks/types/surveys";
export default function VerifyEmail({
survey,
@@ -52,12 +52,6 @@ export default function VerifyEmail({
setEmailSent(false);
};
const handleKeyPress = (e) => {
if (e.key === "Enter") {
submitEmail(email);
}
};
if (isErrorComponent) {
return (
<div className="flex h-full w-full flex-col items-center justify-center bg-slate-50">
@@ -71,15 +65,13 @@ export default function VerifyEmail({
}
return (
<div className="flex h-full w-full flex-col items-center justify-center bg-slate-50 text-center">
<div className="flex h-full w-full flex-col items-center justify-center bg-slate-50">
<Toaster />
{!emailSent && !showPreviewQuestions && (
<div className="flex flex-col items-center justify-center px-4">
<div className="flex flex-col items-center justify-center">
<EnvelopeIcon className="h-24 w-24 rounded-full bg-slate-300 p-6 text-white" />
<p className="mt-8 text-2xl font-bold lg:text-4xl">Verify your email to respond.</p>
<p className="mt-2 text-sm text-slate-400 lg:text-base">
To respond to this survey please verify your email.
</p>
<p className="mt-8 text-4xl font-bold">Verify your email to respond.</p>
<p className="mt-2 text-slate-400">To respond to this survey please verify your email.</p>
<div className="mt-6 flex w-full space-x-2">
<Input
type="string"
@@ -87,14 +79,13 @@ export default function VerifyEmail({
placeholder="user@gmail.com"
value={email || ""}
onChange={(e) => setEmail(e.target.value)}
onKeyPress={handleKeyPress}
/>
<Button variant="darkCTA" onClick={() => submitEmail(email)} loading={isLoading}>
Verify
</Button>
</div>
<p className="mt-6 cursor-pointer text-xs text-slate-400" onClick={handlePreviewClick}>
Just curious? <span className="underline">Preview survey questions.</span>
<p className="mt-6 cursor-pointer text-sm text-slate-400" onClick={handlePreviewClick}>
Just curious? Preview survey questions.
</p>
</div>
)}
@@ -106,15 +97,15 @@ export default function VerifyEmail({
<p key={index} className="my-1">{`${index + 1}. ${question.headline}`}</p>
))}
</div>
<p className="mt-6 cursor-pointer text-xs text-slate-400" onClick={handlePreviewClick}>
Want to respond? <span className="underline">Verify email.</span>
<p className="mt-6 cursor-pointer text-sm text-slate-400" onClick={handlePreviewClick}>
Want to respond? Verify email.
</p>
</div>
)}
{emailSent && (
<div className="flex flex-col items-center justify-center px-4">
<h1 className="mt-8 text-2xl font-bold lg:text-4xl">Check your email.</h1>
<p className="mt-4 text-center text-sm text-slate-400 lg:text-base">
<div className="flex flex-col items-center justify-center">
<h1 className="mt-8 text-4xl font-bold">Survey sent successfully</h1>
<p className="mt-4 text-center text-slate-400">
We sent an email to <span className="font-semibold italic">{email}</span>. Please click the link
in the email to take your survey.
</p>

View File

@@ -1,10 +1,3 @@
import LegalFooter from "@/app/s/[surveyId]/components/LegalFooter";
export default async function SurveyLayout({ children }) {
return (
<div className="flex h-full flex-col justify-between bg-white">
<div className="h-full overflow-y-auto">{children}</div>
<LegalFooter />
</div>
);
return <div>{children}</div>;
}

View File

@@ -1,7 +1,9 @@
export const revalidate = REVALIDATION_INTERVAL;
import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
import LegalFooter from "@/app/s/[surveyId]/components/LegalFooter";
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
import MediaBackground from "@/app/s/[surveyId]/components/MediaBackground";
import PinScreen from "@/app/s/[surveyId]/components/PinScreen";
import SurveyInactive from "@/app/s/[surveyId]/components/SurveyInactive";
import { checkValidity } from "@/app/s/[surveyId]/lib/prefilling";
@@ -12,6 +14,7 @@ import { getResponseBySingleUseId } from "@formbricks/lib/response/service";
import { getSurvey } from "@formbricks/lib/survey/service";
import { TResponse } from "@formbricks/types/responses";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getEmailVerificationStatus } from "./lib/helpers";
@@ -53,7 +56,6 @@ export async function generateMetadata({ params }: LinkSurveyPageProps): Promise
const ogImgURL = `/api/v1/og?brandColor=${brandColor}&name=${surveyName}`;
return {
title: survey.name,
metadataBase: new URL(WEBAPP_URL),
openGraph: {
title: survey.name,
@@ -172,16 +174,21 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
);
}
return (
<LinkSurvey
survey={survey}
product={product}
userId={userId}
emailVerificationStatus={emailVerificationStatus}
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
singleUseId={isSingleUseSurvey ? singleUseId : undefined}
singleUseResponse={singleUseResponse ? singleUseResponse : undefined}
webAppUrl={WEBAPP_URL}
/>
);
return survey ? (
<div>
<MediaBackground survey={survey}>
<LinkSurvey
survey={survey}
product={product}
userId={userId}
emailVerificationStatus={emailVerificationStatus}
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
singleUseId={isSingleUseSurvey ? singleUseId : undefined}
singleUseResponse={singleUseResponse ? singleUseResponse : undefined}
webAppUrl={WEBAPP_URL}
/>
</MediaBackground>
<LegalFooter bgColor={survey.surveyBackground?.bg || "#ffff"} />
</div>
) : null;
}

View File

@@ -15,7 +15,7 @@ const getFile = async (environmentId: string, accessType: string, fileName: stri
return new Response(fileBuffer, {
headers: {
"Content-Type": metaData.contentType,
"Content-Disposition": "attachment",
"Content-Disposition": "inline",
},
});
} catch (err) {

View File

@@ -22,9 +22,7 @@ export async function GET(
);
}
const { environmentId, accessType, fileName: fileNameOG } = params;
const fileName = decodeURIComponent(fileNameOG);
const { environmentId, accessType, fileName } = params;
if (accessType === "public") {
return await getFile(environmentId, accessType, fileName);
@@ -44,8 +42,7 @@ export async function GET(
return responses.unauthorizedResponse();
}
const file = await getFile(environmentId, accessType, fileName);
return file;
return await getFile(environmentId, accessType, fileName);
}
export async function DELETE(_: NextRequest, { params }: { params: { fileName: string } }) {

View File

@@ -102,13 +102,6 @@ const nextConfig = {
},
};
// set actions allowed origins
if (process.env.WEBAPP_URL) {
nextConfig.experimental.serverActions = {
allowedOrigins: [process.env.WEBAPP_URL.replace(/https?:\/\//, "")],
};
}
const sentryOptions = {
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options

View File

@@ -1,6 +1,6 @@
{
"name": "@formbricks/web",
"version": "1.3.3",
"version": "1.3.1",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",
@@ -26,7 +26,7 @@
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@react-email/components": "^0.0.11",
"@sentry/nextjs": "^7.81.1",
"@sentry/nextjs": "^7.80.1",
"@vercel/og": "^0.5.20",
"bcryptjs": "^2.4.3",
"dotenv": "^16.3.1",
@@ -35,13 +35,13 @@
"googleapis": "^128.0.0",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"lru-cache": "^10.1.0",
"lucide-react": "^0.293.0",
"lru-cache": "^10.0.2",
"lucide-react": "^0.292.0",
"mime": "^3.0.0",
"next": "13.5.6",
"nodemailer": "^6.9.7",
"otplib": "^12.0.1",
"posthog-js": "^1.93.2",
"posthog-js": "^1.91.1",
"prismjs": "^1.29.0",
"qrcode": "^1.5.3",
"react": "18.2.0",
@@ -58,8 +58,8 @@
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",
"@types/bcryptjs": "^2.4.6",
"@types/lodash": "^4.14.202",
"@types/markdown-it": "^13.0.7",
"@types/lodash": "^4.14.201",
"@types/markdown-it": "^13.0.6",
"@types/qrcode": "^1.5.5",
"eslint-config-formbricks": "workspace:*"
}

View File

@@ -70,11 +70,11 @@ x-sentry-ignore-api-resolution-error: &sentry_ignore_api_resolution_error # Enab
x-next-public-sentry-dsn: &next_public_sentry_dsn # Cron Secret
# Set this to a random string to secure your cron endpoints
x-cron-secret: &cron_secret YOUR_CRON_SECRET
# Configure ASSET_PREFIX_URL when you want to ship JS & CSS files from a complete URL instead of the current domain
x-cron-secret:
&cron_secret # Configure ASSET_PREFIX_URL when you want to ship JS & CSS files from a complete URL instead of the current domain
x-asset-prefix-url: &asset_prefix_url
services:
@@ -94,7 +94,6 @@ services:
args:
DATABASE_URL: *database_url
NEXTAUTH_SECRET: *nextauth_secret
ENCRYPTION_KEY: *encryption_key
depends_on:
- postgres

View File

@@ -1,3 +0,0 @@
0 0 * * * $WEBAPP_URL/api/cron/close_surveys -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''
0 8 * * 1 curl $WEBAPP_URL/api/cron/weekly_summary -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''
0 9 * * * curl $WEBAPP_URL/api/cron/ping -X POST -H 'content-type: application/json' -H 'x-api-key: '"$CRON_SECRET"''

268
docker/production.sh Executable file → Normal file
View File

@@ -3,71 +3,70 @@
set -e
ubuntu_version=$(lsb_release -a 2>/dev/null | grep -v "No LSB modules are available." | grep "Description:" | awk -F "Description:\t" '{print $2}')
install_formbricks() {
# Friendly welcome
echo "🧱 Welcome to the Formbricks Setup Script"
echo ""
echo "🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your $ubuntu_version server."
echo ""
# Friendly welcome
echo "🧱 Welcome to the Formbricks single instance installer"
echo ""
echo "🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your $ubuntu_version server."
echo ""
# Remove any old Docker installations, without stopping the script if they're not found
echo "🧹 Time to sweep away any old Docker installations."
sudo apt-get remove docker docker-engine docker.io containerd runc >/dev/null 2>&1 || true
# Remove any old Docker installations, without stopping the script if they're not found
echo "🧹 Time to sweep away any old Docker installations."
sudo apt-get remove docker docker-engine docker.io containerd runc >/dev/null 2>&1 || true
# Update package list
echo "🔄 Updating your package list."
sudo apt-get update >/dev/null 2>&1
# Update package list
echo "🔄 Updating your package list."
sudo apt-get update >/dev/null 2>&1
# Install dependencies
echo "📦 Installing the necessary dependencies."
sudo apt-get install -y \
ca-certificates \
curl \
gnupg \
lsb-release >/dev/null 2>&1
# Install dependencies
echo "📦 Installing the necessary dependencies."
sudo apt-get install -y \
ca-certificates \
curl \
gnupg \
lsb-release >/dev/null 2>&1
# Set up Docker's official GPG key & stable repository
echo "🔑 Adding Docker's official GPG key and setting up the stable repository."
sudo mkdir -m 0755 -p /etc/apt/keyrings >/dev/null 2>&1
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg >/dev/null 2>&1
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
# Set up Docker's official GPG key & stable repository
echo "🔑 Adding Docker's official GPG key and setting up the stable repository."
sudo mkdir -m 0755 -p /etc/apt/keyrings >/dev/null 2>&1
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg >/dev/null 2>&1
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null 2>&1
# Update package list again
echo "🔄 Updating your package list again."
sudo apt-get update >/dev/null 2>&1
# Update package list again
echo "🔄 Updating your package list again."
sudo apt-get update >/dev/null 2>&1
# Install Docker
echo "🐳 Installing Docker."
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >/dev/null 2>&1
# Install Docker
echo "🐳 Installing Docker."
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >/dev/null 2>&1
# Test Docker installation
echo "🚀 Testing your Docker installation."
if docker --version >/dev/null 2>&1; then
echo "🎉 Docker is installed!"
else
echo "❌ Docker is not installed. Please install Docker before proceeding."
exit 1
fi
# Test Docker installation
echo "🚀 Testing your Docker installation."
if docker --version >/dev/null 2>&1; then
echo "🎉 Docker is installed!"
else
echo "❌ Docker is not installed. Please install Docker before proceeding."
exit 1
fi
# Adding your user to the Docker group
echo "🐳 Adding your user to the Docker group to avoid using sudo with docker commands."
sudo groupadd docker >/dev/null 2>&1 || true
sudo usermod -aG docker $USER >/dev/null 2>&1
# Adding your user to the Docker group
echo "🐳 Adding your user to the Docker group to avoid using sudo with docker commands."
sudo groupadd docker >/dev/null 2>&1 || true
sudo usermod -aG docker $USER >/dev/null 2>&1
echo "🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!"
echo "🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!"
# Installing Traefik
echo "🚗 Installing Traefik..."
mkdir -p formbricks && cd formbricks
echo "📁 Created Formbricks Quickstart directory at ./formbricks."
# Installing Traefik
echo "🚗 Installing Traefik..."
mkdir -p formbricks && cd formbricks
echo "📁 Created Formbricks Quickstart directory at ./formbricks."
# Ask the user for their email address
echo "💡 Please enter your email address for the SSL certificate:"
read email_address
# Ask the user for their email address
echo "💡 Please enter your email address for the SSL certificate:"
read email_address
cat <<EOT >traefik.yaml
cat <<EOT >traefik.yaml
entryPoints:
web:
address: ":80"
@@ -95,71 +94,72 @@ certificatesResolvers:
tlsChallenge: {}
EOT
echo "💡 Created traefik.yaml file with your provided email address."
echo "💡 Created traefik.yaml file with your provided email address."
touch acme.json
chmod 600 acme.json
echo "💡 Created acme.json file with correct permissions."
touch acme.json
chmod 600 acme.json
echo "💡 Created acme.json file with correct permissions."
# Ask the user for their domain name
echo "🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):"
read domain_name
# Ask the user for their domain name
echo "🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):"
read domain_name
# Prompt for email service setup
read -p "Do you want to set up the email service? (yes/no) You will need SMTP credentials for the same! " email_service
if [[ $email_service == "yes" ]]; then
echo "Please provide the following email service details: "
# Prompt for email service setup
read -p "Do you want to set up the email service? (yes/no) You will need SMTP credentials for the same! " email_service
if [[ $email_service == "yes" ]]; then
echo "Please provide the following email service details: "
echo -n "Enter your SMTP configured Email ID: "
read mail_from
echo -n "Enter your SMTP configured Email ID: "
read mail_from
echo -n "Enter your SMTP Host URL: "
read smtp_host
echo -n "Enter your SMTP Host URL: "
read smtp_host
echo -n "Enter your SMTP Host Port: "
read smtp_port
echo -n "Enter your SMTP Host Port: "
read smtp_port
echo -n "Enter your SMTP username: "
read smtp_user
echo -n "Enter your SMTP username: "
read smtp_user
echo -n "Enter your SMTP password: "
read smtp_password
echo -n "Enter your SMTP password: "
read smtp_password
echo -n "Enable Secure SMTP (use SSL)? Enter 1 for yes and 0 for no: "
read smtp_secure_enabled
echo -n "Enable Secure SMTP (use SSL)? Enter 1 for yes and 0 for no: "
read smtp_secure_enabled
else
mail_from=""
smtp_host=""
smtp_port=""
smtp_user=""
smtp_password=""
smtp_secure_enabled=0
fi
else
mail_from=""
smtp_host=""
smtp_port=""
smtp_user=""
smtp_password=""
smtp_secure_enabled=0
fi
echo "📥 Downloading docker-compose.yml from Formbricks GitHub repository..."
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/main/docker/docker-compose.yml
echo "📥 Downloading docker-compose.yml from Formbricks GitHub repository..."
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/main/docker/docker-compose.yml
echo "🚙 Updating docker-compose.yml with your custom inputs..."
sed -i "/WEBAPP_URL:/s|WEBAPP_URL:.*|WEBAPP_URL: \"https://$domain_name\"|" docker-compose.yml
sed -i "/NEXTAUTH_URL:/s|NEXTAUTH_URL:.*|NEXTAUTH_URL: \"https://$domain_name\"|" docker-compose.yml
echo "🚙 Updating docker-compose.yml with your custom inputs..."
sed -i "/WEBAPP_URL:/s|WEBAPP_URL:.*|WEBAPP_URL: \"https://$domain_name\"|" docker-compose.yml
sed -i "/NEXTAUTH_URL:/s|NEXTAUTH_URL:.*|NEXTAUTH_URL: \"https://$domain_name\"|" docker-compose.yml
nextauth_secret=$(openssl rand -hex 32) && sed -i "/NEXTAUTH_SECRET:$/s/NEXTAUTH_SECRET:.*/NEXTAUTH_SECRET: $nextauth_secret/" docker-compose.yml
echo "🚗 NEXTAUTH_SECRET updated successfully!"
nextauth_secret=$(openssl rand -hex 32) && sed -i "/NEXTAUTH_SECRET:$/s/NEXTAUTH_SECRET:.*/NEXTAUTH_SECRET: $nextauth_secret/" docker-compose.yml
echo "🚗 NEXTAUTH_SECRET updated successfully!"
encryption_key=$(openssl rand -hex 32) && sed -i "/ENCRYPTION_KEY:$/s/ENCRYPTION_KEY:.*/ENCRYPTION_KEY: $encryption_key/" docker-compose.yml
echo "🚗 ENCRYPTION_KEY updated successfully!"
encryption_key=$(openssl rand -hex 32) && sed -i "/ENCRYPTION_KEY:$/s/ENCRYPTION_KEY:.*/ENCRYPTION_KEY: $encryption_key/" docker-compose.yml
echo "🚗 ENCRYPTION_KEY updated successfully!"
if [[ -n $mail_from ]]; then
sed -i "s|# MAIL_FROM:|MAIL_FROM: \"$mail_from\"|" docker-compose.yml
sed -i "s|# SMTP_HOST:|SMTP_HOST: \"$smtp_host\"|" docker-compose.yml
sed -i "s|# SMTP_PORT:|SMTP_PORT: \"$smtp_port\"|" docker-compose.yml
sed -i "s|# SMTP_SECURE_ENABLED:|SMTP_SECURE_ENABLED: $smtp_secure_enabled|" docker-compose.yml
sed -i "s|# SMTP_USER:|SMTP_USER: \"$smtp_user\"|" docker-compose.yml
sed -i "s|# SMTP_PASSWORD:|SMTP_PASSWORD: \"$smtp_password\"|" docker-compose.yml
fi
awk -v domain_name="$domain_name" '
if [[ -n $mail_from ]]; then
sed -i "s|# MAIL_FROM:|MAIL_FROM: \"$mail_from\"|" docker-compose.yml
sed -i "s|# SMTP_HOST:|SMTP_HOST: \"$smtp_host\"|" docker-compose.yml
sed -i "s|# SMTP_PORT:|SMTP_PORT: \"$smtp_port\"|" docker-compose.yml
sed -i "s|# SMTP_SECURE_ENABLED:|SMTP_SECURE_ENABLED: $smtp_secure_enabled|" docker-compose.yml
sed -i "s|# SMTP_USER:|SMTP_USER: \"$smtp_user\"|" docker-compose.yml
sed -i "s|# SMTP_PASSWORD:|SMTP_PASSWORD: \"$smtp_password\"|" docker-compose.yml
fi
awk -v domain_name="$domain_name" '
/formbricks:/,/^ *$/ {
if ($0 ~ /depends_on:/) {
inserting_labels=1
@@ -195,9 +195,9 @@ EOT
1
' docker-compose.yml >tmp.yml && mv tmp.yml docker-compose.yml
newgrp docker <<END
newgrp docker <<END
docker compose up -d
docker compose up
echo "🔗 To edit more variables and deeper config, go to the formbricks/docker-compose.yml, edit the file, and restart the container!"
@@ -206,65 +206,3 @@ echo ""
echo "🎉 All done! Check the status of Formbricks & Traefik with 'cd formbricks && sudo docker compose ps.'"
END
}
uninstall_formbricks() {
echo "🗑️ Preparing to Uninstalling Formbricks..."
read -p "Are you sure you want to uninstall Formbricks? This will delete all the data associated with it! (yes/no): " uninstall_confirmation
if [[ $uninstall_confirmation == "yes" ]]; then
cd formbricks
sudo docker compose down
cd ..
sudo rm -rf formbricks
echo "🛑 Formbricks uninstalled successfully!"
else
echo "❌ Uninstalling Formbricks has been cancelled."
fi
}
stop_formbricks() {
echo "🛑 Stopping Formbricks..."
cd formbricks
sudo docker compose down
echo "🎉 Formbricks instance stopped successfully!"
}
update_formbricks() {
echo "🔄 Updating Formbricks..."
cd formbricks
sudo docker compose pull
sudo docker compose down
sudo docker compose up -d
echo "🎉 Formbricks updated successfully!"
echo "🎉 Check the status of Formbricks & Traefik with 'cd formbricks && sudo docker compose logs.'"
}
restart_formbricks() {
echo "🔄 Restarting Formbricks..."
cd formbricks
sudo docker compose restart
echo "🎉 Formbricks restarted successfully!"
}
case "$1" in
install)
install_formbricks
;;
update)
update_formbricks
;;
stop)
stop_formbricks
;;
restart)
restart_formbricks
;;
uninstall)
uninstall_formbricks
;;
*)
echo "🚀 Executing default step of installing Formbricks"
install_formbricks
;;
esac

View File

@@ -35,7 +35,7 @@
"@formbricks/tsconfig": "workspace:*",
"eslint-config-formbricks": "workspace:*",
"terser": "^5.24.0",
"vite": "^5.0.2",
"vite": "^5.0.0",
"vite-plugin-dts": "^3.6.3"
}
}

View File

@@ -1,7 +1,7 @@
import { TDisplayCreateInput, TDisplayUpdateInput } from "@formbricks/types/displays";
import { Result } from "@formbricks/types/errorHandlers";
import { NetworkError } from "@formbricks/types/errors";
import { makeRequest } from "../../utils/makeRequest";
import { TDisplay, TDisplayCreateInput, TDisplayUpdateInput } from "@formbricks/types/displays";
export class DisplayAPI {
private apiHost: string;
@@ -14,14 +14,14 @@ export class DisplayAPI {
async create(
displayInput: Omit<TDisplayCreateInput, "environmentId">
): Promise<Result<{ id: string }, NetworkError | Error>> {
): Promise<Result<TDisplay, NetworkError | Error>> {
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/displays`, "POST", displayInput);
}
async update(
displayId: string,
displayInput: Omit<TDisplayUpdateInput, "environmentId">
): Promise<Result<{}, NetworkError | Error>> {
): Promise<Result<TDisplay, NetworkError | Error>> {
return makeRequest(
this.apiHost,
`/api/v1/client/${this.environmentId}/displays/${displayId}`,

View File

@@ -3,14 +3,12 @@ import { DisplayAPI } from "./display";
import { ApiConfig } from "../../types";
import { ActionAPI } from "./action";
import { PeopleAPI } from "./people";
import { StorageAPI } from "./storage";
export class Client {
response: ResponseAPI;
display: DisplayAPI;
action: ActionAPI;
people: PeopleAPI;
storage: StorageAPI;
constructor(options: ApiConfig) {
const { apiHost, environmentId } = options;
@@ -19,6 +17,5 @@ export class Client {
this.display = new DisplayAPI(apiHost, environmentId);
this.action = new ActionAPI(apiHost, environmentId);
this.people = new PeopleAPI(apiHost, environmentId);
this.storage = new StorageAPI(apiHost, environmentId);
}
}

View File

@@ -1,7 +1,7 @@
import { Result } from "@formbricks/types/errorHandlers";
import { NetworkError } from "@formbricks/types/errors";
import { TPersonUpdateInput } from "@formbricks/types/people";
import { makeRequest } from "../../utils/makeRequest";
import { TPerson, TPersonUpdateInput } from "@formbricks/types/people";
export class PeopleAPI {
private apiHost: string;
@@ -12,14 +12,17 @@ export class PeopleAPI {
this.environmentId = environmentId;
}
async create(userId: string): Promise<Result<{}, NetworkError | Error>> {
async create(userId: string): Promise<Result<TPerson, NetworkError | Error>> {
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/people`, "POST", {
environmentId: this.environmentId,
userId,
});
}
async update(userId: string, personInput: TPersonUpdateInput): Promise<Result<{}, NetworkError | Error>> {
async update(
userId: string,
personInput: TPersonUpdateInput
): Promise<Result<TPerson, NetworkError | Error>> {
return makeRequest(
this.apiHost,
`/api/v1/client/${this.environmentId}/people/${userId}`,

View File

@@ -1,6 +1,6 @@
import { Result } from "@formbricks/types/errorHandlers";
import { NetworkError } from "@formbricks/types/errors";
import { TResponseInput, TResponseUpdateInput } from "@formbricks/types/responses";
import { TResponse, TResponseInput, TResponseUpdateInput } from "@formbricks/types/responses";
import { makeRequest } from "../../utils/makeRequest";
type TResponseUpdateInputWithResponseId = TResponseUpdateInput & { responseId: string };
@@ -16,7 +16,7 @@ export class ResponseAPI {
async create(
responseInput: Omit<TResponseInput, "environmentId">
): Promise<Result<{ id: string }, NetworkError | Error>> {
): Promise<Result<TResponse, NetworkError | Error>> {
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses`, "POST", responseInput);
}
@@ -24,7 +24,7 @@ export class ResponseAPI {
responseId,
finished,
data,
}: TResponseUpdateInputWithResponseId): Promise<Result<{}, NetworkError | Error>> {
}: TResponseUpdateInputWithResponseId): Promise<Result<TResponse, NetworkError | Error>> {
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses/${responseId}`, "PUT", {
finished,
data,

View File

@@ -1,86 +0,0 @@
interface UploadFileConfig {
allowedFileExtensions?: string[];
surveyId?: string;
}
export class StorageAPI {
private apiHost: string;
private environmentId: string;
constructor(apiHost: string, environmentId: string) {
this.apiHost = apiHost;
this.environmentId = environmentId;
}
async uploadFile(
file: File,
{ allowedFileExtensions, surveyId }: UploadFileConfig | undefined = {}
): Promise<string> {
if (!(file instanceof Blob) || !(file instanceof File)) {
throw new Error(`Invalid file type. Expected Blob or File, but received ${typeof file}`);
}
const payload = {
fileName: file.name,
fileType: file.type,
allowedFileExtensions,
surveyId,
};
const response = await fetch(`${this.apiHost}/api/v1/client/${this.environmentId}/storage`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`Upload failed with status: ${response.status}`);
}
const json = await response.json();
const { data } = json;
const { signedUrl, fileUrl, signingData, presignedFields } = data;
let requestHeaders: Record<string, string> = {};
if (signingData) {
const { signature, timestamp, uuid } = signingData;
requestHeaders = {
"X-File-Type": file.type,
"X-File-Name": encodeURIComponent(file.name),
"X-Survey-ID": surveyId ?? "",
"X-Signature": signature,
"X-Timestamp": String(timestamp),
"X-UUID": uuid,
};
}
const formData = new FormData();
if (presignedFields) {
Object.keys(presignedFields).forEach((key) => {
formData.append(key, presignedFields[key]);
});
}
// Add the actual file to be uploaded
formData.append("file", file);
const uploadResponse = await fetch(signedUrl, {
method: "POST",
...(signingData ? { headers: requestHeaders } : {}),
body: formData,
});
if (!uploadResponse.ok) {
const uploadJson = await uploadResponse.json();
throw new Error(`${uploadJson.message}`);
}
return fileUrl;
}
}

View File

@@ -6,6 +6,7 @@ import {
TSurveyClosedMessage,
TSurveyHiddenFields,
TSurveyProductOverwrites,
TSurveyBackground,
TSurveyQuestions,
TSurveySingleUse,
TSurveyThankYouCard,
@@ -27,6 +28,7 @@ declare global {
export type SurveyThankYouCard = TSurveyThankYouCard;
export type SurveyHiddenFields = TSurveyHiddenFields;
export type SurveyProductOverwrites = TSurveyProductOverwrites;
export type SurveyBackground = TSurveyBackground;
export type SurveyClosedMessage = TSurveyClosedMessage;
export type SurveySingleUse = TSurveySingleUse;
export type SurveyVerifyEmail = TSurveyVerifyEmail;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Survey" ADD COLUMN "surveyBackground" JSONB;

View File

@@ -1,18 +0,0 @@
-- 1. Rename the existing ENUM type.
ALTER TYPE "WehbhookSource" RENAME TO "TempWebhookSource";
-- 2. Create the new ENUM type.
CREATE TYPE "WebhookSource" AS ENUM ('user', 'zapier', 'make', 'n8n');
-- 3. Remove the default.
ALTER TABLE "Webhook" ALTER COLUMN "source" DROP DEFAULT;
-- 4. Change the column type using the USING clause for casting.
ALTER TABLE "Webhook"
ALTER COLUMN "source" TYPE "WebhookSource" USING "source"::text::"WebhookSource";
-- 5. Add the default back.
ALTER TABLE "Webhook" ALTER COLUMN "source" SET DEFAULT 'user';
-- Optionally, if you want to drop the old ENUM type after verifying everything works:
DROP TYPE "TempWebhookSource";

View File

@@ -31,11 +31,9 @@ enum PipelineTriggers {
responseFinished
}
enum WebhookSource {
enum WehbhookSource {
user
zapier
make
n8n
}
model Webhook {
@@ -44,7 +42,7 @@ model Webhook {
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
url String
source WebhookSource @default(user)
source WehbhookSource @default(user)
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
environmentId String
triggers PipelineTriggers[]
@@ -286,6 +284,9 @@ model Survey {
/// @zod.custom(imports.ZSurveyProductOverwrites)
/// [SurveyProductOverwrites]
productOverwrites Json?
/// @zod.custom(imports.ZSurveyBackground)
/// [SurveyBackground]
surveyBackground Json?
/// @zod.custom(imports.ZSurveySingleUse)
/// [SurveySingleUse]
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")

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