mirror of
https://github.com/formbricks/formbricks.git
synced 2026-03-19 09:21:30 -05:00
Compare commits
51 Commits
@formbrick
...
surveyBg
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38d21be5fa | ||
|
|
5c47bd00d7 | ||
|
|
6091413d82 | ||
|
|
d8e1e103ee | ||
|
|
a063f920be | ||
|
|
ab4c8838f9 | ||
|
|
7d2fffbdab | ||
|
|
b14d4e4235 | ||
|
|
52659b14ac | ||
|
|
cbe610aa90 | ||
|
|
d4a302fb20 | ||
|
|
28a33ee818 | ||
|
|
870ba09abe | ||
|
|
39b11f9dba | ||
|
|
71cfd31b7a | ||
|
|
b47eeb4673 | ||
|
|
708bfd8810 | ||
|
|
e6e41b81a3 | ||
|
|
e7d8a675c9 | ||
|
|
12e4cc110c | ||
|
|
fa7f7c497c | ||
|
|
4066fbf7d6 | ||
|
|
16245fe355 | ||
|
|
057d2ec446 | ||
|
|
9f80ed6ec2 | ||
|
|
90b0734cd2 | ||
|
|
6711543cdf | ||
|
|
862e72ca8f | ||
|
|
14c249cbde | ||
|
|
1bded4cce1 | ||
|
|
bbc6dfbc97 | ||
|
|
351ade6448 | ||
|
|
a530017c3b | ||
|
|
0479c34be9 | ||
|
|
e0e5ca93b6 | ||
|
|
c80dde1ba1 | ||
|
|
c9ba9e8e1b | ||
|
|
fd5284025f | ||
|
|
2213adb24a | ||
|
|
8d028cdf45 | ||
|
|
e705fcf763 | ||
|
|
7f65b41b59 | ||
|
|
0809055247 | ||
|
|
8546ce918c | ||
|
|
3cf1e7ad3b | ||
|
|
c7e2e08026 | ||
|
|
f39b15790b | ||
|
|
aa9142b3c0 | ||
|
|
ca634cd798 | ||
|
|
f8518fcd80 | ||
|
|
2e2ad05367 |
@@ -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.
|
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
|
## 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.
|
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
|
- [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
|
- [Attribute Class API](/docs/api/management/attribute-classes) - Create, Update, and Delete Attribute Classes
|
||||||
|
|||||||
@@ -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.",
|
"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
|
# 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.
|
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.
|
Add a new response to a survey.
|
||||||
|
|
||||||
@@ -43,8 +39,8 @@ Add a new response to a survey.
|
|||||||
### Optional Body Fields
|
### Optional Body Fields
|
||||||
|
|
||||||
<Properties>
|
<Properties>
|
||||||
<Property name="userId" type="string" required>
|
<Property name="personId" type="string" required>
|
||||||
Pre-existing User ID to identify the user sending the response
|
Internal Formbricks id to identify the user sending the response
|
||||||
</Property>
|
</Property>
|
||||||
</Properties>
|
</Properties>
|
||||||
|
|
||||||
@@ -53,20 +49,20 @@ Add a new response to a survey.
|
|||||||
| field name | required | default | description |
|
| 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. |
|
| 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. |
|
| surveyId | yes | - | The survey this response is connected to. |
|
||||||
| finished | yes | false | Mark a response as complete to be able to filter accordingly. |
|
| finished | yes | false | Mark a response as complete to be able to filter accordingly. |
|
||||||
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col sticky>
|
<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' }}
|
```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 '{
|
--data-raw '{
|
||||||
"surveyId":"cloqzeuu70000z8khcirufo60",
|
"surveyId":"clfqz1esd0000yzah51trddn8",
|
||||||
"userId": "1",
|
"personId": "clfqjny0v000ayzgsycx54a2c",
|
||||||
"finished": true,
|
"finished": true,
|
||||||
"data": {
|
"data": {
|
||||||
"clfqjny0v0003yzgscnog1j9i": 10,
|
"clfqjny0v0003yzgscnog1j9i": 10,
|
||||||
@@ -77,8 +73,8 @@ Add a new response to a survey.
|
|||||||
|
|
||||||
```json {{ title: 'Example Request Body' }}
|
```json {{ title: 'Example Request Body' }}
|
||||||
{
|
{
|
||||||
"userId": "1",
|
"personId": "clfqjny0v000ayzgsycx54a2c",
|
||||||
"surveyId": "cloqzeuu70000z8khcirufo60",
|
"surveyId": "clfqz1esd0000yzah51trddn8",
|
||||||
"finished": true,
|
"finished": true,
|
||||||
"data": {
|
"data": {
|
||||||
"clfqjny0v0003yzgscnog1j9i": 10,
|
"clfqjny0v0003yzgscnog1j9i": 10,
|
||||||
@@ -94,30 +90,19 @@ Add a new response to a survey.
|
|||||||
```json {{ title: '200 Success' }}
|
```json {{ title: '200 Success' }}
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"id": "clp84xdld0002px36fkgue5ka",
|
"id": "clisyqeoi000219t52m5gopke",
|
||||||
"createdAt": "2023-11-21T09:30:09.553Z",
|
"surveyId": "clfqz1esd0000yzah51trddn8",
|
||||||
"updatedAt": "2023-11-21T09:30:09.553Z",
|
|
||||||
"surveyId": "cloqzeuu70000z8khcirufo60",
|
|
||||||
"finished": true,
|
"finished": true,
|
||||||
|
"person": {
|
||||||
|
"id": "clfqjny0v000ayzgsycx54a2c",
|
||||||
|
"attributes": {
|
||||||
|
"email": "me@johndoe.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"clfqjny0v0003yzgscnog1j9i": 10,
|
"clfqjny0v0003yzgscnog1j9i": 10,
|
||||||
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
|
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
|
||||||
},
|
}
|
||||||
"meta": {
|
|
||||||
"userAgent": {}
|
|
||||||
},
|
|
||||||
"personAttributes": {},
|
|
||||||
"singleUseId": null,
|
|
||||||
"person": {
|
|
||||||
"id": "cloo25v3e0000z8ptskh030jd",
|
|
||||||
"userId": "1",
|
|
||||||
"attributes": {},
|
|
||||||
"environmentId": "clonzr6vc0009z8md7y06hipl",
|
|
||||||
"createdAt": "2023-11-07T08:17:23.114Z",
|
|
||||||
"updatedAt": "2023-11-07T08:17:23.114Z"
|
|
||||||
},
|
|
||||||
"tags": [],
|
|
||||||
"notes": []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -139,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.
|
Update an existing response in a survey.
|
||||||
|
|
||||||
@@ -149,9 +134,6 @@ Update an existing response in a survey.
|
|||||||
### Mandatory Body Fields
|
### Mandatory Body Fields
|
||||||
|
|
||||||
<Properties>
|
<Properties>
|
||||||
<Property name="finished" type="boolean">
|
|
||||||
Marks whether the response is complete or not.
|
|
||||||
</Property>
|
|
||||||
<Property name="data" type="string">
|
<Property name="data" type="string">
|
||||||
The data of the response as JSON object (key: questionId, value: answer).
|
The data of the response as JSON object (key: questionId, value: answer).
|
||||||
</Property>
|
</Property>
|
||||||
@@ -167,25 +149,27 @@ Update an existing response in a survey.
|
|||||||
</Col>
|
</Col>
|
||||||
<Col sticky>
|
<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' }}
|
```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 '{
|
--data-raw '{
|
||||||
"finished":false,
|
"personId": "clfqjny0v000ayzgsycx54a2c",
|
||||||
|
"surveyId": "clfqz1esd0000yzah51trddn8",
|
||||||
|
"finished": true,
|
||||||
"data": {
|
"data": {
|
||||||
"clfqjny0v0003yzgscnog1j9i": 10,
|
"clggpvpvu0009n40g8ikawby8": 5,
|
||||||
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
|
|
||||||
}
|
}
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
```json {{ title: 'Example Request Body' }}
|
```json {{ title: 'Example Request Body' }}
|
||||||
{
|
{
|
||||||
"finished":false,
|
"personId": "clfqjny0v000ayzgsycx54a2c",
|
||||||
|
"surveyId": "clfqz1esd0000yzah51trddn8",
|
||||||
|
"finished": true,
|
||||||
"data": {
|
"data": {
|
||||||
"clfqjny0v0003yzgscnog1j9i": 10,
|
"clggpvpvu0009n40g8ikawby8": 5,
|
||||||
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -197,30 +181,20 @@ Update an existing response in a survey.
|
|||||||
```json {{ title: '200 Success' }}
|
```json {{ title: '200 Success' }}
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"id": "clp84xdld0002px36fkgue5ka",
|
"id": "clisyqeoi000219t52m5gopke",
|
||||||
"createdAt": "2023-11-21T09:30:09.553Z",
|
"surveyId": "clfqz1esd0000yzah51trddn8",
|
||||||
"updatedAt": "2023-11-21T09:35:38.357Z",
|
"finished": true,
|
||||||
"surveyId": "cloqzeuu70000z8khcirufo60",
|
"person": {
|
||||||
"finished": false,
|
"id": "clfqjny0v000ayzgsycx54a2c",
|
||||||
|
"attributes": {
|
||||||
|
"email": "me@johndoe.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"clfqjny0v0003yzgscnog1j9i": 10,
|
"clfqjny0v0003yzgscnog1j9i": 10,
|
||||||
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
|
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks",
|
||||||
},
|
"clggpvpvu0009n40g8ikawby8": 5
|
||||||
"meta": {
|
}
|
||||||
"userAgent": {}
|
|
||||||
},
|
|
||||||
"personAttributes": {},
|
|
||||||
"singleUseId": null,
|
|
||||||
"person": {
|
|
||||||
"id": "cloo25v3e0000z8ptskh030jd",
|
|
||||||
"userId": "1",
|
|
||||||
"attributes": {},
|
|
||||||
"environmentId": "clonzr6vc0009z8md7y06hipl",
|
|
||||||
"createdAt": "2023-11-07T08:17:23.114Z",
|
|
||||||
"updatedAt": "2023-11-07T08:17:23.114Z"
|
|
||||||
},
|
|
||||||
"tags": [],
|
|
||||||
"notes": []
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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
|
3. Connect to API
|
||||||
4. Test
|
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).
|
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”**.
|
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>
|
<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.
|
choices accordingly. They have to be identical to the frontend we're building in the next step.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
@@ -108,10 +108,10 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
|
|||||||
|
|
||||||
**You’re all setup in Formbricks Cloud for now 👍**
|
**You’re all setup in Formbricks Cloud for now 👍**
|
||||||
|
|
||||||
## 2. Build the frontend
|
### 2. Build the frontend
|
||||||
|
|
||||||
<Note>
|
<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 😊
|
specific to our tech stack. We want to illustrate what you should consider building yours 😊
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
@@ -311,7 +311,7 @@ return (
|
|||||||
</Col>
|
</Col>
|
||||||
## 3. Connecting to the Formbricks API
|
## 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:
|
Here is the code for the `handleFeedbackSubmit` function with comments:
|
||||||
<Col>
|
<Col>
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ Click "Create a webhook":
|
|||||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
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" />
|
<Image src={EnterApiKey} alt="Enter API Key" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
<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.
|
Once you copied it in the API Key field, hit Save button to test the connection and save the credentials.
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
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:
|
Once you copied it in the newly opened Zapier window, you will be connected:
|
||||||
|
|
||||||
|
|||||||
@@ -1271,14 +1271,6 @@ export const templates: TTemplate[] = [
|
|||||||
buttonUrl: "https://app.formbricks.com/auth/signup",
|
buttonUrl: "https://app.formbricks.com/auth/signup",
|
||||||
buttonExternal: true,
|
buttonExternal: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: createId(),
|
|
||||||
type: TSurveyQuestionType.FileUpload,
|
|
||||||
headline: "Upload file",
|
|
||||||
required: false,
|
|
||||||
allowMultipleFiles: false,
|
|
||||||
maxSizeInMB: 10,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
thankYouCard: thankYouCardDefault,
|
thankYouCard: thankYouCardDefault,
|
||||||
welcomeCard: {
|
welcomeCard: {
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
# Installer stage: Building the application
|
|
||||||
FROM node:18-alpine AS installer
|
FROM node:18-alpine AS installer
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
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
|
ARG DATABASE_URL
|
||||||
ENV DATABASE_URL=$DATABASE_URL
|
ENV DATABASE_URL=$DATABASE_URL
|
||||||
|
|
||||||
@@ -22,42 +15,41 @@ WORKDIR /app
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN touch /app/apps/web/.env
|
RUN touch /app/apps/web/.env
|
||||||
|
|
||||||
|
|
||||||
RUN pnpm install
|
RUN pnpm install
|
||||||
|
|
||||||
# Build the project
|
# Build the project
|
||||||
RUN pnpm post-install --filter=web...
|
RUN pnpm post-install --filter=web...
|
||||||
RUN pnpm turbo run build --filter=web...
|
RUN pnpm turbo run build --filter=web...
|
||||||
|
|
||||||
# Runner stage: Setting up the runtime environment
|
|
||||||
FROM node:18-alpine AS runner
|
FROM node:18-alpine AS runner
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
RUN apk add --no-cache curl \
|
# Don't run production as root
|
||||||
# && addgroup --system --gid 1001 nodejs \
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
&& adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
WORKDIR /home/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/next.config.mjs .
|
||||||
COPY --from=installer /app/apps/web/package.json .
|
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 ./
|
# Automatically leverage output traces to reduce image size
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/.next/static ./apps/web/.next/static
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/apps/web/public ./apps/web/public
|
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/schema.prisma ./packages/database/schema.prisma
|
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
|
||||||
COPY --from=installer --chown=nextjs:nextjs /app/packages/database/migrations ./packages/database/migrations
|
COPY --from=installer --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
|
||||||
COPY --from=installer /app/docker/cronjobs /app/docker/cronjobs
|
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
|
EXPOSE 3000
|
||||||
ENV HOSTNAME "0.0.0.0"
|
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
CMD supercronic -quiet /app/docker/cronjobs & \
|
ENV HOSTNAME "0.0.0.0"
|
||||||
if [ "$NEXTAUTH_SECRET" != "RANDOM_STRING" ]; then \
|
|
||||||
pnpm dlx prisma migrate deploy && \
|
CMD if [ "$NEXTAUTH_SECRET" != "RANDOM_STRING" ]; then \
|
||||||
exec node apps/web/server.js; \
|
pnpm dlx prisma migrate deploy && node apps/web/server.js; \
|
||||||
else \
|
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; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
@@ -41,7 +41,7 @@ export default async function SettingsLayout({ children, params }) {
|
|||||||
membershipRole={currentUserMembership?.role}
|
membershipRole={currentUserMembership?.role}
|
||||||
/>
|
/>
|
||||||
<div className="w-full md:ml-64">
|
<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>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,12 +51,12 @@ export const handleFileUpload = async (
|
|||||||
const { signature, timestamp, uuid } = signingData;
|
const { signature, timestamp, uuid } = signingData;
|
||||||
|
|
||||||
requestHeaders = {
|
requestHeaders = {
|
||||||
"X-File-Type": file.type,
|
fileType: file.type,
|
||||||
"X-File-Name": encodeURIComponent(file.name),
|
fileName: file.name,
|
||||||
"X-Environment-ID": environmentId ?? "",
|
environmentId: environmentId ?? "",
|
||||||
"X-Signature": signature,
|
signature,
|
||||||
"X-Timestamp": String(timestamp),
|
timestamp,
|
||||||
"X-UUID": uuid,
|
uuid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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 HiddenFieldsSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/HiddenFieldsSummary";
|
||||||
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
|
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
|
||||||
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||||
import type {
|
import type { TSurveyPictureSelectionQuestion, TSurveyQuestionSummary } from "@formbricks/types/surveys";
|
||||||
TSurveyFileUploadQuestion,
|
|
||||||
TSurveyPictureSelectionQuestion,
|
|
||||||
TSurveyQuestionSummary,
|
|
||||||
} from "@formbricks/types/surveys";
|
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { TResponse } from "@formbricks/types/responses";
|
import { TResponse } from "@formbricks/types/responses";
|
||||||
import {
|
import {
|
||||||
@@ -26,8 +22,7 @@ import MultipleChoiceSummary from "./MultipleChoiceSummary";
|
|||||||
import NPSSummary from "./NPSSummary";
|
import NPSSummary from "./NPSSummary";
|
||||||
import OpenTextSummary from "./OpenTextSummary";
|
import OpenTextSummary from "./OpenTextSummary";
|
||||||
import RatingSummary from "./RatingSummary";
|
import RatingSummary from "./RatingSummary";
|
||||||
import FileUploadSummary from "./FileUploadSummary";
|
import PictureChoiceSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/PictureChoiceSummary";
|
||||||
import PictureChoiceSummary from "./PictureChoiceSummary";
|
|
||||||
|
|
||||||
interface SummaryListProps {
|
interface SummaryListProps {
|
||||||
environment: TEnvironment;
|
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) {
|
if (questionSummary.question.type === TSurveyQuestionType.PictureSelection) {
|
||||||
return (
|
return (
|
||||||
<PictureChoiceSummary
|
<PictureChoiceSummary
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
import { AuthenticationError } from "@formbricks/types/errors";
|
import { AuthenticationError } from "@formbricks/types/errors";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
import CodeBlock from "@formbricks/ui/CodeBlock";
|
import CodeBlock from "@formbricks/ui/CodeBlock";
|
||||||
import LoadingSpinner from "@formbricks/ui/LoadingSpinner";
|
|
||||||
import { CodeBracketIcon, DocumentDuplicateIcon, EnvelopeIcon } from "@heroicons/react/24/solid";
|
import { CodeBracketIcon, DocumentDuplicateIcon, EnvelopeIcon } from "@heroicons/react/24/solid";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { getEmailHtmlAction, sendEmailAction } from "../../actions";
|
import { getEmailHtmlAction, sendEmailAction } from "../../actions";
|
||||||
|
import LoadingSpinner from "@formbricks/ui/LoadingSpinner";
|
||||||
|
|
||||||
interface EmailTabProps {
|
interface EmailTabProps {
|
||||||
surveyId: string;
|
surveyId: string;
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ export default function LinkTab({ surveyUrl, survey, brandColor }: EmailTabProps
|
|||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
isRedirectDisabled={false}
|
isRedirectDisabled={false}
|
||||||
key={survey.id}
|
key={survey.id}
|
||||||
onFileUpload={async () => ""}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -81,7 +81,6 @@ export default function LogicEditor({
|
|||||||
cta: ["clicked", "skipped"],
|
cta: ["clicked", "skipped"],
|
||||||
consent: ["skipped", "accepted"],
|
consent: ["skipped", "accepted"],
|
||||||
pictureSelection: ["submitted", "skipped"],
|
pictureSelection: ["submitted", "skipped"],
|
||||||
fileUpload: ["uploaded", "notUploaded"],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const logicConditions: LogicConditions = {
|
const logicConditions: LogicConditions = {
|
||||||
@@ -100,16 +99,6 @@ export default function LogicEditor({
|
|||||||
values: null,
|
values: null,
|
||||||
unique: true,
|
unique: true,
|
||||||
},
|
},
|
||||||
uploaded: {
|
|
||||||
label: "has uploaded file",
|
|
||||||
values: null,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
notUploaded: {
|
|
||||||
label: "has not uploaded file",
|
|
||||||
values: null,
|
|
||||||
unique: true,
|
|
||||||
},
|
|
||||||
clicked: {
|
clicked: {
|
||||||
label: "is clicked",
|
label: "is clicked",
|
||||||
values: null,
|
values: null,
|
||||||
@@ -253,7 +242,7 @@ export default function LogicEditor({
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
{conditions[question.type].map(
|
{conditions[question.type].map(
|
||||||
(condition) =>
|
(condition) =>
|
||||||
!(question.required && (condition === "skipped" || condition === "notUploaded")) && (
|
!(question.required && condition === "skipped") && (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={condition}
|
key={condition}
|
||||||
value={condition}
|
value={condition}
|
||||||
|
|||||||
@@ -18,13 +18,11 @@ import {
|
|||||||
PresentationChartBarIcon,
|
PresentationChartBarIcon,
|
||||||
QueueListIcon,
|
QueueListIcon,
|
||||||
StarIcon,
|
StarIcon,
|
||||||
ArrowUpTrayIcon,
|
|
||||||
PhotoIcon,
|
PhotoIcon,
|
||||||
} from "@heroicons/react/24/solid";
|
} from "@heroicons/react/24/solid";
|
||||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
import FileUploadQuestionForm from "./FileUploadQuestionForm";
|
|
||||||
import CTAQuestionForm from "./CTAQuestionForm";
|
import CTAQuestionForm from "./CTAQuestionForm";
|
||||||
import ConsentQuestionForm from "./ConsentQuestionForm";
|
import ConsentQuestionForm from "./ConsentQuestionForm";
|
||||||
import MultipleChoiceMultiForm from "./MultipleChoiceMultiForm";
|
import MultipleChoiceMultiForm from "./MultipleChoiceMultiForm";
|
||||||
@@ -34,11 +32,9 @@ import OpenQuestionForm from "./OpenQuestionForm";
|
|||||||
import QuestionDropdown from "./QuestionMenu";
|
import QuestionDropdown from "./QuestionMenu";
|
||||||
import RatingQuestionForm from "./RatingQuestionForm";
|
import RatingQuestionForm from "./RatingQuestionForm";
|
||||||
import PictureSelectionForm from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/PictureSelectionForm";
|
import PictureSelectionForm from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/PictureSelectionForm";
|
||||||
import { TProduct } from "@formbricks/types/product";
|
|
||||||
|
|
||||||
interface QuestionCardProps {
|
interface QuestionCardProps {
|
||||||
localSurvey: TSurvey;
|
localSurvey: TSurvey;
|
||||||
product?: TProduct;
|
|
||||||
questionIdx: number;
|
questionIdx: number;
|
||||||
moveQuestion: (questionIndex: number, up: boolean) => void;
|
moveQuestion: (questionIndex: number, up: boolean) => void;
|
||||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||||
@@ -78,7 +74,6 @@ export function BackButtonInput({
|
|||||||
|
|
||||||
export default function QuestionCard({
|
export default function QuestionCard({
|
||||||
localSurvey,
|
localSurvey,
|
||||||
product,
|
|
||||||
questionIdx,
|
questionIdx,
|
||||||
moveQuestion,
|
moveQuestion,
|
||||||
updateQuestion,
|
updateQuestion,
|
||||||
@@ -128,9 +123,7 @@ export default function QuestionCard({
|
|||||||
<div>
|
<div>
|
||||||
<div className="inline-flex">
|
<div className="inline-flex">
|
||||||
<div className="-ml-0.5 mr-3 h-6 w-6 text-slate-400">
|
<div className="-ml-0.5 mr-3 h-6 w-6 text-slate-400">
|
||||||
{question.type === TSurveyQuestionType.FileUpload ? (
|
{question.type === TSurveyQuestionType.OpenText ? (
|
||||||
<ArrowUpTrayIcon />
|
|
||||||
) : question.type === TSurveyQuestionType.OpenText ? (
|
|
||||||
<ChatBubbleBottomCenterTextIcon />
|
<ChatBubbleBottomCenterTextIcon />
|
||||||
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
|
) : question.type === TSurveyQuestionType.MultipleChoiceSingle ? (
|
||||||
<QueueListIcon />
|
<QueueListIcon />
|
||||||
@@ -243,16 +236,6 @@ export default function QuestionCard({
|
|||||||
lastQuestion={lastQuestion}
|
lastQuestion={lastQuestion}
|
||||||
isInValid={isInValid}
|
isInValid={isInValid}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.FileUpload ? (
|
|
||||||
<FileUploadQuestionForm
|
|
||||||
localSurvey={localSurvey}
|
|
||||||
product={product}
|
|
||||||
question={question}
|
|
||||||
questionIdx={questionIdx}
|
|
||||||
updateQuestion={updateQuestion}
|
|
||||||
lastQuestion={lastQuestion}
|
|
||||||
isInValid={isInValid}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<Collapsible.Root open={openAdvanced} onOpenChange={setOpenAdvanced} className="mt-5">
|
<Collapsible.Root open={openAdvanced} onOpenChange={setOpenAdvanced} className="mt-5">
|
||||||
|
|||||||
@@ -202,7 +202,6 @@ export default function QuestionsView({
|
|||||||
<QuestionCard
|
<QuestionCard
|
||||||
key={internalQuestionIdMap[question.id]}
|
key={internalQuestionIdMap[question.id]}
|
||||||
localSurvey={localSurvey}
|
localSurvey={localSurvey}
|
||||||
product={product}
|
|
||||||
questionIdx={questionIdx}
|
questionIdx={questionIdx}
|
||||||
moveQuestion={moveQuestion}
|
moveQuestion={moveQuestion}
|
||||||
updateQuestion={updateQuestion}
|
updateQuestion={updateQuestion}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ interface SettingsViewProps {
|
|||||||
attributeClasses: TAttributeClass[];
|
attributeClasses: TAttributeClass[];
|
||||||
responseCount: number;
|
responseCount: number;
|
||||||
membershipRole?: TMembershipRole;
|
membershipRole?: TMembershipRole;
|
||||||
|
colours: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsView({
|
export default function SettingsView({
|
||||||
@@ -28,6 +29,7 @@ export default function SettingsView({
|
|||||||
attributeClasses,
|
attributeClasses,
|
||||||
responseCount,
|
responseCount,
|
||||||
membershipRole,
|
membershipRole,
|
||||||
|
colours,
|
||||||
}: SettingsViewProps) {
|
}: SettingsViewProps) {
|
||||||
return (
|
return (
|
||||||
<div className="mt-12 space-y-3 p-5">
|
<div className="mt-12 space-y-3 p-5">
|
||||||
@@ -60,7 +62,7 @@ export default function SettingsView({
|
|||||||
environmentId={environment.id}
|
environmentId={environment.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} />
|
<StylingCard localSurvey={localSurvey} setLocalSurvey={setLocalSurvey} colours={colours} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,18 +9,28 @@ import { CheckCircleIcon } from "@heroicons/react/24/solid";
|
|||||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Placement from "./Placement";
|
import Placement from "./Placement";
|
||||||
|
import SurveyBgSelectorTab from "./SurveyBgSelectorTab";
|
||||||
|
|
||||||
interface StylingCardProps {
|
interface StylingCardProps {
|
||||||
localSurvey: TSurvey;
|
localSurvey: TSurvey;
|
||||||
setLocalSurvey: React.Dispatch<React.SetStateAction<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 [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { type, productOverwrites } = localSurvey;
|
const { type, productOverwrites, surveyBackground } = localSurvey;
|
||||||
const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } =
|
const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } =
|
||||||
productOverwrites ?? {};
|
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 = () => {
|
const togglePlacement = () => {
|
||||||
setLocalSurvey({
|
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 = () => {
|
const toggleHighlightBorderColor = () => {
|
||||||
setLocalSurvey({
|
setLocalSurvey({
|
||||||
...localSurvey,
|
...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) => {
|
const handleBorderColorChange = (color: string) => {
|
||||||
setLocalSurvey({
|
setLocalSurvey({
|
||||||
...localSurvey,
|
...localSurvey,
|
||||||
@@ -143,6 +198,66 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 */}
|
{/* positioning */}
|
||||||
{type !== "link" && (
|
{type !== "link" && (
|
||||||
<div className="p-3 ">
|
<div className="p-3 ">
|
||||||
@@ -184,9 +299,9 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
|
|||||||
/>
|
/>
|
||||||
<Label htmlFor="autoComplete" className="cursor-pointer">
|
<Label htmlFor="autoComplete" className="cursor-pointer">
|
||||||
<div className="ml-2">
|
<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">
|
<p className="text-xs font-normal text-slate-500">
|
||||||
Change the highlight border for this survey.
|
Change the border highlight for this survey.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ interface SurveyEditorProps {
|
|||||||
attributeClasses: TAttributeClass[];
|
attributeClasses: TAttributeClass[];
|
||||||
responseCount: number;
|
responseCount: number;
|
||||||
membershipRole?: TMembershipRole;
|
membershipRole?: TMembershipRole;
|
||||||
|
colours: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SurveyEditor({
|
export default function SurveyEditor({
|
||||||
@@ -33,6 +34,7 @@ export default function SurveyEditor({
|
|||||||
attributeClasses,
|
attributeClasses,
|
||||||
responseCount,
|
responseCount,
|
||||||
membershipRole,
|
membershipRole,
|
||||||
|
colours,
|
||||||
}: SurveyEditorProps): JSX.Element {
|
}: SurveyEditorProps): JSX.Element {
|
||||||
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
|
const [activeView, setActiveView] = useState<"questions" | "settings">("questions");
|
||||||
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
|
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
|
||||||
@@ -99,6 +101,7 @@ export default function SurveyEditor({
|
|||||||
attributeClasses={attributeClasses}
|
attributeClasses={attributeClasses}
|
||||||
responseCount={responseCount}
|
responseCount={responseCount}
|
||||||
membershipRole={membershipRole}
|
membershipRole={membershipRole}
|
||||||
|
colours={colours}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
@@ -110,7 +113,6 @@ export default function SurveyEditor({
|
|||||||
product={product}
|
product={product}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
previewType={localSurvey.type === "web" ? "modal" : "fullwidth"}
|
previewType={localSurvey.type === "web" ? "modal" : "fullwidth"}
|
||||||
onFileUpload={async (file) => file.name}
|
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { getSurvey } from "@formbricks/lib/survey/service";
|
|||||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||||
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
|
||||||
import { getServerSession } from "next-auth";
|
import { getServerSession } from "next-auth";
|
||||||
|
import { colours } from "@formbricks/lib/constants";
|
||||||
import SurveyEditor from "./components/SurveyEditor";
|
import SurveyEditor from "./components/SurveyEditor";
|
||||||
|
|
||||||
export const generateMetadata = async ({ params }) => {
|
export const generateMetadata = async ({ params }) => {
|
||||||
@@ -67,6 +68,7 @@ export default async function SurveysEditPage({ params }) {
|
|||||||
attributeClasses={attributeClasses}
|
attributeClasses={attributeClasses}
|
||||||
responseCount={responseCount}
|
responseCount={responseCount}
|
||||||
membershipRole={currentUserMembership?.role}
|
membershipRole={currentUserMembership?.role}
|
||||||
|
colours={colours}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Modal from "@/app/(app)/environments/[environmentId]/surveys/components/Modal";
|
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 TabOption from "@/app/(app)/environments/[environmentId]/surveys/components/TabOption";
|
||||||
|
import MediaBackground from "@/app/s/[surveyId]/components/MediaBackground";
|
||||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
|
||||||
import type { TEnvironment } from "@formbricks/types/environment";
|
import type { TEnvironment } from "@formbricks/types/environment";
|
||||||
import type { TProduct } from "@formbricks/types/product";
|
import type { TProduct } from "@formbricks/types/product";
|
||||||
import { TSurvey } from "@formbricks/types/surveys";
|
import { TSurvey } from "@formbricks/types/surveys";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
|
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||||
import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline";
|
import { ArrowPathRoundedSquareIcon } from "@heroicons/react/24/outline";
|
||||||
import {
|
import {
|
||||||
ArrowsPointingInIcon,
|
ArrowsPointingInIcon,
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
} from "@heroicons/react/24/solid";
|
} from "@heroicons/react/24/solid";
|
||||||
import { Variants, motion } from "framer-motion";
|
import { Variants, motion } from "framer-motion";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
|
||||||
|
|
||||||
type TPreviewType = "modal" | "fullwidth" | "email";
|
type TPreviewType = "modal" | "fullwidth" | "email";
|
||||||
|
|
||||||
@@ -28,7 +28,6 @@ interface PreviewSurveyProps {
|
|||||||
previewType?: TPreviewType;
|
previewType?: TPreviewType;
|
||||||
product: TProduct;
|
product: TProduct;
|
||||||
environment: TEnvironment;
|
environment: TEnvironment;
|
||||||
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let surveyNameTemp;
|
let surveyNameTemp;
|
||||||
@@ -66,7 +65,6 @@ export default function PreviewSurvey({
|
|||||||
previewType,
|
previewType,
|
||||||
product,
|
product,
|
||||||
environment,
|
environment,
|
||||||
onFileUpload,
|
|
||||||
}: PreviewSurveyProps) {
|
}: PreviewSurveyProps) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(true);
|
const [isModalOpen, setIsModalOpen] = useState(true);
|
||||||
const [isFullScreenPreview, setIsFullScreenPreview] = useState(false);
|
const [isFullScreenPreview, setIsFullScreenPreview] = useState(false);
|
||||||
@@ -155,6 +153,20 @@ export default function PreviewSurvey({
|
|||||||
setActiveQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id);
|
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(() => {
|
useEffect(() => {
|
||||||
if (environment && environment.widgetSetupCompleted) {
|
if (environment && environment.widgetSetupCompleted) {
|
||||||
setWidgetSetupCompleted(true);
|
setWidgetSetupCompleted(true);
|
||||||
@@ -194,7 +206,7 @@ export default function PreviewSurvey({
|
|||||||
<div className="absolute right-0 top-0 m-2">
|
<div className="absolute right-0 top-0 m-2">
|
||||||
<ResetProgressButton resetQuestionProgress={resetQuestionProgress} />
|
<ResetProgressButton resetQuestionProgress={resetQuestionProgress} />
|
||||||
</div>
|
</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 */}
|
{/* 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>
|
<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" ? (
|
{previewType === "modal" ? (
|
||||||
@@ -210,28 +222,20 @@ export default function PreviewSurvey({
|
|||||||
isBrandingEnabled={product.linkSurveyBranding}
|
isBrandingEnabled={product.linkSurveyBranding}
|
||||||
onActiveQuestionChange={setActiveQuestionId}
|
onActiveQuestionChange={setActiveQuestionId}
|
||||||
isRedirectDisabled={true}
|
isRedirectDisabled={true}
|
||||||
onFileUpload={onFileUpload}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="relative z-10 w-full max-w-md px-4">
|
||||||
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
|
<SurveyInline
|
||||||
survey={survey}
|
survey={survey}
|
||||||
brandColor={brandColor}
|
brandColor={brandColor}
|
||||||
activeQuestionId={activeQuestionId || undefined}
|
activeQuestionId={activeQuestionId || undefined}
|
||||||
isBrandingEnabled={product.linkSurveyBranding}
|
isBrandingEnabled={product.linkSurveyBranding}
|
||||||
onActiveQuestionChange={setActiveQuestionId}
|
onActiveQuestionChange={setActiveQuestionId}
|
||||||
onFileUpload={onFileUpload}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</PreviewSurveyBgMobile>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{previewMode === "desktop" && (
|
{previewMode === "desktop" && (
|
||||||
@@ -282,13 +286,11 @@ export default function PreviewSurvey({
|
|||||||
isBrandingEnabled={product.linkSurveyBranding}
|
isBrandingEnabled={product.linkSurveyBranding}
|
||||||
onActiveQuestionChange={setActiveQuestionId}
|
onActiveQuestionChange={setActiveQuestionId}
|
||||||
isRedirectDisabled={true}
|
isRedirectDisabled={true}
|
||||||
onFileUpload={onFileUpload}
|
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-grow flex-col overflow-y-auto rounded-b-lg" ref={ContentRef}>
|
<MediaBackground survey={survey} ContentRef={ContentRef} isEditorView>
|
||||||
<div className="flex w-full flex-grow flex-col items-center justify-center bg-white p-4 py-6">
|
<div className="z-0 w-full max-w-md rounded-lg p-4">
|
||||||
<div className="w-full max-w-md">
|
|
||||||
<SurveyInline
|
<SurveyInline
|
||||||
survey={survey}
|
survey={survey}
|
||||||
brandColor={brandColor}
|
brandColor={brandColor}
|
||||||
@@ -296,11 +298,9 @@ export default function PreviewSurvey({
|
|||||||
isBrandingEnabled={product.linkSurveyBranding}
|
isBrandingEnabled={product.linkSurveyBranding}
|
||||||
onActiveQuestionChange={setActiveQuestionId}
|
onActiveQuestionChange={setActiveQuestionId}
|
||||||
isRedirectDisabled={true}
|
isRedirectDisabled={true}
|
||||||
onFileUpload={onFileUpload}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</MediaBackground>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -77,7 +77,6 @@ export default function TemplateContainerWithPreview({
|
|||||||
product={product}
|
product={product}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
setActiveQuestionId={setActiveQuestionId}
|
setActiveQuestionId={setActiveQuestionId}
|
||||||
onFileUpload={async (file) => file.name}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import {
|
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||||
TSurvey,
|
import { TSurvey, TSurveyHiddenFields, TSurveyWelcomeCard } from "@formbricks/types/surveys";
|
||||||
TSurveyHiddenFields,
|
|
||||||
TSurveyQuestionType,
|
|
||||||
TSurveyWelcomeCard,
|
|
||||||
} from "@formbricks/types/surveys";
|
|
||||||
import { TTemplate } from "@formbricks/types/templates";
|
import { TTemplate } from "@formbricks/types/templates";
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
|
|
||||||
@@ -2482,7 +2478,7 @@ export const customSurvey: TTemplate = {
|
|||||||
{
|
{
|
||||||
id: createId(),
|
id: createId(),
|
||||||
type: TSurveyQuestionType.OpenText,
|
type: TSurveyQuestionType.OpenText,
|
||||||
headline: "What would you like to know?",
|
headline: "Custom Survey",
|
||||||
subheader: "This is an example survey.",
|
subheader: "This is an example survey.",
|
||||||
placeholder: "Type your answer here...",
|
placeholder: "Type your answer here...",
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import Role from "./Role";
|
|||||||
const MAX_STEPS = 6;
|
const MAX_STEPS = 6;
|
||||||
|
|
||||||
interface OnboardingProps {
|
interface OnboardingProps {
|
||||||
session: Session;
|
session: Session | null;
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
profile: TProfile;
|
profile: TProfile;
|
||||||
product: TProduct;
|
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} />
|
<Greeting next={next} skip={doLater} name={profile.name ? profile.name : ""} session={session} />
|
||||||
)}
|
)}
|
||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<Role
|
<Role next={next} skip={skipStep} setFormbricksResponseId={setFormbricksResponseId} />
|
||||||
next={next}
|
|
||||||
skip={skipStep}
|
|
||||||
setFormbricksResponseId={setFormbricksResponseId}
|
|
||||||
session={session}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{currentStep === 3 && (
|
{currentStep === 3 && (
|
||||||
<Objective
|
<Objective
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { createResponse, formbricksEnabled } from "@/app/lib/formbricks";
|
|||||||
import { cn } from "@formbricks/lib/cn";
|
import { cn } from "@formbricks/lib/cn";
|
||||||
import { env } from "@formbricks/lib/env.mjs";
|
import { env } from "@formbricks/lib/env.mjs";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
import { Session } from "next-auth";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { handleTabNavigation } from "../utils";
|
import { handleTabNavigation } from "../utils";
|
||||||
@@ -14,7 +13,6 @@ type RoleProps = {
|
|||||||
next: () => void;
|
next: () => void;
|
||||||
skip: () => void;
|
skip: () => void;
|
||||||
setFormbricksResponseId: (id: string) => void;
|
setFormbricksResponseId: (id: string) => void;
|
||||||
session: Session;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type RoleChoice = {
|
type RoleChoice = {
|
||||||
@@ -22,7 +20,7 @@ type RoleChoice = {
|
|||||||
id: "project_manager" | "engineer" | "founder" | "marketing_specialist" | "other";
|
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 [selectedChoice, setSelectedChoice] = useState<string | null>(null);
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
const fieldsetRef = useRef<HTMLFieldSetElement>(null);
|
const fieldsetRef = useRef<HTMLFieldSetElement>(null);
|
||||||
@@ -57,7 +55,7 @@ const Role: React.FC<RoleProps> = ({ next, skip, setFormbricksResponseId, sessio
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
if (formbricksEnabled && env.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID) {
|
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,
|
role: selectedRole.label,
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -19,8 +19,10 @@ export async function POST(request: NextRequest) {
|
|||||||
const ws = xlsx.utils.json_to_sheet(json, { header: fields });
|
const ws = xlsx.utils.json_to_sheet(json, { header: fields });
|
||||||
xlsx.utils.book_append_sheet(wb, ws, "Sheet1");
|
xlsx.utils.book_append_sheet(wb, ws, "Sheet1");
|
||||||
|
|
||||||
const buffer = xlsx.write(wb, { type: "buffer", bookType: "xlsx" }) as Buffer;
|
const buffer = xlsx.write(wb, { type: "buffer", bookType: "xlsx" });
|
||||||
const base64String = buffer.toString("base64");
|
|
||||||
|
const binaryString = String.fromCharCode.apply(null, buffer);
|
||||||
|
const base64String = btoa(binaryString);
|
||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
headers.set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service
|
|||||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||||
import { getSyncSurveys } from "@formbricks/lib/survey/service";
|
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";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
interface Context {
|
interface Context {
|
||||||
@@ -80,8 +80,8 @@ export async function POST(req: Request, context: Context): Promise<NextResponse
|
|||||||
}
|
}
|
||||||
|
|
||||||
// return state
|
// return state
|
||||||
const state: TJsStateSync = {
|
const state: TJsState = {
|
||||||
person: { id: person.id, userId: person.userId },
|
person,
|
||||||
surveys,
|
surveys,
|
||||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||||
product,
|
product,
|
||||||
|
|||||||
@@ -14,8 +14,15 @@ export async function POST(_: Request, { params }: { params: { displayId: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await markDisplayRespondedLegacy(displayId);
|
const display = await markDisplayRespondedLegacy(displayId);
|
||||||
return responses.successResponse({}, true);
|
return responses.successResponse(
|
||||||
|
{
|
||||||
|
...display,
|
||||||
|
createdAt: display.createdAt.toISOString(),
|
||||||
|
updatedAt: display.updatedAt.toISOString(),
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return responses.internalServerErrorResponse(error.message);
|
return responses.internalServerErrorResponse(error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,5 +72,12 @@ export async function POST(request: Request): Promise<NextResponse> {
|
|||||||
console.warn("Posthog capture not possible. No team owner found");
|
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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service
|
|||||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||||
import { getSyncSurveys } from "@formbricks/lib/survey/service";
|
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";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
interface Context {
|
interface Context {
|
||||||
@@ -79,8 +79,8 @@ export async function POST(req: Request, context: Context): Promise<NextResponse
|
|||||||
}
|
}
|
||||||
|
|
||||||
// return state
|
// return state
|
||||||
const state: TJsStateSync = {
|
const state: TJsState = {
|
||||||
person: { id: person.id, userId: person.userId },
|
person,
|
||||||
surveys,
|
surveys,
|
||||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||||
product,
|
product,
|
||||||
|
|||||||
@@ -83,5 +83,5 @@ export async function PUT(
|
|||||||
response: response,
|
response: response,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return responses.successResponse({}, true);
|
return responses.successResponse(response, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,5 +105,5 @@ export async function POST(request: Request): Promise<NextResponse> {
|
|||||||
console.warn("Posthog capture not possible. No team owner found");
|
console.warn("Posthog capture not possible. No team owner found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return responses.successResponse({ id: response.id }, true);
|
return responses.successResponse(response, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { personCache } from "@formbricks/lib/person/cache";
|
|||||||
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
|
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
|
||||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||||
import { ZJsPeopleLegacyAttributeInput } from "@formbricks/types/js";
|
import { ZJsPeopleLegacyAttributeInput } from "@formbricks/types/js";
|
||||||
import { TPersonClient } from "@formbricks/types/people";
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
export async function OPTIONS(): Promise<NextResponse> {
|
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);
|
const state = await getUpdatedState(environmentId, personId);
|
||||||
|
|
||||||
let person: TPersonClient | null = null;
|
return responses.successResponse({ ...state }, true);
|
||||||
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);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return responses.internalServerErrorResponse(`Unable to complete request: ${error.message}`, true);
|
return responses.internalServerErrorResponse(`Unable to complete request: ${error.message}`, true);
|
||||||
|
|||||||
@@ -31,13 +31,8 @@ export async function POST(req: Request): Promise<NextResponse> {
|
|||||||
person = await createPerson(environmentId, userId);
|
person = await createPerson(environmentId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const personClient = {
|
|
||||||
id: person.id,
|
|
||||||
userId: person.userId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = await getUpdatedState(environmentId, person.id);
|
const state = await getUpdatedState(environmentId, person.id);
|
||||||
return responses.successResponse({ ...state, person: personClient }, true);
|
return responses.successResponse({ ...state }, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);
|
return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ export async function POST(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createPerson(environmentId, userId);
|
const person = await createPerson(environmentId, userId);
|
||||||
|
|
||||||
return responses.successResponse({}, true);
|
return responses.successResponse({ status: "success", person }, true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return responses.internalServerErrorResponse("Something went wrong", true);
|
return responses.internalServerErrorResponse("Something went wrong", true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { getUpdatedState } from "@/app/api/v1/(legacy)/js/sync/lib/sync";
|
|||||||
import { responses } from "@/app/lib/api/response";
|
import { responses } from "@/app/lib/api/response";
|
||||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||||
import { ZJsSyncLegacyInput } from "@formbricks/types/js";
|
import { ZJsSyncLegacyInput } from "@formbricks/types/js";
|
||||||
import { TPersonClient } from "@formbricks/types/people";
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
export async function OPTIONS(): Promise<NextResponse> {
|
export async function OPTIONS(): Promise<NextResponse> {
|
||||||
@@ -28,15 +27,7 @@ export async function POST(req: Request): Promise<NextResponse> {
|
|||||||
|
|
||||||
const state = await getUpdatedState(environmentId, personId);
|
const state = await getUpdatedState(environmentId, personId);
|
||||||
|
|
||||||
let person: TPersonClient | null = null;
|
return responses.successResponse({ ...state }, true);
|
||||||
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);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);
|
return responses.internalServerErrorResponse("Unable to handle the request: " + error.message, true);
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ export async function PUT(request: Request, context: Context): Promise<NextRespo
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateDisplay(displayId, inputValidation.data);
|
const display = await updateDisplay(displayId, inputValidation.data);
|
||||||
return responses.successResponse({}, true);
|
return responses.successResponse(display, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return responses.internalServerErrorResponse(error.message, true);
|
return responses.internalServerErrorResponse(error.message, true);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
|
|||||||
import { createDisplay } from "@formbricks/lib/display/service";
|
import { createDisplay } from "@formbricks/lib/display/service";
|
||||||
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
|
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
|
||||||
import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
|
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 { InvalidInputError } from "@formbricks/types/errors";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
@@ -36,8 +36,9 @@ export async function POST(request: Request, context: Context): Promise<NextResp
|
|||||||
const teamDetails = await getTeamDetails(inputValidation.data.environmentId);
|
const teamDetails = await getTeamDetails(inputValidation.data.environmentId);
|
||||||
|
|
||||||
// create display
|
// create display
|
||||||
|
let display: TDisplay;
|
||||||
try {
|
try {
|
||||||
await createDisplay(inputValidation.data);
|
display = await createDisplay(inputValidation.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof InvalidInputError) {
|
if (error instanceof InvalidInputError) {
|
||||||
return responses.badRequestResponse(error.message);
|
return responses.badRequestResponse(error.message);
|
||||||
@@ -53,5 +54,12 @@ export async function POST(request: Request, context: Context): Promise<NextResp
|
|||||||
console.warn("Posthog capture not possible. No team owner found");
|
console.warn("Posthog capture not possible. No team owner found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return responses.successResponse({}, true);
|
return responses.successResponse(
|
||||||
|
{
|
||||||
|
...display,
|
||||||
|
createdAt: display.createdAt.toISOString(),
|
||||||
|
updatedAt: display.updatedAt.toISOString(),
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
|||||||
import { getSyncSurveys } from "@formbricks/lib/survey/service";
|
import { getSyncSurveys } from "@formbricks/lib/survey/service";
|
||||||
import { getMonthlyActiveTeamPeopleCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
import { getMonthlyActiveTeamPeopleCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||||
import { TEnvironment } from "@formbricks/types/environment";
|
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";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
export async function OPTIONS(): Promise<NextResponse> {
|
export async function OPTIONS(): Promise<NextResponse> {
|
||||||
@@ -101,8 +101,8 @@ export async function GET(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// return state
|
// return state
|
||||||
const state: TJsStateSync = {
|
const state: TJsState = {
|
||||||
person: { id: person.id, userId: person.userId },
|
person,
|
||||||
surveys,
|
surveys,
|
||||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||||
product,
|
product,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
|||||||
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
|
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
|
||||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||||
import { getSurveys } from "@formbricks/lib/survey/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";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
export async function OPTIONS(): Promise<NextResponse> {
|
export async function OPTIONS(): Promise<NextResponse> {
|
||||||
@@ -51,7 +51,7 @@ export async function GET(
|
|||||||
throw new Error("Product not found");
|
throw new Error("Product not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const state: TJsStateSync = {
|
const state: TJsState = {
|
||||||
surveys: surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web"),
|
surveys: surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web"),
|
||||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||||
product,
|
product,
|
||||||
|
|||||||
@@ -91,5 +91,5 @@ export async function PUT(
|
|||||||
response: response,
|
response: response,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return responses.successResponse({}, true);
|
return responses.successResponse(response, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,5 +122,5 @@ export async function POST(request: Request, context: Context): Promise<NextResp
|
|||||||
console.warn("Posthog capture not possible. No team owner found");
|
console.warn("Posthog capture not possible. No team owner found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return responses.successResponse({ id: response.id }, true);
|
return responses.successResponse(response, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {
|
export async function POST(req: NextRequest, context: Context): Promise<NextResponse> {
|
||||||
const environmentId = context.params.environmentId;
|
const environmentId = context.params.environmentId;
|
||||||
|
|
||||||
const accessType = "private"; // private files are accessible only by authorized users
|
const accessType = "private"; // private files are accessible only by authorized users
|
||||||
const headersList = headers();
|
const headersList = headers();
|
||||||
|
|
||||||
const fileType = headersList.get("X-File-Type");
|
const fileType = headersList.get("fileType");
|
||||||
const encodedFileName = headersList.get("X-File-Name");
|
const fileName = headersList.get("fileName");
|
||||||
const surveyId = headersList.get("X-Survey-ID");
|
const surveyId = headersList.get("surveyId");
|
||||||
|
|
||||||
const signedSignature = headersList.get("X-Signature");
|
const signedSignature = headersList.get("signature");
|
||||||
const signedUuid = headersList.get("X-UUID");
|
const signedUuid = headersList.get("uuid");
|
||||||
const signedTimestamp = headersList.get("X-Timestamp");
|
const signedTimestamp = headersList.get("timestamp");
|
||||||
|
|
||||||
if (!fileType) {
|
if (!fileType) {
|
||||||
return responses.badRequestResponse("contentType is required");
|
return responses.badRequestResponse("contentType is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!encodedFileName) {
|
if (!fileName) {
|
||||||
return responses.badRequestResponse("fileName is required");
|
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);
|
return responses.notFoundResponse("TeamByEnvironmentId", environmentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = decodeURIComponent(encodedFileName);
|
|
||||||
|
|
||||||
// validate signature
|
// validate signature
|
||||||
|
|
||||||
const validated = validateLocalSignedUrl(
|
const validated = validateLocalSignedUrl(
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ interface Context {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function OPTIONS(): Promise<NextResponse> {
|
|
||||||
return responses.successResponse({}, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// api endpoint for uploading private files
|
// api endpoint for uploading private files
|
||||||
// uploaded files will be private, only the user who has access to the environment can access the file
|
// uploaded files will be private, only the user who has access to the environment can access the file
|
||||||
// uploading private files requires no authentication
|
// uploading private files requires no authentication
|
||||||
|
|||||||
@@ -16,19 +16,19 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
|||||||
const accessType = "public"; // public files are accessible by anyone
|
const accessType = "public"; // public files are accessible by anyone
|
||||||
const headersList = headers();
|
const headersList = headers();
|
||||||
|
|
||||||
const fileType = headersList.get("X-File-Type");
|
const fileType = headersList.get("fileType");
|
||||||
const encodedFileName = headersList.get("X-File-Name");
|
const fileName = headersList.get("fileName");
|
||||||
const environmentId = headersList.get("X-Environment-ID");
|
const environmentId = headersList.get("environmentId");
|
||||||
|
|
||||||
const signedSignature = headersList.get("X-Signature");
|
const signedSignature = headersList.get("signature");
|
||||||
const signedUuid = headersList.get("X-UUID");
|
const signedUuid = headersList.get("uuid");
|
||||||
const signedTimestamp = headersList.get("X-Timestamp");
|
const signedTimestamp = headersList.get("timestamp");
|
||||||
|
|
||||||
if (!fileType) {
|
if (!fileType) {
|
||||||
return responses.badRequestResponse("fileType is required");
|
return responses.badRequestResponse("fileType is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!encodedFileName) {
|
if (!fileName) {
|
||||||
return responses.badRequestResponse("fileName is required");
|
return responses.badRequestResponse("fileName is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,8 +60,6 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
|||||||
return responses.unauthorizedResponse();
|
return responses.unauthorizedResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = decodeURIComponent(encodedFileName);
|
|
||||||
|
|
||||||
// validate signature
|
// validate signature
|
||||||
|
|
||||||
const validated = validateLocalSignedUrl(
|
const validated = validateLocalSignedUrl(
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ export const formbricksEnabled =
|
|||||||
|
|
||||||
export const createResponse = async (
|
export const createResponse = async (
|
||||||
surveyId: string,
|
surveyId: string,
|
||||||
userId: string,
|
|
||||||
data: { [questionId: string]: any },
|
data: { [questionId: string]: any },
|
||||||
finished: boolean = false
|
finished: boolean = false
|
||||||
): Promise<any> => {
|
): Promise<any> => {
|
||||||
const api = formbricks.getApi();
|
const api = formbricks.getApi();
|
||||||
|
const userId = formbricks.getPerson()?.userId;
|
||||||
|
|
||||||
return await api.client.response.create({
|
return await api.client.response.create({
|
||||||
surveyId,
|
surveyId,
|
||||||
userId,
|
userId: userId ?? "",
|
||||||
finished,
|
finished,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { TSurveyQuestionType as QuestionId } from "@formbricks/types/surveys";
|
import { TSurveyQuestionType as QuestionId } from "@formbricks/types/surveys";
|
||||||
import {
|
import {
|
||||||
ArrowUpTrayIcon,
|
|
||||||
ChatBubbleBottomCenterTextIcon,
|
ChatBubbleBottomCenterTextIcon,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
CursorArrowRippleIcon,
|
CursorArrowRippleIcon,
|
||||||
@@ -25,7 +24,7 @@ export const questionTypes: TSurveyQuestionType[] = [
|
|||||||
{
|
{
|
||||||
id: QuestionId.OpenText,
|
id: QuestionId.OpenText,
|
||||||
label: "Free text",
|
label: "Free text",
|
||||||
description: "Ask for a text-based answer",
|
description: "A single line of text",
|
||||||
icon: ChatBubbleBottomCenterTextIcon,
|
icon: ChatBubbleBottomCenterTextIcon,
|
||||||
preset: {
|
preset: {
|
||||||
headline: "Who let the dogs out?",
|
headline: "Who let the dogs out?",
|
||||||
@@ -67,7 +66,7 @@ export const questionTypes: TSurveyQuestionType[] = [
|
|||||||
{
|
{
|
||||||
id: QuestionId.PictureSelection,
|
id: QuestionId.PictureSelection,
|
||||||
label: "Picture Selection",
|
label: "Picture Selection",
|
||||||
description: "Ask respondents to select one or more pictures",
|
description: "Select one or more pictures",
|
||||||
icon: PhotoIcon,
|
icon: PhotoIcon,
|
||||||
preset: {
|
preset: {
|
||||||
headline: "Which is the cutest puppy?",
|
headline: "Which is the cutest puppy?",
|
||||||
@@ -88,7 +87,7 @@ export const questionTypes: TSurveyQuestionType[] = [
|
|||||||
{
|
{
|
||||||
id: QuestionId.Rating,
|
id: QuestionId.Rating,
|
||||||
label: "Rating",
|
label: "Rating",
|
||||||
description: "Ask respondents for a rating",
|
description: "Ask your users to rate something",
|
||||||
icon: StarIcon,
|
icon: StarIcon,
|
||||||
preset: {
|
preset: {
|
||||||
headline: "How would you rate {{productName}}",
|
headline: "How would you rate {{productName}}",
|
||||||
@@ -113,7 +112,7 @@ export const questionTypes: TSurveyQuestionType[] = [
|
|||||||
{
|
{
|
||||||
id: QuestionId.CTA,
|
id: QuestionId.CTA,
|
||||||
label: "Call-to-Action",
|
label: "Call-to-Action",
|
||||||
description: "Prompt respondents to perform an action",
|
description: "Ask your users to perform an action",
|
||||||
icon: CursorArrowRippleIcon,
|
icon: CursorArrowRippleIcon,
|
||||||
preset: {
|
preset: {
|
||||||
headline: "You are one of our power users!",
|
headline: "You are one of our power users!",
|
||||||
@@ -125,7 +124,7 @@ export const questionTypes: TSurveyQuestionType[] = [
|
|||||||
{
|
{
|
||||||
id: QuestionId.Consent,
|
id: QuestionId.Consent,
|
||||||
label: "Consent",
|
label: "Consent",
|
||||||
description: "Ask respondents for consent",
|
description: "Ask your users to accept something",
|
||||||
icon: CheckIcon,
|
icon: CheckIcon,
|
||||||
preset: {
|
preset: {
|
||||||
headline: "Terms and Conditions",
|
headline: "Terms and Conditions",
|
||||||
@@ -133,16 +132,6 @@ export const questionTypes: TSurveyQuestionType[] = [
|
|||||||
dismissButtonLabel: "Skip",
|
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 = {
|
export const universalQuestionPresets = {
|
||||||
|
|||||||
@@ -1,19 +1,28 @@
|
|||||||
import { IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants";
|
import { IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants";
|
||||||
import Link from "next/link";
|
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;
|
if (!IMPRINT_URL && !PRIVACY_URL) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-10 w-full border-t border-slate-200">
|
<div
|
||||||
<div className="mx-auto max-w-lg p-3 text-center text-sm text-slate-400">
|
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 && (
|
{IMPRINT_URL && (
|
||||||
<Link href={IMPRINT_URL} target="_blank">
|
<Link href={IMPRINT_URL} target="_blank" className="hover:underline">
|
||||||
Imprint
|
Imprint
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{IMPRINT_URL && PRIVACY_URL && <span> | </span>}
|
{IMPRINT_URL && PRIVACY_URL && <span className="px-2">|</span>}
|
||||||
{PRIVACY_URL && (
|
{PRIVACY_URL && (
|
||||||
<Link href={PRIVACY_URL} target="_blank">
|
<Link href={PRIVACY_URL} target="_blank" className="hover:underline">
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import ContentWrapper from "@formbricks/ui/ContentWrapper";
|
|
||||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
|
||||||
import SurveyLinkUsed from "@/app/s/[surveyId]/components/SurveyLinkUsed";
|
import SurveyLinkUsed from "@/app/s/[surveyId]/components/SurveyLinkUsed";
|
||||||
import VerifyEmail from "@/app/s/[surveyId]/components/VerifyEmail";
|
import VerifyEmail from "@/app/s/[surveyId]/components/VerifyEmail";
|
||||||
import { getPrefillResponseData } from "@/app/s/[surveyId]/lib/prefilling";
|
import { getPrefillResponseData } from "@/app/s/[surveyId]/lib/prefilling";
|
||||||
|
import { FormbricksAPI } from "@formbricks/api";
|
||||||
import { ResponseQueue } from "@formbricks/lib/responseQueue";
|
import { ResponseQueue } from "@formbricks/lib/responseQueue";
|
||||||
import { SurveyState } from "@formbricks/lib/surveyState";
|
import { SurveyState } from "@formbricks/lib/surveyState";
|
||||||
import { TProduct } from "@formbricks/types/product";
|
import { TProduct } from "@formbricks/types/product";
|
||||||
import { TResponse, TResponseData, TResponseUpdate } from "@formbricks/types/responses";
|
import { TResponse, TResponseData, TResponseUpdate } from "@formbricks/types/responses";
|
||||||
import { TSurvey } from "@formbricks/types/surveys";
|
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 { ArrowPathIcon } from "@heroicons/react/24/solid";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { FormbricksAPI } from "@formbricks/api";
|
|
||||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
|
||||||
|
|
||||||
interface LinkSurveyProps {
|
interface LinkSurveyProps {
|
||||||
survey: TSurvey;
|
survey: TSurvey;
|
||||||
@@ -117,7 +116,7 @@ export default function LinkSurvey({
|
|||||||
|
|
||||||
return (
|
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 && (
|
{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 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 />
|
<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)}
|
onActiveQuestionChange={(questionId) => setActiveQuestionId(questionId)}
|
||||||
activeQuestionId={activeQuestionId}
|
activeQuestionId={activeQuestionId}
|
||||||
autoFocus={autoFocus}
|
autoFocus={autoFocus}
|
||||||
|
|||||||
91
apps/web/app/s/[surveyId]/components/MediaBackground.tsx
Normal file
91
apps/web/app/s/[surveyId]/components/MediaBackground.tsx
Normal 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;
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
"use client";
|
"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 { TProduct } from "@formbricks/types/product";
|
||||||
import { TResponse } from "@formbricks/types/responses";
|
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 { TSurvey } from "@formbricks/types/surveys";
|
||||||
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
|
import { OTPInput } from "@formbricks/ui/OTPInput";
|
||||||
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
|
import type { NextPage } from "next";
|
||||||
import { cn } from "@formbricks/lib/cn";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
interface LinkSurveyPinScreenProps {
|
interface LinkSurveyPinScreenProps {
|
||||||
surveyId: string;
|
surveyId: string;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { sendLinkSurveyEmailAction } from "@/app/s/[surveyId]/actions";
|
import React, { useState } from "react";
|
||||||
import { TSurvey } from "@formbricks/types/surveys";
|
import { EnvelopeIcon } from "@heroicons/react/24/solid";
|
||||||
import { Button } from "@formbricks/ui/Button";
|
import { Button } from "@formbricks/ui/Button";
|
||||||
import { Input } from "@formbricks/ui/Input";
|
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 { Toaster, toast } from "react-hot-toast";
|
||||||
|
import { sendLinkSurveyEmailAction } from "@/app/s/[surveyId]/actions";
|
||||||
|
import { TSurvey } from "@formbricks/types/surveys";
|
||||||
|
|
||||||
export default function VerifyEmail({
|
export default function VerifyEmail({
|
||||||
survey,
|
survey,
|
||||||
@@ -52,12 +52,6 @@ export default function VerifyEmail({
|
|||||||
setEmailSent(false);
|
setEmailSent(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyPress = (e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
submitEmail(email);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isErrorComponent) {
|
if (isErrorComponent) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center bg-slate-50">
|
<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 (
|
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 />
|
<Toaster />
|
||||||
{!emailSent && !showPreviewQuestions && (
|
{!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" />
|
<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-8 text-4xl font-bold">Verify your email to respond.</p>
|
||||||
<p className="mt-2 text-sm text-slate-400 lg:text-base">
|
<p className="mt-2 text-slate-400">To respond to this survey please verify your email.</p>
|
||||||
To respond to this survey please verify your email.
|
|
||||||
</p>
|
|
||||||
<div className="mt-6 flex w-full space-x-2">
|
<div className="mt-6 flex w-full space-x-2">
|
||||||
<Input
|
<Input
|
||||||
type="string"
|
type="string"
|
||||||
@@ -87,14 +79,13 @@ export default function VerifyEmail({
|
|||||||
placeholder="user@gmail.com"
|
placeholder="user@gmail.com"
|
||||||
value={email || ""}
|
value={email || ""}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
onKeyPress={handleKeyPress}
|
|
||||||
/>
|
/>
|
||||||
<Button variant="darkCTA" onClick={() => submitEmail(email)} loading={isLoading}>
|
<Button variant="darkCTA" onClick={() => submitEmail(email)} loading={isLoading}>
|
||||||
Verify
|
Verify
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-6 cursor-pointer text-xs text-slate-400" onClick={handlePreviewClick}>
|
<p className="mt-6 cursor-pointer text-sm text-slate-400" onClick={handlePreviewClick}>
|
||||||
Just curious? <span className="underline">Preview survey questions.</span>
|
Just curious? Preview survey questions.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -106,15 +97,15 @@ export default function VerifyEmail({
|
|||||||
<p key={index} className="my-1">{`${index + 1}. ${question.headline}`}</p>
|
<p key={index} className="my-1">{`${index + 1}. ${question.headline}`}</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-6 cursor-pointer text-xs text-slate-400" onClick={handlePreviewClick}>
|
<p className="mt-6 cursor-pointer text-sm text-slate-400" onClick={handlePreviewClick}>
|
||||||
Want to respond? <span className="underline">Verify email.</span>
|
Want to respond? Verify email.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{emailSent && (
|
{emailSent && (
|
||||||
<div className="flex flex-col items-center justify-center px-4">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<h1 className="mt-8 text-2xl font-bold lg:text-4xl">Check your email.</h1>
|
<h1 className="mt-8 text-4xl font-bold">Survey sent successfully</h1>
|
||||||
<p className="mt-4 text-center text-sm text-slate-400 lg:text-base">
|
<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
|
We sent an email to <span className="font-semibold italic">{email}</span>. Please click the link
|
||||||
in the email to take your survey.
|
in the email to take your survey.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
import LegalFooter from "@/app/s/[surveyId]/components/LegalFooter";
|
|
||||||
|
|
||||||
export default async function SurveyLayout({ children }) {
|
export default async function SurveyLayout({ children }) {
|
||||||
return (
|
return <div>{children}</div>;
|
||||||
<div className="flex h-full flex-col justify-between bg-white">
|
|
||||||
<div className="h-full overflow-y-auto">{children}</div>
|
|
||||||
<LegalFooter />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
export const revalidate = REVALIDATION_INTERVAL;
|
export const revalidate = REVALIDATION_INTERVAL;
|
||||||
|
|
||||||
import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
|
import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
|
||||||
|
import LegalFooter from "@/app/s/[surveyId]/components/LegalFooter";
|
||||||
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
|
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 PinScreen from "@/app/s/[surveyId]/components/PinScreen";
|
||||||
import SurveyInactive from "@/app/s/[surveyId]/components/SurveyInactive";
|
import SurveyInactive from "@/app/s/[surveyId]/components/SurveyInactive";
|
||||||
import { checkValidity } from "@/app/s/[surveyId]/lib/prefilling";
|
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 { getSurvey } from "@formbricks/lib/survey/service";
|
||||||
import { TResponse } from "@formbricks/types/responses";
|
import { TResponse } from "@formbricks/types/responses";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { getEmailVerificationStatus } from "./lib/helpers";
|
import { getEmailVerificationStatus } from "./lib/helpers";
|
||||||
|
|
||||||
@@ -171,7 +174,9 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return survey ? (
|
||||||
|
<div>
|
||||||
|
<MediaBackground survey={survey}>
|
||||||
<LinkSurvey
|
<LinkSurvey
|
||||||
survey={survey}
|
survey={survey}
|
||||||
product={product}
|
product={product}
|
||||||
@@ -182,5 +187,8 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
|
|||||||
singleUseResponse={singleUseResponse ? singleUseResponse : undefined}
|
singleUseResponse={singleUseResponse ? singleUseResponse : undefined}
|
||||||
webAppUrl={WEBAPP_URL}
|
webAppUrl={WEBAPP_URL}
|
||||||
/>
|
/>
|
||||||
);
|
</MediaBackground>
|
||||||
|
<LegalFooter bgColor={survey.surveyBackground?.bg || "#ffff"} />
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const getFile = async (environmentId: string, accessType: string, fileName: stri
|
|||||||
return new Response(fileBuffer, {
|
return new Response(fileBuffer, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": metaData.contentType,
|
"Content-Type": metaData.contentType,
|
||||||
"Content-Disposition": "attachment",
|
"Content-Disposition": "inline",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ export async function GET(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { environmentId, accessType, fileName: fileNameOG } = params;
|
const { environmentId, accessType, fileName } = params;
|
||||||
|
|
||||||
const fileName = decodeURIComponent(fileNameOG);
|
|
||||||
|
|
||||||
if (accessType === "public") {
|
if (accessType === "public") {
|
||||||
return await getFile(environmentId, accessType, fileName);
|
return await getFile(environmentId, accessType, fileName);
|
||||||
@@ -44,8 +42,7 @@ export async function GET(
|
|||||||
return responses.unauthorizedResponse();
|
return responses.unauthorizedResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = await getFile(environmentId, accessType, fileName);
|
return await getFile(environmentId, accessType, fileName);
|
||||||
return file;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(_: NextRequest, { params }: { params: { fileName: string } }) {
|
export async function DELETE(_: NextRequest, { params }: { params: { fileName: string } }) {
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ const nextConfig = {
|
|||||||
assetPrefix: process.env.ASSET_PREFIX_URL || undefined,
|
assetPrefix: process.env.ASSET_PREFIX_URL || undefined,
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
experimental: {
|
experimental: {
|
||||||
serverActions: {
|
serverActions: true,
|
||||||
allowedOrigins: [process.env.WEBAPP_URL.replace(/https?:\/\//, "")],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
transpilePackages: ["@formbricks/database", "@formbricks/ee", "@formbricks/ui", "@formbricks/lib"],
|
transpilePackages: ["@formbricks/database", "@formbricks/ee", "@formbricks/ui", "@formbricks/lib"],
|
||||||
images: {
|
images: {
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
||||||
|
|
||||||
|
|
||||||
# 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
|
x-asset-prefix-url: &asset_prefix_url
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -94,7 +94,6 @@ services:
|
|||||||
args:
|
args:
|
||||||
DATABASE_URL: *database_url
|
DATABASE_URL: *database_url
|
||||||
NEXTAUTH_SECRET: *nextauth_secret
|
NEXTAUTH_SECRET: *nextauth_secret
|
||||||
ENCRYPTION_KEY: *encryption_key
|
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
|
|||||||
@@ -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"''
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { TDisplayCreateInput, TDisplayUpdateInput } from "@formbricks/types/displays";
|
|
||||||
import { Result } from "@formbricks/types/errorHandlers";
|
import { Result } from "@formbricks/types/errorHandlers";
|
||||||
import { NetworkError } from "@formbricks/types/errors";
|
import { NetworkError } from "@formbricks/types/errors";
|
||||||
import { makeRequest } from "../../utils/makeRequest";
|
import { makeRequest } from "../../utils/makeRequest";
|
||||||
|
import { TDisplay, TDisplayCreateInput, TDisplayUpdateInput } from "@formbricks/types/displays";
|
||||||
|
|
||||||
export class DisplayAPI {
|
export class DisplayAPI {
|
||||||
private apiHost: string;
|
private apiHost: string;
|
||||||
@@ -14,14 +14,14 @@ export class DisplayAPI {
|
|||||||
|
|
||||||
async create(
|
async create(
|
||||||
displayInput: Omit<TDisplayCreateInput, "environmentId">
|
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);
|
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/displays`, "POST", displayInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
displayId: string,
|
displayId: string,
|
||||||
displayInput: Omit<TDisplayUpdateInput, "environmentId">
|
displayInput: Omit<TDisplayUpdateInput, "environmentId">
|
||||||
): Promise<Result<{}, NetworkError | Error>> {
|
): Promise<Result<TDisplay, NetworkError | Error>> {
|
||||||
return makeRequest(
|
return makeRequest(
|
||||||
this.apiHost,
|
this.apiHost,
|
||||||
`/api/v1/client/${this.environmentId}/displays/${displayId}`,
|
`/api/v1/client/${this.environmentId}/displays/${displayId}`,
|
||||||
|
|||||||
@@ -3,14 +3,12 @@ import { DisplayAPI } from "./display";
|
|||||||
import { ApiConfig } from "../../types";
|
import { ApiConfig } from "../../types";
|
||||||
import { ActionAPI } from "./action";
|
import { ActionAPI } from "./action";
|
||||||
import { PeopleAPI } from "./people";
|
import { PeopleAPI } from "./people";
|
||||||
import { StorageAPI } from "./storage";
|
|
||||||
|
|
||||||
export class Client {
|
export class Client {
|
||||||
response: ResponseAPI;
|
response: ResponseAPI;
|
||||||
display: DisplayAPI;
|
display: DisplayAPI;
|
||||||
action: ActionAPI;
|
action: ActionAPI;
|
||||||
people: PeopleAPI;
|
people: PeopleAPI;
|
||||||
storage: StorageAPI;
|
|
||||||
|
|
||||||
constructor(options: ApiConfig) {
|
constructor(options: ApiConfig) {
|
||||||
const { apiHost, environmentId } = options;
|
const { apiHost, environmentId } = options;
|
||||||
@@ -19,6 +17,5 @@ export class Client {
|
|||||||
this.display = new DisplayAPI(apiHost, environmentId);
|
this.display = new DisplayAPI(apiHost, environmentId);
|
||||||
this.action = new ActionAPI(apiHost, environmentId);
|
this.action = new ActionAPI(apiHost, environmentId);
|
||||||
this.people = new PeopleAPI(apiHost, environmentId);
|
this.people = new PeopleAPI(apiHost, environmentId);
|
||||||
this.storage = new StorageAPI(apiHost, environmentId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Result } from "@formbricks/types/errorHandlers";
|
import { Result } from "@formbricks/types/errorHandlers";
|
||||||
import { NetworkError } from "@formbricks/types/errors";
|
import { NetworkError } from "@formbricks/types/errors";
|
||||||
import { TPersonUpdateInput } from "@formbricks/types/people";
|
|
||||||
import { makeRequest } from "../../utils/makeRequest";
|
import { makeRequest } from "../../utils/makeRequest";
|
||||||
|
import { TPerson, TPersonUpdateInput } from "@formbricks/types/people";
|
||||||
|
|
||||||
export class PeopleAPI {
|
export class PeopleAPI {
|
||||||
private apiHost: string;
|
private apiHost: string;
|
||||||
@@ -12,14 +12,17 @@ export class PeopleAPI {
|
|||||||
this.environmentId = environmentId;
|
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", {
|
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/people`, "POST", {
|
||||||
environmentId: this.environmentId,
|
environmentId: this.environmentId,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(userId: string, personInput: TPersonUpdateInput): Promise<Result<{}, NetworkError | Error>> {
|
async update(
|
||||||
|
userId: string,
|
||||||
|
personInput: TPersonUpdateInput
|
||||||
|
): Promise<Result<TPerson, NetworkError | Error>> {
|
||||||
return makeRequest(
|
return makeRequest(
|
||||||
this.apiHost,
|
this.apiHost,
|
||||||
`/api/v1/client/${this.environmentId}/people/${userId}`,
|
`/api/v1/client/${this.environmentId}/people/${userId}`,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Result } from "@formbricks/types/errorHandlers";
|
import { Result } from "@formbricks/types/errorHandlers";
|
||||||
import { NetworkError } from "@formbricks/types/errors";
|
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";
|
import { makeRequest } from "../../utils/makeRequest";
|
||||||
|
|
||||||
type TResponseUpdateInputWithResponseId = TResponseUpdateInput & { responseId: string };
|
type TResponseUpdateInputWithResponseId = TResponseUpdateInput & { responseId: string };
|
||||||
@@ -16,7 +16,7 @@ export class ResponseAPI {
|
|||||||
|
|
||||||
async create(
|
async create(
|
||||||
responseInput: Omit<TResponseInput, "environmentId">
|
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);
|
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses`, "POST", responseInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ export class ResponseAPI {
|
|||||||
responseId,
|
responseId,
|
||||||
finished,
|
finished,
|
||||||
data,
|
data,
|
||||||
}: TResponseUpdateInputWithResponseId): Promise<Result<{}, NetworkError | Error>> {
|
}: TResponseUpdateInputWithResponseId): Promise<Result<TResponse, NetworkError | Error>> {
|
||||||
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses/${responseId}`, "PUT", {
|
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses/${responseId}`, "PUT", {
|
||||||
finished,
|
finished,
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
TSurveyClosedMessage,
|
TSurveyClosedMessage,
|
||||||
TSurveyHiddenFields,
|
TSurveyHiddenFields,
|
||||||
TSurveyProductOverwrites,
|
TSurveyProductOverwrites,
|
||||||
|
TSurveyBackground,
|
||||||
TSurveyQuestions,
|
TSurveyQuestions,
|
||||||
TSurveySingleUse,
|
TSurveySingleUse,
|
||||||
TSurveyThankYouCard,
|
TSurveyThankYouCard,
|
||||||
@@ -27,6 +28,7 @@ declare global {
|
|||||||
export type SurveyThankYouCard = TSurveyThankYouCard;
|
export type SurveyThankYouCard = TSurveyThankYouCard;
|
||||||
export type SurveyHiddenFields = TSurveyHiddenFields;
|
export type SurveyHiddenFields = TSurveyHiddenFields;
|
||||||
export type SurveyProductOverwrites = TSurveyProductOverwrites;
|
export type SurveyProductOverwrites = TSurveyProductOverwrites;
|
||||||
|
export type SurveyBackground = TSurveyBackground;
|
||||||
export type SurveyClosedMessage = TSurveyClosedMessage;
|
export type SurveyClosedMessage = TSurveyClosedMessage;
|
||||||
export type SurveySingleUse = TSurveySingleUse;
|
export type SurveySingleUse = TSurveySingleUse;
|
||||||
export type SurveyVerifyEmail = TSurveyVerifyEmail;
|
export type SurveyVerifyEmail = TSurveyVerifyEmail;
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Survey" ADD COLUMN "surveyBackground" JSONB;
|
||||||
@@ -284,6 +284,9 @@ model Survey {
|
|||||||
/// @zod.custom(imports.ZSurveyProductOverwrites)
|
/// @zod.custom(imports.ZSurveyProductOverwrites)
|
||||||
/// [SurveyProductOverwrites]
|
/// [SurveyProductOverwrites]
|
||||||
productOverwrites Json?
|
productOverwrites Json?
|
||||||
|
/// @zod.custom(imports.ZSurveyBackground)
|
||||||
|
/// [SurveyBackground]
|
||||||
|
surveyBackground Json?
|
||||||
/// @zod.custom(imports.ZSurveySingleUse)
|
/// @zod.custom(imports.ZSurveySingleUse)
|
||||||
/// [SurveySingleUse]
|
/// [SurveySingleUse]
|
||||||
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")
|
singleUse Json? @default("{\"enabled\": false, \"isEncrypted\": true}")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export {
|
|||||||
ZSurveyHiddenFields,
|
ZSurveyHiddenFields,
|
||||||
ZSurveyClosedMessage,
|
ZSurveyClosedMessage,
|
||||||
ZSurveyProductOverwrites,
|
ZSurveyProductOverwrites,
|
||||||
|
ZSurveyBackground,
|
||||||
ZSurveyVerifyEmail,
|
ZSurveyVerifyEmail,
|
||||||
ZSurveySingleUse,
|
ZSurveySingleUse,
|
||||||
} from "@formbricks/types/surveys";
|
} from "@formbricks/types/surveys";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@formbricks/js",
|
"name": "@formbricks/js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "1.2.4",
|
"version": "1.2.3",
|
||||||
"description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.",
|
"description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Formbricks",
|
"Formbricks",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { TJsConfigInput } from "@formbricks/types/js";
|
import { TJsConfigInput } from "@formbricks/types/js";
|
||||||
import { trackAction } from "./lib/actions";
|
|
||||||
import { getApi } from "./lib/api";
|
import { getApi } from "./lib/api";
|
||||||
import { CommandQueue } from "./lib/commandQueue";
|
import { CommandQueue } from "./lib/commandQueue";
|
||||||
import { ErrorHandler } from "./lib/errors";
|
import { ErrorHandler } from "./lib/errors";
|
||||||
|
import { trackAction } from "./lib/actions";
|
||||||
import { initialize } from "./lib/initialize";
|
import { initialize } from "./lib/initialize";
|
||||||
import { Logger } from "./lib/logger";
|
import { Logger } from "./lib/logger";
|
||||||
import { checkPageUrl } from "./lib/noCodeActions";
|
import { checkPageUrl } from "./lib/noCodeActions";
|
||||||
import { logoutPerson, resetPerson, setPersonAttribute, setPersonUserId } from "./lib/person";
|
import { resetPerson, setPersonAttribute, setPersonUserId, getPerson, logoutPerson } from "./lib/person";
|
||||||
|
|
||||||
const logger = Logger.getInstance();
|
const logger = Logger.getInstance();
|
||||||
|
|
||||||
@@ -64,6 +64,7 @@ const formbricks = {
|
|||||||
reset,
|
reset,
|
||||||
registerRouteChange,
|
registerRouteChange,
|
||||||
getApi,
|
getApi,
|
||||||
|
getPerson,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FormbricksType = typeof formbricks;
|
export type FormbricksType = typeof formbricks;
|
||||||
|
|||||||
@@ -14,16 +14,15 @@ export const trackAction = async (
|
|||||||
name: string,
|
name: string,
|
||||||
properties: TJsActionInput["properties"] = {}
|
properties: TJsActionInput["properties"] = {}
|
||||||
): Promise<Result<void, NetworkError>> => {
|
): Promise<Result<void, NetworkError>> => {
|
||||||
const { userId } = config.get();
|
|
||||||
const input: TJsActionInput = {
|
const input: TJsActionInput = {
|
||||||
environmentId: config.get().environmentId,
|
environmentId: config.get().environmentId,
|
||||||
userId,
|
userId: config.get().state?.person?.userId,
|
||||||
name,
|
name,
|
||||||
properties: properties || {},
|
properties: properties || {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// don't send actions to the backend if the person is not identified
|
// don't send actions to the backend if the person is not identified
|
||||||
if (userId && !intentsToNotCreateOnApp.includes(name)) {
|
if (config.get().state?.person?.userId && !intentsToNotCreateOnApp.includes(name)) {
|
||||||
logger.debug(`Sending action "${name}" to backend`);
|
logger.debug(`Sending action "${name}" to backend`);
|
||||||
|
|
||||||
const api = new FormbricksAPI({
|
const api = new FormbricksAPI({
|
||||||
@@ -32,7 +31,7 @@ export const trackAction = async (
|
|||||||
});
|
});
|
||||||
const res = await api.client.action.create({
|
const res = await api.client.action.create({
|
||||||
...input,
|
...input,
|
||||||
userId,
|
userId: config.get().state.person!.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const initialize = async (
|
|||||||
localConfigResult.value.state &&
|
localConfigResult.value.state &&
|
||||||
localConfigResult.value.environmentId === c.environmentId &&
|
localConfigResult.value.environmentId === c.environmentId &&
|
||||||
localConfigResult.value.apiHost === c.apiHost &&
|
localConfigResult.value.apiHost === c.apiHost &&
|
||||||
localConfigResult.value.userId === c.userId &&
|
localConfigResult.value.state?.person?.userId === c.userId &&
|
||||||
localConfigResult.value.expiresAt // only accept config when they follow new config version with expiresAt
|
localConfigResult.value.expiresAt // only accept config when they follow new config version with expiresAt
|
||||||
) {
|
) {
|
||||||
logger.debug("Found existing configuration.");
|
logger.debug("Found existing configuration.");
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { FormbricksAPI } from "@formbricks/api";
|
import { TPerson, TPersonUpdateInput } from "@formbricks/types/people";
|
||||||
import { TPersonUpdateInput } from "@formbricks/types/people";
|
|
||||||
import { Config } from "./config";
|
import { Config } from "./config";
|
||||||
import { AttributeAlreadyExistsError, MissingPersonError, NetworkError, Result, err, okVoid } from "./errors";
|
import { AttributeAlreadyExistsError, MissingPersonError, NetworkError, Result, err, okVoid } from "./errors";
|
||||||
import { deinitalize, initialize } from "./initialize";
|
import { deinitalize, initialize } from "./initialize";
|
||||||
import { Logger } from "./logger";
|
import { Logger } from "./logger";
|
||||||
import { sync } from "./sync";
|
import { sync } from "./sync";
|
||||||
|
import { FormbricksAPI } from "@formbricks/api";
|
||||||
import { closeSurvey } from "./widget";
|
import { closeSurvey } from "./widget";
|
||||||
|
|
||||||
const config = Config.getInstance();
|
const config = Config.getInstance();
|
||||||
@@ -14,11 +14,10 @@ export const updatePersonAttribute = async (
|
|||||||
key: string,
|
key: string,
|
||||||
value: string
|
value: string
|
||||||
): Promise<Result<void, NetworkError | MissingPersonError>> => {
|
): Promise<Result<void, NetworkError | MissingPersonError>> => {
|
||||||
const { apiHost, environmentId, userId } = config.get();
|
if (!config.get().state.person || !config.get().state.person?.id) {
|
||||||
if (!userId) {
|
|
||||||
return err({
|
return err({
|
||||||
code: "missing_person",
|
code: "missing_person",
|
||||||
message: "Unable to update attribute. User identification deactivated. No userId set.",
|
message: "Unable to update attribute. No person set.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,14 +31,16 @@ export const updatePersonAttribute = async (
|
|||||||
apiHost: config.get().apiHost,
|
apiHost: config.get().apiHost,
|
||||||
environmentId: config.get().environmentId,
|
environmentId: config.get().environmentId,
|
||||||
});
|
});
|
||||||
const res = await api.client.people.update(userId, input);
|
const res = await api.client.people.update(config.get().state.person!.userId, input);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return err({
|
return err({
|
||||||
code: "network_error",
|
code: "network_error",
|
||||||
status: 500,
|
status: 500,
|
||||||
message: `Error updating person with userId ${userId}`,
|
message: `Error updating person with userId ${config.get().state.person?.userId}`,
|
||||||
url: `${config.get().apiHost}/api/v1/client/${environmentId}/people/${userId}`,
|
url: `${config.get().apiHost}/api/v1/client/${config.get().environmentId}/people/${
|
||||||
|
config.get().state.person?.userId
|
||||||
|
}`,
|
||||||
responseMessage: res.error.message,
|
responseMessage: res.error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -47,16 +48,23 @@ export const updatePersonAttribute = async (
|
|||||||
logger.debug("Attribute updated. Syncing...");
|
logger.debug("Attribute updated. Syncing...");
|
||||||
|
|
||||||
await sync({
|
await sync({
|
||||||
environmentId: environmentId,
|
environmentId: config.get().environmentId,
|
||||||
apiHost: apiHost,
|
apiHost: config.get().apiHost,
|
||||||
userId: userId,
|
userId: config.get().state.person?.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return okVoid();
|
return okVoid();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isExistingAttribute = (key: string, value: string): boolean => {
|
export const hasAttributeValue = (key: string, value: string): boolean => {
|
||||||
if (config.get().state.attributes[key] === value) {
|
if (config.get().state.person?.attributes?.[key] === value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasAttributeKey = (key: string): boolean => {
|
||||||
|
if (config.get().state.person?.attributes?.[key]) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -75,7 +83,7 @@ export const setPersonAttribute = async (
|
|||||||
): Promise<Result<void, NetworkError | MissingPersonError>> => {
|
): Promise<Result<void, NetworkError | MissingPersonError>> => {
|
||||||
logger.debug("Setting attribute: " + key + " to value: " + value);
|
logger.debug("Setting attribute: " + key + " to value: " + value);
|
||||||
// check if attribute already exists with this value
|
// check if attribute already exists with this value
|
||||||
if (isExistingAttribute(key, value.toString())) {
|
if (hasAttributeValue(key, value.toString())) {
|
||||||
logger.debug("Attribute already set to this value. Skipping update.");
|
logger.debug("Attribute already set to this value. Skipping update.");
|
||||||
return okVoid();
|
return okVoid();
|
||||||
}
|
}
|
||||||
@@ -83,19 +91,6 @@ export const setPersonAttribute = async (
|
|||||||
const result = await updatePersonAttribute(key, value.toString());
|
const result = await updatePersonAttribute(key, value.toString());
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
// udpdate attribute in config
|
|
||||||
config.update({
|
|
||||||
environmentId: config.get().environmentId,
|
|
||||||
apiHost: config.get().apiHost,
|
|
||||||
userId: config.get().userId,
|
|
||||||
state: {
|
|
||||||
...config.get().state,
|
|
||||||
attributes: {
|
|
||||||
...config.get().state.attributes,
|
|
||||||
[key]: value.toString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return okVoid();
|
return okVoid();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +107,7 @@ export const resetPerson = async (): Promise<Result<void, NetworkError>> => {
|
|||||||
const syncParams = {
|
const syncParams = {
|
||||||
environmentId: config.get().environmentId,
|
environmentId: config.get().environmentId,
|
||||||
apiHost: config.get().apiHost,
|
apiHost: config.get().apiHost,
|
||||||
userId: config.get().userId,
|
userId: config.get().state?.person?.userId,
|
||||||
};
|
};
|
||||||
await logoutPerson();
|
await logoutPerson();
|
||||||
try {
|
try {
|
||||||
@@ -122,3 +117,7 @@ export const resetPerson = async (): Promise<Result<void, NetworkError>> => {
|
|||||||
return err(e as NetworkError);
|
return err(e as NetworkError);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPerson = (): TPerson | null => {
|
||||||
|
return config.get().state.person;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TJsState, TJsStateSync, TJsSyncParams } from "@formbricks/types/js";
|
import { TJsState, TJsSyncParams } from "@formbricks/types/js";
|
||||||
import { Config } from "./config";
|
import { Config } from "./config";
|
||||||
import { NetworkError, Result, err, ok } from "./errors";
|
import { NetworkError, Result, err, ok } from "./errors";
|
||||||
import { Logger } from "./logger";
|
import { Logger } from "./logger";
|
||||||
@@ -17,7 +17,7 @@ const syncWithBackend = async ({
|
|||||||
apiHost,
|
apiHost,
|
||||||
environmentId,
|
environmentId,
|
||||||
userId,
|
userId,
|
||||||
}: TJsSyncParams): Promise<Result<TJsStateSync, NetworkError>> => {
|
}: TJsSyncParams): Promise<Result<TJsState, NetworkError>> => {
|
||||||
const url = `${apiHost}/api/v1/client/${environmentId}/in-app/sync/${userId}`;
|
const url = `${apiHost}/api/v1/client/${environmentId}/in-app/sync/${userId}`;
|
||||||
const publicUrl = `${apiHost}/api/v1/client/${environmentId}/in-app/sync`;
|
const publicUrl = `${apiHost}/api/v1/client/${environmentId}/in-app/sync`;
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ const syncWithBackend = async ({
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
const { data: state } = data;
|
const { data: state } = data;
|
||||||
|
|
||||||
return ok(state as TJsStateSync);
|
return ok(state as TJsState);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sync = async (params: TJsSyncParams): Promise<void> => {
|
export const sync = async (params: TJsSyncParams): Promise<void> => {
|
||||||
@@ -72,6 +72,7 @@ export const sync = async (params: TJsSyncParams): Promise<void> => {
|
|||||||
throw syncResult.error;
|
throw syncResult.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const state = syncResult.value;
|
||||||
let oldState: TJsState | undefined;
|
let oldState: TJsState | undefined;
|
||||||
try {
|
try {
|
||||||
oldState = config.get().state;
|
oldState = config.get().state;
|
||||||
@@ -79,37 +80,37 @@ export const sync = async (params: TJsSyncParams): Promise<void> => {
|
|||||||
// ignore error
|
// ignore error
|
||||||
}
|
}
|
||||||
|
|
||||||
let state: TJsState = {
|
config.update({
|
||||||
surveys: syncResult.value.surveys,
|
apiHost: params.apiHost,
|
||||||
noCodeActionClasses: syncResult.value.noCodeActionClasses,
|
environmentId: params.environmentId,
|
||||||
product: syncResult.value.product,
|
state,
|
||||||
attributes: oldState?.attributes || {},
|
});
|
||||||
};
|
|
||||||
|
|
||||||
if (!params.userId) {
|
// before finding the surveys, check for public use
|
||||||
|
|
||||||
|
if (!state.person?.id) {
|
||||||
// unidentified user
|
// unidentified user
|
||||||
// set the displays and filter out surveys
|
// set the displays and filter out surveys
|
||||||
state = {
|
const publicState = {
|
||||||
...state,
|
...state,
|
||||||
displays: oldState?.displays || [],
|
displays: oldState?.displays || [],
|
||||||
};
|
};
|
||||||
state = filterPublicSurveys(state);
|
|
||||||
|
|
||||||
const surveyNames = state.surveys.map((s) => s.name);
|
const filteredState = filterPublicSurveys(publicState);
|
||||||
|
|
||||||
|
// update config
|
||||||
|
config.update({
|
||||||
|
apiHost: params.apiHost,
|
||||||
|
environmentId: params.environmentId,
|
||||||
|
state: filteredState,
|
||||||
|
});
|
||||||
|
|
||||||
|
const surveyNames = filteredState.surveys.map((s) => s.name);
|
||||||
logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames.join(", "));
|
logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames.join(", "));
|
||||||
} else {
|
} else {
|
||||||
const surveyNames = state.surveys.map((s) => s.name);
|
const surveyNames = state.surveys.map((s) => s.name);
|
||||||
logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames.join(", "));
|
logger.debug("Fetched " + surveyNames.length + " surveys during sync: " + surveyNames.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
config.update({
|
|
||||||
apiHost: params.apiHost,
|
|
||||||
environmentId: params.environmentId,
|
|
||||||
userId: params.userId,
|
|
||||||
state,
|
|
||||||
});
|
|
||||||
|
|
||||||
// before finding the surveys, check for public use
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error during sync: ${error}`);
|
logger.error(`Error during sync: ${error}`);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -176,7 +177,7 @@ export const addExpiryCheckListener = (): void => {
|
|||||||
await sync({
|
await sync({
|
||||||
apiHost: config.get().apiHost,
|
apiHost: config.get().apiHost,
|
||||||
environmentId: config.get().environmentId,
|
environmentId: config.get().environmentId,
|
||||||
userId: config.get().userId,
|
userId: config.get().state?.person?.userId,
|
||||||
// personId: config.get().state?.person?.id,
|
// personId: config.get().state?.person?.id,
|
||||||
});
|
});
|
||||||
}, updateInterval);
|
}, updateInterval);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const renderWidget = (survey: TSurvey) => {
|
|||||||
|
|
||||||
const product = config.get().state.product;
|
const product = config.get().state.product;
|
||||||
|
|
||||||
const surveyState = new SurveyState(survey.id, null, null, config.get().userId);
|
const surveyState = new SurveyState(survey.id, null, null, config.get().state.person?.id);
|
||||||
|
|
||||||
const responseQueue = new ResponseQueue(
|
const responseQueue = new ResponseQueue(
|
||||||
{
|
{
|
||||||
@@ -61,9 +61,8 @@ export const renderWidget = (survey: TSurvey) => {
|
|||||||
highlightBorderColor,
|
highlightBorderColor,
|
||||||
placement,
|
placement,
|
||||||
onDisplay: async () => {
|
onDisplay: async () => {
|
||||||
const { userId } = config.get();
|
|
||||||
// if config does not have a person, we store the displays in local storage
|
// if config does not have a person, we store the displays in local storage
|
||||||
if (!userId) {
|
if (!config.get().state.person || !config.get().state.person?.userId) {
|
||||||
const localDisplay: TJSStateDisplay = {
|
const localDisplay: TJSStateDisplay = {
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
surveyId: survey.id,
|
surveyId: survey.id,
|
||||||
@@ -89,7 +88,7 @@ export const renderWidget = (survey: TSurvey) => {
|
|||||||
});
|
});
|
||||||
const res = await api.client.display.create({
|
const res = await api.client.display.create({
|
||||||
surveyId: survey.id,
|
surveyId: survey.id,
|
||||||
userId,
|
userId: config.get().state.person?.userId,
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error("Could not create display");
|
throw new Error("Could not create display");
|
||||||
@@ -100,9 +99,8 @@ export const renderWidget = (survey: TSurvey) => {
|
|||||||
responseQueue.updateSurveyState(surveyState);
|
responseQueue.updateSurveyState(surveyState);
|
||||||
},
|
},
|
||||||
onResponse: (responseUpdate: TResponseUpdate) => {
|
onResponse: (responseUpdate: TResponseUpdate) => {
|
||||||
const { userId } = config.get();
|
|
||||||
// if user is unidentified, update the display in local storage if not already updated
|
// if user is unidentified, update the display in local storage if not already updated
|
||||||
if (!userId) {
|
if (!config.get().state.person || !config.get().state.person?.userId) {
|
||||||
const displays = config.get().state.displays;
|
const displays = config.get().state.displays;
|
||||||
const lastDisplay = displays && displays[displays.length - 1];
|
const lastDisplay = displays && displays[displays.length - 1];
|
||||||
if (!lastDisplay) {
|
if (!lastDisplay) {
|
||||||
@@ -122,8 +120,8 @@ export const renderWidget = (survey: TSurvey) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId) {
|
if (config.get().state.person && config.get().state.person?.userId) {
|
||||||
surveyState.updateUserId(userId);
|
surveyState.updateUserId(config.get().state.person?.userId!);
|
||||||
}
|
}
|
||||||
responseQueue.updateSurveyState(surveyState);
|
responseQueue.updateSurveyState(surveyState);
|
||||||
responseQueue.add({
|
responseQueue.add({
|
||||||
@@ -132,14 +130,6 @@ export const renderWidget = (survey: TSurvey) => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
onClose: closeSurvey,
|
onClose: closeSurvey,
|
||||||
onFileUpload: async (file: File, params) => {
|
|
||||||
const api = new FormbricksAPI({
|
|
||||||
apiHost: config.get().apiHost,
|
|
||||||
environmentId: config.get().environmentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return await api.client.storage.uploadFile(file, params);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}, survey.delay * 1000);
|
}, survey.delay * 1000);
|
||||||
};
|
};
|
||||||
@@ -150,7 +140,7 @@ export const closeSurvey = async (): Promise<void> => {
|
|||||||
addWidgetContainer();
|
addWidgetContainer();
|
||||||
|
|
||||||
// if unidentified user, refilter the surveys
|
// if unidentified user, refilter the surveys
|
||||||
if (!config.get().userId) {
|
if (!config.get().state.person || !config.get().state.person?.userId) {
|
||||||
const state = config.get().state;
|
const state = config.get().state;
|
||||||
const updatedState = filterPublicSurveys(state);
|
const updatedState = filterPublicSurveys(state);
|
||||||
config.update({
|
config.update({
|
||||||
@@ -166,7 +156,7 @@ export const closeSurvey = async (): Promise<void> => {
|
|||||||
await sync({
|
await sync({
|
||||||
apiHost: config.get().apiHost,
|
apiHost: config.get().apiHost,
|
||||||
environmentId: config.get().environmentId,
|
environmentId: config.get().environmentId,
|
||||||
userId: config.get().userId,
|
userId: config.get().state?.person?.userId,
|
||||||
});
|
});
|
||||||
surveyRunning = false;
|
surveyRunning = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ beforeEach(() => {
|
|||||||
fetchMock.resetMocks();
|
fetchMock.resetMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
/* test("Formbricks should Initialise", async () => {
|
test("Formbricks should Initialise", async () => {
|
||||||
mockInitResponse();
|
mockInitResponse();
|
||||||
|
|
||||||
await formbricks.init({
|
await formbricks.init({
|
||||||
@@ -54,7 +54,14 @@ beforeEach(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Formbricks should set email", async () => {
|
test("Formbricks should get the current person with no attributes", () => {
|
||||||
|
const currentStatePerson = formbricks.getPerson();
|
||||||
|
|
||||||
|
const currentStatePersonAttributes: TPersonAttributes = currentStatePerson.attributes;
|
||||||
|
expect(Object.keys(currentStatePersonAttributes)).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* test("Formbricks should set email", async () => {
|
||||||
mockSetEmailIdResponse();
|
mockSetEmailIdResponse();
|
||||||
await formbricks.setEmail(initialUserEmail);
|
await formbricks.setEmail(initialUserEmail);
|
||||||
|
|
||||||
@@ -105,7 +112,7 @@ test("Formbricks should update attribute", async () => {
|
|||||||
expect(email).toStrictEqual(updatedUserEmail);
|
expect(email).toStrictEqual(updatedUserEmail);
|
||||||
const customAttribute = currentStatePersonAttributes[customAttributeKey];
|
const customAttribute = currentStatePersonAttributes[customAttributeKey];
|
||||||
expect(customAttribute).toStrictEqual(customAttributeValue);
|
expect(customAttribute).toStrictEqual(customAttributeValue);
|
||||||
});
|
}); */
|
||||||
|
|
||||||
test("Formbricks should track event", async () => {
|
test("Formbricks should track event", async () => {
|
||||||
mockEventTrackResponse();
|
mockEventTrackResponse();
|
||||||
@@ -133,4 +140,3 @@ test("Formbricks should reset", async () => {
|
|||||||
|
|
||||||
expect(Object.keys(currentStatePersonAttributes).length).toBe(0);
|
expect(Object.keys(currentStatePersonAttributes).length).toBe(0);
|
||||||
});
|
});
|
||||||
*/
|
|
||||||
|
|||||||
@@ -66,19 +66,51 @@ export const MAX_SIZES = {
|
|||||||
} as const;
|
} as const;
|
||||||
export const IS_S3_CONFIGURED: boolean =
|
export const IS_S3_CONFIGURED: boolean =
|
||||||
env.S3_ACCESS_KEY && env.S3_SECRET_KEY && env.S3_REGION && env.S3_BUCKET_NAME ? true : false;
|
env.S3_ACCESS_KEY && env.S3_SECRET_KEY && env.S3_REGION && env.S3_BUCKET_NAME ? true : false;
|
||||||
|
export const LOCAL_UPLOAD_URL = {
|
||||||
|
public: new URL(`${WEBAPP_URL}/api/v1/management/storage/local`).href,
|
||||||
|
private: new URL(`${WEBAPP_URL}/api/v1/client/storage/local`).href,
|
||||||
|
} as const;
|
||||||
|
|
||||||
// Pricing
|
// Pricing
|
||||||
export const PRICING_USERTARGETING_FREE_MTU = 2500;
|
export const PRICING_USERTARGETING_FREE_MTU = 2500;
|
||||||
export const PRICING_APPSURVEYS_FREE_RESPONSES = 250;
|
export const PRICING_APPSURVEYS_FREE_RESPONSES = 250;
|
||||||
|
|
||||||
|
// Colors for Survey Bg
|
||||||
|
export const colours = [
|
||||||
|
"#FFF2D8",
|
||||||
|
"#EAD7BB",
|
||||||
|
"#BCA37F",
|
||||||
|
"#113946",
|
||||||
|
"#04364A",
|
||||||
|
"#176B87",
|
||||||
|
"#64CCC5",
|
||||||
|
"#DAFFFB",
|
||||||
|
"#132043",
|
||||||
|
"#1F4172",
|
||||||
|
"#F1B4BB",
|
||||||
|
"#FDF0F0",
|
||||||
|
"#001524",
|
||||||
|
"#445D48",
|
||||||
|
"#D6CC99",
|
||||||
|
"#FDE5D4",
|
||||||
|
"#BEADFA",
|
||||||
|
"#D0BFFF",
|
||||||
|
"#DFCCFB",
|
||||||
|
"#FFF8C9",
|
||||||
|
"#FF8080",
|
||||||
|
"#FFCF96",
|
||||||
|
"#F6FDC3",
|
||||||
|
"#CDFAD5",
|
||||||
|
];
|
||||||
|
|
||||||
// Rate Limiting
|
// Rate Limiting
|
||||||
export const SIGNUP_RATE_LIMIT = {
|
export const SIGNUP_RATE_LIMIT = {
|
||||||
interval: 60 * 60 * 1000, // 60 minutes
|
interval: 60 * 60 * 1000, // 60 minutes
|
||||||
allowedPerInterval: 30,
|
allowedPerInterval: 5,
|
||||||
};
|
};
|
||||||
export const LOGIN_RATE_LIMIT = {
|
export const LOGIN_RATE_LIMIT = {
|
||||||
interval: 15 * 60 * 1000, // 15 minutes
|
interval: 15 * 60 * 1000, // 15 minutes
|
||||||
allowedPerInterval: 30,
|
allowedPerInterval: 5,
|
||||||
};
|
};
|
||||||
export const CLIENT_SIDE_API_RATE_LIMIT = {
|
export const CLIENT_SIDE_API_RATE_LIMIT = {
|
||||||
interval: 10 * 60 * 1000, // 60 minutes
|
interval: 10 * 60 * 1000, // 60 minutes
|
||||||
|
|||||||
@@ -83,10 +83,6 @@ export const getProductByEnvironmentId = async (environmentId: string): Promise<
|
|||||||
select: selectProduct,
|
select: selectProduct,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!productPrisma) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return productPrisma;
|
return productPrisma;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
@@ -268,7 +264,9 @@ export const createProduct = async (
|
|||||||
type: "production",
|
type: "production",
|
||||||
});
|
});
|
||||||
|
|
||||||
return await updateProduct(product.id, {
|
product = await updateProduct(product.id, {
|
||||||
environments: [devEnvironment, prodEnvironment],
|
environments: [devEnvironment, prodEnvironment],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return product;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { access, mkdir, writeFile, readFile, unlink, rmdir } from "fs/promises";
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import mime from "mime";
|
import mime from "mime";
|
||||||
import { env } from "../env.mjs";
|
import { env } from "../env.mjs";
|
||||||
import { IS_S3_CONFIGURED, MAX_SIZES, UPLOADS_DIR, WEBAPP_URL } from "../constants";
|
import { IS_S3_CONFIGURED, LOCAL_UPLOAD_URL, MAX_SIZES, UPLOADS_DIR, WEBAPP_URL } from "../constants";
|
||||||
import { unstable_cache } from "next/cache";
|
import { unstable_cache } from "next/cache";
|
||||||
import { storageCache } from "./cache";
|
import { storageCache } from "./cache";
|
||||||
import { TAccessType } from "@formbricks/types/storage";
|
import { TAccessType } from "@formbricks/types/storage";
|
||||||
@@ -175,10 +175,7 @@ export const getUploadSignedUrl = async (
|
|||||||
const { signature, timestamp, uuid } = generateLocalSignedUrl(fileName, environmentId, fileType);
|
const { signature, timestamp, uuid } = generateLocalSignedUrl(fileName, environmentId, fileType);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
signedUrl:
|
signedUrl: LOCAL_UPLOAD_URL[accessType],
|
||||||
accessType === "private"
|
|
||||||
? new URL(`${WEBAPP_URL}/api/v1/client/${environmentId}/storage/local`).href
|
|
||||||
: new URL(`${WEBAPP_URL}/api/v1/management/storage/local`).href,
|
|
||||||
signingData: {
|
signingData: {
|
||||||
signature,
|
signature,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const selectSurvey = {
|
|||||||
verifyEmail: true,
|
verifyEmail: true,
|
||||||
redirectUrl: true,
|
redirectUrl: true,
|
||||||
productOverwrites: true,
|
productOverwrites: true,
|
||||||
|
surveyBackground: true,
|
||||||
surveyClosedMessage: true,
|
surveyClosedMessage: true,
|
||||||
singleUse: true,
|
singleUse: true,
|
||||||
pin: true,
|
pin: true,
|
||||||
@@ -585,6 +586,9 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string) =
|
|||||||
productOverwrites: existingSurvey.productOverwrites
|
productOverwrites: existingSurvey.productOverwrites
|
||||||
? JSON.parse(JSON.stringify(existingSurvey.productOverwrites))
|
? JSON.parse(JSON.stringify(existingSurvey.productOverwrites))
|
||||||
: Prisma.JsonNull,
|
: Prisma.JsonNull,
|
||||||
|
surveyBackground: existingSurvey.surveyBackground
|
||||||
|
? JSON.parse(JSON.stringify(existingSurvey.surveyBackground))
|
||||||
|
: Prisma.JsonNull,
|
||||||
verifyEmail: existingSurvey.verifyEmail
|
verifyEmail: existingSurvey.verifyEmail
|
||||||
? JSON.parse(JSON.stringify(existingSurvey.verifyEmail))
|
? JSON.parse(JSON.stringify(existingSurvey.verifyEmail))
|
||||||
: Prisma.JsonNull,
|
: Prisma.JsonNull,
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
"use server";
|
|
||||||
import "server-only";
|
|
||||||
|
|
||||||
import { AuthenticationError, ResourceNotFoundError } from "@formbricks/types/errors";
|
|
||||||
import { authOptions } from "../../authOptions";
|
|
||||||
import { getServerSession } from "next-auth";
|
|
||||||
import { getTeam, getTeamBillingInfo } from "../service";
|
|
||||||
|
|
||||||
export const getTeamBillingInfoAction = async (teamId: string) => {
|
|
||||||
const session = await getServerSession(authOptions);
|
|
||||||
const team = await getTeam(teamId);
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
throw new AuthenticationError("Not authenticated");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!team) {
|
|
||||||
throw new ResourceNotFoundError("Team", teamId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await getTeamBillingInfo(teamId);
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { TTeamBilling } from "@formbricks/types/teams";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { getTeamBillingInfoAction } from "./actions";
|
|
||||||
|
|
||||||
export const useGetBillingInfo = (teamId: string) => {
|
|
||||||
const [billingInfo, setBillingInfo] = useState<TTeamBilling>();
|
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
||||||
const [error, setError] = useState<string>("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getBillingInfo = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true);
|
|
||||||
const billingInfo = await getTeamBillingInfoAction(teamId);
|
|
||||||
|
|
||||||
if (!billingInfo) {
|
|
||||||
setError("No billing info found");
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(false);
|
|
||||||
setBillingInfo(billingInfo);
|
|
||||||
} catch (err: any) {
|
|
||||||
setIsLoading(false);
|
|
||||||
setError(err.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
getBillingInfo();
|
|
||||||
}, [teamId]);
|
|
||||||
|
|
||||||
return { billingInfo, isLoading, error };
|
|
||||||
};
|
|
||||||
@@ -4,7 +4,7 @@ import { prisma } from "@formbricks/database";
|
|||||||
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
|
import { ZOptionalNumber, ZString } from "@formbricks/types/common";
|
||||||
import { ZId } from "@formbricks/types/environment";
|
import { ZId } from "@formbricks/types/environment";
|
||||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||||
import { TTeam, TTeamBilling, TTeamUpdateInput, ZTeamUpdateInput } from "@formbricks/types/teams";
|
import { TTeam, TTeamUpdateInput, ZTeamUpdateInput } from "@formbricks/types/teams";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { unstable_cache } from "next/cache";
|
import { unstable_cache } from "next/cache";
|
||||||
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||||
@@ -361,21 +361,3 @@ export const getMonthlyTeamResponseCount = async (teamId: string): Promise<numbe
|
|||||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||||
}
|
}
|
||||||
)();
|
)();
|
||||||
|
|
||||||
export const getTeamBillingInfo = async (teamId: string): Promise<TTeamBilling | null> =>
|
|
||||||
await unstable_cache(
|
|
||||||
async () => {
|
|
||||||
const billingInfo = await prisma.team.findUnique({
|
|
||||||
where: {
|
|
||||||
id: teamId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return billingInfo?.billing ?? null;
|
|
||||||
},
|
|
||||||
[`getTeamBillingInfo-${teamId}`],
|
|
||||||
{
|
|
||||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
|
||||||
tags: [teamCache.tag.byId(teamId)],
|
|
||||||
}
|
|
||||||
)();
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
"clean": "rimraf .turbo node_modules dist"
|
"clean": "rimraf .turbo node_modules dist"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@formbricks/lib": "workspace:*",
|
|
||||||
"@formbricks/tsconfig": "workspace:*",
|
"@formbricks/tsconfig": "workspace:*",
|
||||||
"@formbricks/types": "workspace:*",
|
"@formbricks/types": "workspace:*",
|
||||||
"@preact/preset-vite": "^2.7.0",
|
"@preact/preset-vite": "^2.7.0",
|
||||||
|
|||||||
@@ -1,295 +0,0 @@
|
|||||||
import { TAllowedFileExtension } from "@formbricks/types/common";
|
|
||||||
import { useMemo } from "preact/hooks";
|
|
||||||
import { JSXInternal } from "preact/src/jsx";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
|
||||||
|
|
||||||
interface MultipleFileInputProps {
|
|
||||||
allowedFileExtensions?: TAllowedFileExtension[];
|
|
||||||
surveyId: string | undefined;
|
|
||||||
onUploadCallback: (uploadedUrls: string[]) => void;
|
|
||||||
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
|
|
||||||
fileUrls: string[] | undefined;
|
|
||||||
maxSizeInMB?: number;
|
|
||||||
allowMultipleFiles?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FileInput({
|
|
||||||
allowedFileExtensions,
|
|
||||||
surveyId,
|
|
||||||
onUploadCallback,
|
|
||||||
onFileUpload,
|
|
||||||
fileUrls,
|
|
||||||
maxSizeInMB,
|
|
||||||
allowMultipleFiles,
|
|
||||||
}: MultipleFileInputProps) {
|
|
||||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
|
||||||
|
|
||||||
const handleFileUpload = async (file: File) => {
|
|
||||||
if (file) {
|
|
||||||
if (maxSizeInMB) {
|
|
||||||
const fileBuffer = await file.arrayBuffer();
|
|
||||||
|
|
||||||
const bufferBytes = fileBuffer.byteLength;
|
|
||||||
const bufferKB = bufferBytes / 1024;
|
|
||||||
if (bufferKB > maxSizeInMB * 1024) {
|
|
||||||
alert(`File should be less than ${maxSizeInMB} MB`);
|
|
||||||
} else {
|
|
||||||
setIsUploading(true);
|
|
||||||
try {
|
|
||||||
const response = await onFileUpload(file, { allowedFileExtensions, surveyId });
|
|
||||||
setSelectedFiles([...selectedFiles, file]);
|
|
||||||
|
|
||||||
setIsUploading(false);
|
|
||||||
if (fileUrls) {
|
|
||||||
onUploadCallback([...fileUrls, response]);
|
|
||||||
} else {
|
|
||||||
onUploadCallback([response]);
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setIsUploading(false);
|
|
||||||
if (err.message === "File size exceeds the 10 MB limit") {
|
|
||||||
alert(err.message);
|
|
||||||
} else {
|
|
||||||
alert("Upload failed! Please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setIsUploading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await onFileUpload(file, { allowedFileExtensions, surveyId });
|
|
||||||
|
|
||||||
setSelectedFiles([...selectedFiles, file]);
|
|
||||||
setIsUploading(false);
|
|
||||||
if (fileUrls) {
|
|
||||||
onUploadCallback([...fileUrls, response]);
|
|
||||||
} else {
|
|
||||||
onUploadCallback([response]);
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setIsUploading(false);
|
|
||||||
if (err.message === "File size exceeds the 10 MB limit") {
|
|
||||||
alert(err.message);
|
|
||||||
} else {
|
|
||||||
alert("Upload failed! Please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
alert("Please select a file");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragOver = (e: JSXInternal.TargetedDragEvent<HTMLLabelElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
e.dataTransfer.dropEffect = "copy";
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = async (e: JSXInternal.TargetedDragEvent<HTMLLabelElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// @ts-expect-error
|
|
||||||
const files = Array.from(e.dataTransfer.files);
|
|
||||||
|
|
||||||
if (!allowMultipleFiles && files.length > 1) {
|
|
||||||
alert("Only one file can be uploaded at a time.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (files.length > 0) {
|
|
||||||
const validFiles = files.filter((file) =>
|
|
||||||
allowedFileExtensions && allowedFileExtensions.length > 0
|
|
||||||
? allowedFileExtensions.includes(
|
|
||||||
file.type.substring(file.type.lastIndexOf("/") + 1) as TAllowedFileExtension
|
|
||||||
)
|
|
||||||
: true
|
|
||||||
);
|
|
||||||
|
|
||||||
if (validFiles.length > 0) {
|
|
||||||
const uploadedUrls: string[] = [];
|
|
||||||
|
|
||||||
for (const file of validFiles) {
|
|
||||||
if (maxSizeInMB) {
|
|
||||||
const fileBuffer = await file.arrayBuffer();
|
|
||||||
|
|
||||||
const bufferBytes = fileBuffer.byteLength;
|
|
||||||
const bufferKB = bufferBytes / 1024;
|
|
||||||
|
|
||||||
if (bufferKB > maxSizeInMB * 1024) {
|
|
||||||
alert(`File should be less than ${maxSizeInMB} MB`);
|
|
||||||
} else {
|
|
||||||
setIsUploading(true);
|
|
||||||
try {
|
|
||||||
const response = await onFileUpload(file, { allowedFileExtensions, surveyId });
|
|
||||||
setSelectedFiles([...selectedFiles, file]);
|
|
||||||
|
|
||||||
uploadedUrls.push(response);
|
|
||||||
} catch (err: any) {
|
|
||||||
setIsUploading(false);
|
|
||||||
if (err.message === "File size exceeds the 10 MB limit") {
|
|
||||||
alert(err.message);
|
|
||||||
} else {
|
|
||||||
alert("Upload failed! Please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setIsUploading(true);
|
|
||||||
try {
|
|
||||||
const response = await onFileUpload(file, { allowedFileExtensions, surveyId });
|
|
||||||
setSelectedFiles([...selectedFiles, file]);
|
|
||||||
|
|
||||||
uploadedUrls.push(response);
|
|
||||||
} catch (err: any) {
|
|
||||||
setIsUploading(false);
|
|
||||||
if (err.message === "File size exceeds the 10 MB limit") {
|
|
||||||
alert(err.message);
|
|
||||||
} else {
|
|
||||||
alert("Upload failed! Please try again.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsUploading(false);
|
|
||||||
if (fileUrls) {
|
|
||||||
onUploadCallback([...fileUrls, ...uploadedUrls]);
|
|
||||||
} else {
|
|
||||||
onUploadCallback(uploadedUrls);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
alert("no selected files are valid");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteFile = (index: number, event: JSXInternal.TargetedMouseEvent<SVGSVGElement>) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
if (fileUrls) {
|
|
||||||
const newFiles = [...selectedFiles];
|
|
||||||
newFiles.splice(index, 1);
|
|
||||||
setSelectedFiles(newFiles);
|
|
||||||
const updatedFileUrls = [...fileUrls];
|
|
||||||
updatedFileUrls.splice(index, 1);
|
|
||||||
onUploadCallback(updatedFileUrls);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const showUploader = useMemo(() => {
|
|
||||||
if (isUploading) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowMultipleFiles) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileUrls && fileUrls.length > 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}, [allowMultipleFiles, fileUrls, isUploading]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="items-left relative mt-3 flex w-full cursor-pointer flex-col justify-center rounded-lg border-2 border-dashed border-slate-300 bg-slate-50 hover:bg-slate-100 dark:border-slate-600 dark:bg-slate-700 dark:hover:border-slate-500 dark:hover:bg-slate-800">
|
|
||||||
<div>
|
|
||||||
{fileUrls &&
|
|
||||||
fileUrls?.map((file, index) => (
|
|
||||||
<div key={index} className="relative m-2 rounded-md bg-slate-200">
|
|
||||||
<div className="absolute right-0 top-0 m-2">
|
|
||||||
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-slate-100 hover:bg-slate-50">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 26 26"
|
|
||||||
strokeWidth={1}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-5 text-slate-700 hover:text-slate-900"
|
|
||||||
onClick={(e) => handleDeleteFile(index, e)}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9l10 10m0-10L9 19" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col items-center justify-center p-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="lucide lucide-file"
|
|
||||||
className="h-6 text-slate-500">
|
|
||||||
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
|
||||||
<polyline points="14 2 14 8 20 8" />
|
|
||||||
</svg>
|
|
||||||
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">
|
|
||||||
{decodeURIComponent(file).split("/").pop()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{isUploading && (
|
|
||||||
<div className="inset-0 flex animate-pulse items-center justify-center rounded-lg bg-slate-100 py-4">
|
|
||||||
<label htmlFor="selectedFile" className="text-sm font-medium text-slate-500">
|
|
||||||
Uploading...
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<label htmlFor="selectedFile" onDragOver={(e) => handleDragOver(e)} onDrop={(e) => handleDrop(e)}>
|
|
||||||
{showUploader && (
|
|
||||||
<div className="flex flex-col items-center justify-center py-6">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
stroke="currentColor"
|
|
||||||
className="h-6 text-slate-500">
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
|
||||||
<span className="font-medium">Click or drag to upload files.</span>
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="selectedFile"
|
|
||||||
name="selectedFile"
|
|
||||||
accept={allowedFileExtensions?.map((ext) => `.${ext}`).join(",")}
|
|
||||||
className="hidden"
|
|
||||||
onChange={(e) => {
|
|
||||||
const inputElement = e.target as HTMLInputElement; // Cast e.target to HTMLInputElement
|
|
||||||
if (inputElement.files) {
|
|
||||||
handleFileUpload(inputElement.files[0]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -13,11 +13,10 @@ export default function Headline({
|
|||||||
}: HeadlineProps) {
|
}: HeadlineProps) {
|
||||||
return (
|
return (
|
||||||
<label htmlFor={questionId} className="text-heading mb-1.5 block text-base font-semibold leading-6">
|
<label htmlFor={questionId} className="text-heading mb-1.5 block text-base font-semibold leading-6">
|
||||||
<div
|
<div className={`flex items-center ${alignTextCenter ? "justify-center" : "justify-between"}`}>
|
||||||
className={`flex items-center ${alignTextCenter ? "justify-center" : "mr-[3ch] justify-between"}`}>
|
|
||||||
{headline}
|
{headline}
|
||||||
{!required && (
|
{!required && (
|
||||||
<span className="text-info-text self-start text-sm font-normal leading-7" tabIndex={-1}>
|
<span className="text-info-text ml-2 self-start text-sm font-normal leading-7" tabIndex={-1}>
|
||||||
Optional
|
Optional
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
|
import { TResponseData } from "@formbricks/types/responses";
|
||||||
|
import { TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||||
import CTAQuestion from "@/components/questions/CTAQuestion";
|
import CTAQuestion from "@/components/questions/CTAQuestion";
|
||||||
import ConsentQuestion from "@/components/questions/ConsentQuestion";
|
import ConsentQuestion from "@/components/questions/ConsentQuestion";
|
||||||
import FileUploadQuestion from "@/components/questions/FileUploadQuestion";
|
|
||||||
import MultipleChoiceMultiQuestion from "@/components/questions/MultipleChoiceMultiQuestion";
|
import MultipleChoiceMultiQuestion from "@/components/questions/MultipleChoiceMultiQuestion";
|
||||||
import MultipleChoiceSingleQuestion from "@/components/questions/MultipleChoiceSingleQuestion";
|
import MultipleChoiceSingleQuestion from "@/components/questions/MultipleChoiceSingleQuestion";
|
||||||
import NPSQuestion from "@/components/questions/NPSQuestion";
|
import NPSQuestion from "@/components/questions/NPSQuestion";
|
||||||
import OpenTextQuestion from "@/components/questions/OpenTextQuestion";
|
import OpenTextQuestion from "@/components/questions/OpenTextQuestion";
|
||||||
import PictureSelectionQuestion from "@/components/questions/PictureSelectionQuestion";
|
import PictureSelectionQuestion from "@/components/questions/PictureSelectionQuestion";
|
||||||
import RatingQuestion from "@/components/questions/RatingQuestion";
|
import RatingQuestion from "@/components/questions/RatingQuestion";
|
||||||
import { TResponseData } from "@formbricks/types/responses";
|
|
||||||
import { TUploadFileConfig } from "@formbricks/types/storage";
|
|
||||||
import { TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
|
|
||||||
|
|
||||||
interface QuestionConditionalProps {
|
interface QuestionConditionalProps {
|
||||||
question: TSurveyQuestion;
|
question: TSurveyQuestion;
|
||||||
@@ -17,11 +15,9 @@ interface QuestionConditionalProps {
|
|||||||
onChange: (responseData: TResponseData) => void;
|
onChange: (responseData: TResponseData) => void;
|
||||||
onSubmit: (data: TResponseData) => void;
|
onSubmit: (data: TResponseData) => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onFileUpload: (file: File, config?: TUploadFileConfig) => Promise<string>;
|
|
||||||
isFirstQuestion: boolean;
|
isFirstQuestion: boolean;
|
||||||
isLastQuestion: boolean;
|
isLastQuestion: boolean;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
surveyId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function QuestionConditional({
|
export default function QuestionConditional({
|
||||||
@@ -33,8 +29,6 @@ export default function QuestionConditional({
|
|||||||
isFirstQuestion,
|
isFirstQuestion,
|
||||||
isLastQuestion,
|
isLastQuestion,
|
||||||
autoFocus = true,
|
autoFocus = true,
|
||||||
surveyId,
|
|
||||||
onFileUpload,
|
|
||||||
}: QuestionConditionalProps) {
|
}: QuestionConditionalProps) {
|
||||||
return question.type === TSurveyQuestionType.OpenText ? (
|
return question.type === TSurveyQuestionType.OpenText ? (
|
||||||
<OpenTextQuestion
|
<OpenTextQuestion
|
||||||
@@ -117,17 +111,5 @@ export default function QuestionConditional({
|
|||||||
isFirstQuestion={isFirstQuestion}
|
isFirstQuestion={isFirstQuestion}
|
||||||
isLastQuestion={isLastQuestion}
|
isLastQuestion={isLastQuestion}
|
||||||
/>
|
/>
|
||||||
) : question.type === TSurveyQuestionType.FileUpload ? (
|
|
||||||
<FileUploadQuestion
|
|
||||||
surveyId={surveyId}
|
|
||||||
question={question}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
onBack={onBack}
|
|
||||||
isFirstQuestion={isFirstQuestion}
|
|
||||||
isLastQuestion={isLastQuestion}
|
|
||||||
onFileUpload={onFileUpload}
|
|
||||||
/>
|
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
interface QuestionImageProps {
|
|
||||||
imgUrl: string;
|
|
||||||
altText?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function QuestionImage({ imgUrl, altText = "Image" }: QuestionImageProps) {
|
|
||||||
return (
|
|
||||||
<div className="mb-4 rounded-md">
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img src={imgUrl} alt={altText} className="mb-4 rounded-md" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,6 @@ export function Survey({
|
|||||||
onFinished = () => {},
|
onFinished = () => {},
|
||||||
isRedirectDisabled = false,
|
isRedirectDisabled = false,
|
||||||
prefillResponseData,
|
prefillResponseData,
|
||||||
onFileUpload,
|
|
||||||
}: SurveyBaseProps) {
|
}: SurveyBaseProps) {
|
||||||
const [questionId, setQuestionId] = useState(
|
const [questionId, setQuestionId] = useState(
|
||||||
activeQuestionId || (survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id)
|
activeQuestionId || (survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id)
|
||||||
@@ -147,13 +146,11 @@ export function Survey({
|
|||||||
return (
|
return (
|
||||||
currQues && (
|
currQues && (
|
||||||
<QuestionConditional
|
<QuestionConditional
|
||||||
surveyId={survey.id}
|
|
||||||
question={currQues}
|
question={currQues}
|
||||||
value={responseData[currQues.id]}
|
value={responseData[currQues.id]}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
onFileUpload={onFileUpload}
|
|
||||||
isFirstQuestion={
|
isFirstQuestion={
|
||||||
history && prefillResponseData
|
history && prefillResponseData
|
||||||
? history[history.length - 1] === survey.questions[0].id
|
? history[history.length - 1] === survey.questions[0].id
|
||||||
@@ -169,7 +166,7 @@ export function Survey({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AutoCloseWrapper survey={survey} onClose={onClose}>
|
<AutoCloseWrapper survey={survey} onClose={onClose}>
|
||||||
<div className="flex h-full w-full flex-col justify-between bg-[--fb-survey-background-color] px-6 pb-3 pt-6">
|
<div className="flex h-full w-full flex-col justify-between rounded-lg bg-[--fb-survey-background-color] px-6 pb-3 pt-6">
|
||||||
<div ref={contentRef} className={cn(loadingElement ? "animate-pulse opacity-60" : "", "my-auto")}>
|
<div ref={contentRef} className={cn(loadingElement ? "animate-pulse opacity-60" : "", "my-auto")}>
|
||||||
{survey.questions.length === 0 && !survey.welcomeCard.enabled && !survey.thankYouCard.enabled ? (
|
{survey.questions.length === 0 && !survey.welcomeCard.enabled && !survey.thankYouCard.enabled ? (
|
||||||
// Handle the case when there are no questions and both welcome and thank you cards are disabled
|
// Handle the case when there are no questions and both welcome and thank you cards are disabled
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export function SurveyInline({
|
|||||||
onClose = () => {},
|
onClose = () => {},
|
||||||
prefillResponseData,
|
prefillResponseData,
|
||||||
isRedirectDisabled = false,
|
isRedirectDisabled = false,
|
||||||
onFileUpload,
|
|
||||||
}: SurveyBaseProps) {
|
}: SurveyBaseProps) {
|
||||||
return (
|
return (
|
||||||
<div id="fbjs" className="formbricks-form h-full w-full">
|
<div id="fbjs" className="formbricks-form h-full w-full">
|
||||||
@@ -25,7 +24,6 @@ export function SurveyInline({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
prefillResponseData={prefillResponseData}
|
prefillResponseData={prefillResponseData}
|
||||||
isRedirectDisabled={isRedirectDisabled}
|
isRedirectDisabled={isRedirectDisabled}
|
||||||
onFileUpload={onFileUpload}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export function SurveyModal({
|
|||||||
onResponse = () => {},
|
onResponse = () => {},
|
||||||
onClose = () => {},
|
onClose = () => {},
|
||||||
onFinished = () => {},
|
onFinished = () => {},
|
||||||
onFileUpload,
|
|
||||||
isRedirectDisabled = false,
|
isRedirectDisabled = false,
|
||||||
}: SurveyModalProps) {
|
}: SurveyModalProps) {
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
@@ -53,7 +52,6 @@ export function SurveyModal({
|
|||||||
}
|
}
|
||||||
}, 4000); // close modal automatically after 4 seconds
|
}, 4000); // close modal automatically after 4 seconds
|
||||||
}}
|
}}
|
||||||
onFileUpload={onFileUpload}
|
|
||||||
isRedirectDisabled={isRedirectDisabled}
|
isRedirectDisabled={isRedirectDisabled}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { BackButton } from "@/components/buttons/BackButton";
|
import { BackButton } from "@/components/buttons/BackButton";
|
||||||
import SubmitButton from "@/components/buttons/SubmitButton";
|
import SubmitButton from "@/components/buttons/SubmitButton";
|
||||||
import QuestionImage from "@/components/general/QuestionImage";
|
|
||||||
import Headline from "@/components/general/Headline";
|
import Headline from "@/components/general/Headline";
|
||||||
import HtmlBody from "@/components/general/HtmlBody";
|
import HtmlBody from "@/components/general/HtmlBody";
|
||||||
import { TResponseData } from "@formbricks/types/responses";
|
import { TResponseData } from "@formbricks/types/responses";
|
||||||
@@ -25,7 +24,12 @@ export default function CTAQuestion({
|
|||||||
}: CTAQuestionProps) {
|
}: CTAQuestionProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{question.imageUrl && <QuestionImage imgUrl={question.imageUrl} />}
|
{question.imageUrl && (
|
||||||
|
<div className="my-4 rounded-md">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img src={question.imageUrl} alt="question-image" className={"my-4 rounded-md"} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Headline headline={question.headline} questionId={question.id} required={question.required} />
|
<Headline headline={question.headline} questionId={question.id} required={question.required} />
|
||||||
<HtmlBody htmlString={question.html} questionId={question.id} />
|
<HtmlBody htmlString={question.html} questionId={question.id} />
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user