Compare commits

..

2 Commits

Author SHA1 Message Date
Matthias Nannt
31900f62b3 update target 2024-04-24 12:53:45 +02:00
Matthias Nannt
57de7adeb8 chore: add prisma build targets for vercel 2024-04-24 12:30:56 +02:00
223 changed files with 12881 additions and 18414 deletions

View File

@@ -173,9 +173,6 @@ ENTERPRISE_LICENSE_KEY=
# OpenTelemetry URL for tracing
# OPENTELEMETRY_LISTENER_URL=http://localhost:4318/v1/traces
# Unsplash API Key
UNSPLASH_ACCESS_KEY=
# The below is used for Next Caching (uses In-Memory from Next Cache if not provided)
# REDIS_URL:

View File

@@ -1,25 +0,0 @@
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@formbricks/ui/Select";
interface SurveySwitchProps {
value: "website" | "app";
formbricks: any;
}
export const SurveySwitch = ({ value, formbricks }: SurveySwitchProps) => {
return (
<Select
value={value}
onValueChange={(v) => {
formbricks.logout();
window.location.href = `/${v}`;
}}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="website">Website Surveys</SelectItem>
<SelectItem value="app">App Surveys</SelectItem>
</SelectContent>
</Select>
);
};

View File

@@ -1,7 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
transpilePackages: ["@formbricks/ui"],
async redirects() {
return [
{

View File

@@ -12,9 +12,8 @@
},
"dependencies": {
"@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*",
"lucide-react": "^0.373.0",
"next": "14.2.3",
"lucide-react": "^0.368.0",
"next": "14.2.1",
"react": "18.2.0",
"react-dom": "18.2.0"
},

View File

@@ -1,10 +1,10 @@
import { EarthIcon } from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import formbricksApp from "@formbricks/js/app";
import { SurveySwitch } from "../../components/SurveySwitch";
import fbsetup from "../../public/fb-setup.png";
declare const window: any;
@@ -57,11 +57,27 @@ export default function AppPage({}) {
}
});
const removeFormbricksContainer = () => {
document.getElementById("formbricks-modal-container")?.remove();
document.getElementById("formbricks-app-container")?.remove();
localStorage.removeItem("formbricks-js-app");
};
return (
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
<div className="flex flex-col justify-between md:flex-row">
<div className="flex items-center gap-2">
<SurveySwitch value="app" formbricks={formbricksApp} />
<button
className="rounded-lg bg-[#038178] p-2 text-white focus:outline-none focus:ring-2 focus:ring-slate-900 focus:ring-offset-1"
onClick={() => {
removeFormbricksContainer();
window.location.href = "/website";
}}>
<div className="flex items-center gap-2">
<EarthIcon className="h-10 w-10" />
<span>Website Demo</span>
</div>
</button>
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
Formbricks In-product Survey Demo App

View File

@@ -1,10 +1,10 @@
import { MonitorIcon } from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import formbricksWebsite from "@formbricks/js/website";
import { SurveySwitch } from "../../components/SurveySwitch";
import fbsetup from "../../public/fb-setup.png";
declare const window: any;
@@ -57,11 +57,27 @@ export default function AppPage({}) {
}
});
const removeFormbricksContainer = () => {
document.getElementById("formbricks-modal-container")?.remove();
document.getElementById("formbricks-website-container")?.remove();
localStorage.removeItem("formbricks-js-website");
};
return (
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
<div className="flex flex-col items-center justify-between md:flex-row">
<div className="flex items-center gap-2">
<SurveySwitch value="website" formbricks={formbricksWebsite} />
<button
className="rounded-lg bg-[#038178] p-2 text-white focus:outline-none focus:ring-2 focus:ring-slate-900 focus:ring-offset-1"
onClick={() => {
removeFormbricksContainer();
window.location.href = "/app";
}}>
<div className="flex items-center gap-2">
<MonitorIcon className="h-10 w-10" />
<span>App Demo</span>
</div>
</button>
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
Formbricks Website Survey Demo App

View File

@@ -1,137 +0,0 @@
import { MdxImage } from "@/components/shared/MdxImage";
import AddApiKey from "./add-api-key.webp";
import ApiKeySecret from "./api-key-secret.webp";
export const metadata = {
title: "Formbricks API Overview: Public Client & Management API Breakdown",
description:
"Formbricks provides a powerful API to manage your surveys, responses, users, displays, actions, attributes & webhooks programmatically. Get a detailed understanding of Formbricks' dual API offerings: the unauthenticated Public Client API optimized for client-side tasks and the secured Management API for advanced account operations. Choose the perfect fit for your integration needs and ensure robust data handling",
};
#### API
# API Overview
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.
View our [API Documentation](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh) in more than 30 frameworks and languages. Or directly try out our APIs in Postman by clicking the button below:
<div className="max-w-full sm:max-w-3xl">
<a target="_blank" href="https://formbricks.postman.co/collection/11026000-927c954f-85a9-4f8f-b0ec-14191b903737?source=rip_html">
<img alt="Run in Postman" src="https://run.pstmn.io/button.svg"/>
</a>
</div>
## Public Client API
The [Public Client API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#5c981d9e-5e7d-455d-9795-b9c45bc2f930) is designed for our SDKs and **does not require authentication**. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
We currently have the following Client API methods exposed and below is their documentation attached in Postman:
- [Actions API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#b8f3a10e-1642-4d82-a629-fef0a8c6c86c) - Create actions for a Person
- [Displays API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#949272bf-daec-4d72-9b52-47af3d74a62c) - Mark Survey as Displayed or Update an existing Display by linking it with a Response for a Person
- [People API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#ee3d2188-4253-4bca-9238-6b76455805a9) - Create & Update a Person (e.g. attributes, email, userId, etc)
- [Responses API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#8c773032-536c-483c-a237-c7697347946e) - Create & Update a Response for a Survey
## Management API
The [Management API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#98fce5a1-1365-4125-8de1-acdb28206766) provides access to all data and settings that your account has access to in the Formbricks app. This API **requires a personal API Key** for authentication, which can be generated in the Settings section of the Formbricks app. Checkout the [API Key Setup](#how-to-generate-an-api-key) below to generate & manage API Keys.
We currently have the following Management API methods exposed and below is their documentation attached in Postman:
- [Action Class API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#81947f69-99fc-41c9-a184-f3260e02be48) - Create, List, and Delete Action Classes
- [Attribute Class API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#31089010-d468-4a7c-943e-8ebe71b9a36e) - Create, List, and Delete Attribute Classes
- [Me API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#79e08365-641d-4b2d-aea2-9a855e0438ec) - Retrieve Account Information
- [People API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#cffc27a6-dafb-428f-8ea7-5165bedb911e) - List and Delete People
- [Response API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#e544ec0d-8b30-4e33-8d35-2441cb40d676) - List, List by Survey, Update, and Delete Responses
- [Survey API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#953189b2-37b5-4429-a7bd-f4d01ceae242) - List, Create, Update, and Delete Surveys
- [Webhook API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#62e6ec65-021b-42a4-ac93-d1434b393c6c) - List, Create, and Delete Webhooks
## How to Generate an API key
The API requests are authorized with a personal API key. This API key gives you the same rights as if you were logged in at formbricks.com - **don't share it around!**
1. Go to your settings on [app.formbricks.com](https://app.formbricks.com).
2. Go to page “API keys”
<MdxImage src={AddApiKey} alt="Add API Key" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
3. Create a key for the development or production environment.
4. Copy the key immediately. You wont be able to see it again.
<MdxImage
src={ApiKeySecret}
alt="API Key Secret"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<Note>
### Store API key safely!
Anyone who has your API key has full control over your account. For security reasons, you cannot view the API key again.
</Note>
### Test your API Key
Hit the below request to verify that you are authenticated with your API Key and the server is responding.
## Get My Profile {{ tag: 'GET', label: '/api/v1/me' }}
<Row>
<Col>
Get the product details and environment type of your account.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Delete a personal API key
1. Go to settings on [app.formbricks.com](https://app.formbricks.com/).
2. Go to page “API keys”.
3. Find the key you wish to revoke and select “Delete”.
4. Your API key will stop working immediately.
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/me">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/me' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"id": "cll2m30r70004mx0huqkitgqv",
"createdAt": "2023-08-08T18:04:59.922Z",
"updatedAt": "2023-08-08T18:04:59.922Z",
"type": "production",
"product": {
"id": "cll2m30r60003mx0hnemjfckr",
"name": "My Product"
},
"widgetSetupCompleted": false
}
```
```json {{ title: '401 Not Authenticated' }}
Not authenticated
```
</CodeGroup>
</Col>
</Row>
Cant figure it out? Join our [Discord](https://discord.com/invite/3YFcABF2Ts) and we'd be glad to assist you!
---

View File

@@ -0,0 +1,94 @@
import { Fence } from "@/components/shared/Fence";
export const metadata = {
title: "Formbricks Actions API Documentation - Manage Your Survey Data Seamlessly",
description:
"Unlock the full potential of Formbricks' Client Actions API. Create Actions right from the API.",
};
#### Client API
# Actions API
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
This API can be used to:
- [Add Action for User](#add-action-for-user)
---
## Add Action for User {{ tag: 'POST', label: '/api/v1/client/<environment-id>/actions' }}
Adds an Actions for a given User by their User ID
<Row>
<Col>
### Mandatory Body Fields
<Properties>
<Property name="userId" type="string">
The id of the user for whom the action is being created.
Note: A user with this ID must exist in your environment in Formbricks.
</Property>
<Property name="name" type="string">
The name of the Action being created.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/client/<environment-id>/actions">
```bash {{ title: 'cURL' }}
curl --location --request POST 'https://app.formbricks.com/api/v1/client/<environment-id>/actions' \
--data-raw '{
"userId": "1",
"name": "new_action_v2"
}'
```
```json {{ title: 'Example Request Body' }}
{
"userId": "1",
"name": "new_action_v3"
}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "Fields are missing or incorrectly formatted",
"details": {
"name": "Required"
}
}
```
```json {{ title: '500 Internal Server Error' }}
{
"code": "internal_server_error",
"message": "Unable to handle the request: Database operation failed",
"details": {}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,145 @@
import { Fence } from "@/components/shared/Fence";
export const metadata = {
title: "Formbricks Public Client API Guide: Manage Survey Displays & Responses",
description:
"Dive deep into Formbricks' Public Client API designed for customisation. This comprehensive guide provides detailed instructions on how to mark create and update survey displays for users.",
};
#### Client API
# Displays API
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
This set of API can be used to
- [Create Display](#create-display)
- [Update Display](#update-display)
---
## Create Display {{ tag: 'POST', label: '/api/v1/client/<environment-id>/diplays' }}
<Row>
<Col>
Create Display of survey for a user
### Mandatory Request Body JSON Keys
<Properties>
<Property name="surveyId" type="string">
Survey ID to mark as viewed for a person
</Property>
</Properties>
### Optional Request Body JSON Keys
<Properties>
<Property name="userId" type="string">
Already existing user's ID to mark as viewed for a survey
</Property>
<Property name="responseId" type="string">
Already existing response's ID to link with this new Display
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/client/<environment-id>/displays">
```bash {{ title: 'cURL' }}
curl -X POST \
'https://app.formbricks.com/api/v1/client/displays' \
-H 'Content-Type: application/json' \
-d '{
"surveyId":"<survey-id>",
"userId":"<user-id>"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "clphzz6oo00083zdmc7e0nwzi"
}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "Fields are missing or incorrectly formatted",
"details": {
"surveyId": "Required"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Update Display {{ tag: 'PUT', label: '/api/v1/client/<environment-id>/diplays/<display-id>' }}
<Row>
<Col>
Update a display by it's ID
### Optional Request Body JSON Keys
<Properties>
<Property name="userId" type="string">
Already existing user's ID to mark as viewed for a survey
</Property>
<Property name="responseId" type="string">
Already existing response's ID to link with this new Display
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="PUT" label="/api/v1/client/<environment-id>/displays/<display-id>">
```bash {{ title: 'cURL' }}
curl -X POST \
'https://app.formbricks.com/api/v1/client/<environment-id>/displays/<display-id>' \
-H 'Content-Type: application/json' \
-d '{
"userId":"<user-id>"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "Fields are missing or incorrectly formatted",
"details": {
"surveyId": "Required"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,47 @@
export const metadata = {
title: "Formbricks API Overview: Public Client & Management API Breakdown",
description:
"Get a detailed understanding of Formbricks' dual API offerings: the unauthenticated Public Client API optimized for client-side tasks and the secured Management API for advanced account operations. Choose the perfect fit for your integration needs and ensure robust data handling",
};
#### API
# API Overview
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.
## Public Client 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.
- [Actions API](/docs/api/client/actions) - Create actions for a person
- [Displays API](/docs/api/client/displays) - Mark Survey as Displayed or Responded for a Person
- [People API](/docs/api/client/people) - Create & update people (e.g. attributes)
- [Responses API](/docs/api/client/responses) - Create & update responses for a survey
## Management API
The Management API provides access to all data and settings that are visible in the Formbricks App. This API requires a personal API Key for authentication, which can be generated in the Settings section of the Formbricks App. With the Management API, you can manage your Formbricks account programmatically, accessing and modifying data and settings as needed.
**Auth:** Personal API Key
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).
- [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
- [Me API](/docs/api/management/me) - Retrieve Account Information
- [People API](/docs/api/management/people) - Create, Update, and Delete People
- [Responses API](/docs/api/management/responses) - Create, Update, and Delete Responses
- [Surveys API](/docs/api/management/surveys) - Create, Update, and Delete Surveys
- [Webhook API](/docs/api/management/webhooks) - Create, Update, and Delete Webhooks
<Note>
By understanding the differences between these two APIs, you can choose the appropriate one for your needs,
ensuring a secure and efficient integration with the Formbricks platform.
</Note>
---

View File

@@ -0,0 +1,130 @@
import { Fence } from "@/components/shared/Fence";
export const metadata = {
title: "Formbricks Public Client API Guide: Manage Users",
description:
"Dive deep into Formbricks' Public Client API designed for customisation. This comprehensive guide provides detailed instructions on creating and updating users to help in user identification.",
};
#### Client API
# People API
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
This set of API can be used to
- [Create Person](#create-person)
- [Update Person](#update-person)
---
## Create Person {{ tag: 'POST', label: '/api/v1/client/<environment-id>/people' }}
<Row>
<Col>
Create User with your own User ID
### Mandatory Request Body JSON Keys
<Properties>
<Property name="userId" type="string">
User ID which you would like to identify the person with
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/client/<environment-id>/people">
```bash {{ title: 'cURL' }}
curl -X POST \
'https://app.formbricks.com/api/v1/client/<environment-id>/people' \
-H 'Content-Type: application/json' \
-d '{
"userId":"docs_user"
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"userId": "docs_user"
}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "Fields are missing or incorrectly formatted",
"details": {
"surveyId": "Required"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Update Person {{ tag: 'POST', label: '/api/v1/client/<environment-id>/people/<user-id>' }}
<Row>
<Col>
Update Person by their User ID
### Mandatory Request Body JSON Keys
<Properties>
<Property name="attributes" type="JSON">
Key Value pairs of attributes to add to the user
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/client/<environment-id>/people/<user-id>">
```bash {{ title: 'cURL' }}
curl -X POST \
--location \
'https://app.formbricks.com/api/v1/client/<environment-id>/people/<user-id>'
-H 'Content-Type: application/json' \
-d '{
"attributes":{
"welcome_to":"formbricks"
}
}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {}
}
```
```json {{ title: '500 Internal Server Error' }}
{
"code": "internal_server_error",
"message": "Database operation failed",
"details": {}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,200 @@
import { Fence } from "@/components/shared/Fence";
export const metadata = {
title: "Formbricks Responses API Documentation - Manage Your Survey Data Seamlessly",
description:
"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
# Responses API
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
This set of API can be used to
- [Create Response](#create-response)
- [Update Response](#update-response)
---
## Create Response {{ tag: 'POST', label: '/api/v1/client/<environment-id>/responses' }}
Add a new response to a survey.
<Row>
<Col>
### Mandatory Body Fields
<Properties>
<Property name="surveyId" type="string">
The id of the survey the response belongs to.
</Property>
<Property name="finished" type="boolean">
Marks whether the response is complete or not.
</Property>
<Property name="data" type="string">
The data of the response as JSON object (key: questionId, value: answer).
</Property>
</Properties>
### Optional Body Fields
<Properties>
<Property name="userId" type="string" required>
Pre-existing User ID to identify the user sending the response
</Property>
</Properties>
### Parameters Explained
| field name | required | default | description |
| ---------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| data | yes | - | The response data object (answers to the survey). In this object the key is the questionId, the value the answer of the user to this question. |
| userId | no | - | The person this response is connected to. |
| surveyId | yes | - | The survey this response is connected to. |
| finished | yes | false | Mark a response as complete to be able to filter accordingly. |
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/client/<environment-id>/responses">
```bash {{ title: 'cURL' }}
curl --location --request POST 'https://app.formbricks.com/api/v1/client/<environment-id>/responses' \
--data-raw '{
"surveyId":"cloqzeuu70000z8khcirufo60",
"userId": "1",
"finished": true,
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
}
}'
```
```json {{ title: 'Example Request Body' }}
{
"userId": "1",
"surveyId": "cloqzeuu70000z8khcirufo60",
"finished": true,
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
}
}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {
"id": "clp84xdld0002px36fkgue5ka",
}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "surveyId was not provided.",
"details": {
"surveyId": "This field is required."
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Update Response {{ tag: 'PUT', label: '/api/v1/client/<environment-id>/responses/<response-id>' }}
Update an existing response in a survey.
<Row>
<Col>
### Mandatory Body Fields
<Properties>
<Property name="finished" type="boolean">
Marks whether the response is complete or not.
</Property>
<Property name="data" type="string">
The data of the response as JSON object (key: questionId, value: answer).
</Property>
</Properties>
### Parameters Explained
| 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. |
| finished | yes | false | Mark a response as complete to be able to filter accordingly. |
</Col>
<Col sticky>
<CodeGroup title="Request" tag="PUT" label="/api/v1/client/<environment-id>/responses/<response-id>">
```bash {{ title: 'cURL' }}
curl --location --request PUT 'https://app.formbricks.com/api/v1/client/<environment-id>/responses/<response-id>' \
--data-raw '{
"finished":false,
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
}
}'
```
```json {{ title: 'Example Request Body' }}
{
"finished":false,
"data": {
"clfqjny0v0003yzgscnog1j9i": 10,
"clfqjtn8n0070yzgs6jgx9rog": "I love Formbricks"
}
}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "data was not provided.",
"details": {
"data": "This field is required."
}
}
```
```json {{ title: '404 Not Found' }}
{
"code": "not_found",
"message": "Response not found"
}
```
</CodeGroup>
</Col>
</Row>

View File

@@ -0,0 +1,297 @@
import { Fence } from "@/components/shared/Fence";
import {generateManagementApiMetadata} from "@/lib/utils"
export const metadata = generateManagementApiMetadata("Action Class",["Fetch","Create","Delete"])
#### Management API
# Action Classes API
This set of API can be used to
- [List Actions](#get-all-action-classes)
- [Get Action](#get-action-class-by-id)
- [Create Actions](#create-action-class)
- [Delete Actions](#delete-action-class)
<Note>You will need an API Key to interact with these APIs.</Note>
---
## Get all Action Classes {{ tag: 'GET', label: '/api/v1/management/action-classes' }}
<Row>
<Col>
Get all the existing action classes in your environment.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/action-classes">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/action-classes' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": [
{
"id": "cln8k0t47000gz87nw4ibwv35",
"createdAt": "2023-10-02T07:13:19.207Z",
"updatedAt": "2023-10-02T07:13:19.207Z",
"name": "New Session",
"description": "Gets fired when a new session is created",
"type": "automatic",
"noCodeConfig": null,
"environmentId": "cln8k0t47000fz87njmmu2bck"
},
{
"id": "cln8k0t55000uz87noerwdooj",
"createdAt": "2023-10-02T07:13:19.241Z",
"updatedAt": "2023-10-02T07:13:19.241Z",
"name": "Invited Team Member",
"description": "Person invited a team member",
"type": "noCode",
"noCodeConfig": {
"type": "innerHtml",
"innerHtml": {
"value": "Add Team Member"
}
},
"environmentId": "cln8k0t47000fz87njmmu2bck"
},
]
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Get Action Class by ID {{ tag: 'GET', label: '/api/v1/management/action-classes/<action-class-id>' }}
<Row>
<Col>
Fetch an action class by its ID.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/action-classes/<action-class-id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/action-classes/<action-class-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cln8k0t55000uz87noerwdooj",
"createdAt": "2023-10-02T07:13:19.241Z",
"updatedAt": "2023-10-02T07:13:19.241Z",
"name": "Invited Team Member",
"description": "Person invited a team member",
"type": "noCode",
"noCodeConfig": {
"type": "innerHtml",
"innerHtml": {
"value": "Add Team Member"
}
},
"environmentId": "cln8k0t47000fz87njmmu2bck"
}
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Create Action Class {{ tag: 'POST', label: '/api/v1/management/action-classes/' }}
<Row>
<Col>
Create an action class.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Body
<CodeGroup title="Request Body">
```json {{ title: 'cURL' }}
{
"environmentId": "cln8k0t47000fz87njmmu2bck",
"name": "My Action from API",
"type": "code"
}
```
</CodeGroup>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/management/action-classes/">
```bash {{ title: 'cURL' }}
curl -X POST https://app.formbricks.com/api/v1/management/action-classes/ \
--header 'Content-Type: application/json' \
--header 'x-api-key: <your-api-key>' \
-d '{"environmentId": "cln8k0t47000fz87njmmu2bck", "name": "My Action from API", "type": "code"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cln9w1cno0008z8zu79nk5w0c",
"createdAt": "2023-10-03T05:37:26.100Z",
"updatedAt": "2023-10-03T05:37:26.100Z",
"name": "My Action from API",
"description": null,
"type": "code",
"noCodeConfig": null,
"environmentId": "cln8k0t47000fz87njmmu2bck"
}
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Delete Action Class {{ tag: 'DELETE', label: '/api/v1/management/action-classes/<action-class-id>' }}
<Row>
<Col>
Delete an action class by its ID.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/management/action-classes/<action-class-id>">
```bash {{ title: 'cURL' }}
curl -X DELETE https://app.formbricks.com/api/v1/management/action-classes/<action-class-id> \
--header 'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cln9w1cno0008z8zu79nk5w0c",
"createdAt": "2023-10-03T05:37:26.100Z",
"updatedAt": "2023-10-03T05:37:26.100Z",
"name": "My Action from API",
"description": null,
"type": "code",
"noCodeConfig": null,
"environmentId": "cln8k0t47000fz87njmmu2bck"
}
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,103 @@
import { MdxImage } from "@/components/shared/MdxImage";
import AddApiKey from "./add-api-key.webp";
import ApiKeySecret from "./api-key-secret.webp";
export const metadata = {
title: "Formbricks API Key: Setup and Testing",
description:
"This guide provides step-by-step instructions to generate, store, and delete API keys, ensuring safe and authenticated access to your Formbricks account.",
};
#### API
# API Key Setup
## Auth: Personal API key
The API requests are authorized with a personal API key. This API key gives you the same rights as if you were logged in at formbricks.com - **don't share it around!**
### How to generate an API key
1. Go to your settings on [app.formbricks.com](https://app.formbricks.com).
2. Go to page “API keys”
<MdxImage src={AddApiKey} alt="Add API Key" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
3. Create a key for the development or production environment.
4. Copy the key immediately. You wont be able to see it again.
<MdxImage
src={ApiKeySecret}
alt="API Key Secret"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
<Note>
### Store API key safely!
Anyone who has your API key has full control over your account. For security
reasons, you cannot view the API key again.
</Note>
### Test your API Key
Hit the below request to verify that you are authenticated with your API Key and the server is responding.
## Get My Profile {{ tag: 'GET', label: '/api/v1/me' }}
<Row>
<Col>
Get the product details and environment type of your account.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/me">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/me' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"id": "cll2m30r70004mx0huqkitgqv",
"createdAt": "2023-08-08T18:04:59.922Z",
"updatedAt": "2023-08-08T18:04:59.922Z",
"type": "production",
"product": {
"id": "cll2m30r60003mx0hnemjfckr",
"name": "My Product"
},
"widgetSetupCompleted": false
}
```
```json {{ title: '401 Not Authenticated' }}
Not authenticated
```
</CodeGroup>
</Col>
</Row>
---
### Delete a personal API key
1. Go to settings on [app.formbricks.com](https://app.formbricks.com/).
2. Go to page “API keys”.
3. Find the key you wish to revoke and select “Delete”.
4. Your API key will stop working immediately.

View File

@@ -0,0 +1,289 @@
import { Fence } from "@/components/shared/Fence";
import {generateManagementApiMetadata} from "@/lib/utils"
export const metadata = generateManagementApiMetadata("Attribute Class",["Fetch","Create","Delete"])
#### Management API
# Attribute Classes API
This set of API can be used to
- [List Attributes](#get-all-attribute-classes)
- [Get Attributes](#get-attribute-class-by-id)
- [Create Attributes](#create-attribute-class)
- [Delete Attributes](#delete-attribute-class)
<Note>You will need an API Key to interact with these APIs.</Note>
---
## Get all Attribute Classes {{ tag: 'GET', label: '/api/v1/management/attribute-classes' }}
<Row>
<Col>
Get all the existing attribute classes in your environment.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/attribute-classes">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/attribute-classes' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": [
{
"id": "cln8k0t47000kz87n3lh23zf0",
"createdAt": "2023-10-02T07:13:19.207Z",
"updatedAt": "2023-10-02T07:13:19.207Z",
"name": "email",
"description": "The email of the person",
"archived": false,
"type": "automatic",
"environmentId": "cln8k0t47000fz87njmmu2bck"
},
{
"id": "cln8k0t55000xz87nrtwbo7sf",
"createdAt": "2023-10-02T07:13:19.241Z",
"updatedAt": "2023-10-02T07:13:19.241Z",
"name": "Name",
"description": "Full Name of the Person",
"archived": false,
"type": "code",
"environmentId": "cln8k0t47000fz87njmmu2bck"
},
]
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Get Attribute Class by ID {{ tag: 'GET', label: '/api/v1/management/attribute-classes/<attribute-class-id>' }}
<Row>
<Col>
Fetch an Attribute class by its ID.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/attribute-classes/<attribute-class-id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/attribute-classes/<attribute-class-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cln8k0t47000jz87nfwcey6mh",
"createdAt": "2023-10-02T07:13:19.207Z",
"updatedAt": "2023-10-02T07:13:19.207Z",
"name": "userId",
"description": "The internal ID of the person",
"archived": false,
"type": "automatic",
"environmentId": "cln8k0t47000fz87njmmu2bck"
}
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Create Attribute Class {{ tag: 'POST', label: '/api/v1/management/attribute-classes/' }}
<Row>
<Col>
Create an Attribute class.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Body
<CodeGroup title="Request Body">
```json {{ title: 'cURL' }}
{
"environmentId": "clmlmwdqq0003196ufewo6ibg",
"name": "My Attribute from API",
"type": "code",
"description": "My description"
}
```
</CodeGroup>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/management/attribute-classes/">
```bash {{ title: 'cURL' }}
curl -X POST https://app.formbricks.com/api/v1/management/attribute-classes/ \
--header 'Content-Type: application/json' \
--header 'x-api-key: <your-api-key>' \
-d '{"environmentId": "clmlmwdqq0003196ufewo6ibg", "name": "My Attribute from API", "type": "code", "description":"My description"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "clna0hd7z0009z8zue2z3a7wy",
"createdAt": "2023-10-03T07:41:51.792Z",
"updatedAt": "2023-10-03T07:41:51.792Z",
"name": "My Attribute from API",
"description": null,
"archived": false,
"type": "code",
"environmentId": "cln8k0t47000fz87njmmu2bck"
}
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Delete Attribute Class {{ tag: 'DELETE', label: '/api/v1/management/attribute-classes/<attribute-class-id>' }}
<Row>
<Col>
Delete an Attribute class by its ID.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/management/attribute-classes/<attribute-class-id>">
```bash {{ title: 'cURL' }}
curl -X DELETE https://app.formbricks.com/api/v1/management/attribute-classes/<attribute-class-id> \
--header 'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "clna0hd7z0009z8zue2z3a7wy",
"createdAt": "2023-10-03T07:41:51.792Z",
"updatedAt": "2023-10-03T07:41:51.792Z",
"name": "My Attribute from API",
"description": null,
"archived": false,
"type": "code",
"environmentId": "cln8k0t47000fz87njmmu2bck"
}
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,78 @@
import { Fence } from "@/components/shared/Fence";
export const metadata = {
title: "Formbricks Me API: Fetch your environment details",
description:
"Dive into Formbricks' Me API within the Public Client API suite. Seamlessly fetch your own current environment details.",
};
#### Management API
# Me API
This API can be used to get your own current environment details.
<Note>You will need an API Key to interact with these APIs.</Note>
---
## Get Environment {{ tag: 'GET', label: '/api/v1/management/me' }}
<Row>
<Col>
Get your current environment details.
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/me">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/me' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"id": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.207Z",
"updatedAt": "2023-10-02T07:14:14.162Z",
"type": "production",
"product": {
"id": "cln8k0t47000ez87n57aqywvz",
"name": "Demo Product"
},
"widgetSetupCompleted": true
}
```
```json {{ title: '401 Unauthorized' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,232 @@
import { Fence } from "@/components/shared/Fence";
import {generateManagementApiMetadata} from "@/lib/utils"
export const metadata = generateManagementApiMetadata("People",["Fetch","Delete"])
#### Management API
# People API
This set of API can be used to
- [List People](#list-people)
- [Get Person](#get-person)
- [Delete Person](#delete-person)
<Note>You will need an API Key to interact with these APIs.</Note>
---
## List People {{ tag: 'GET', label: '/api/v1/management/people' }}
<Row>
<Col>
List People
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/people">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/people' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": [
{
"id": "b4wgrzl363dn3zb6yy5gf265",
"attributes": {
"userId": "CYO618",
"email": "sophia@amazon.com",
"Name": "Sophia Johnson",
"Role": "Designer",
"Company": "Amazon",
"Experience": "7 years",
"Usage Frequency": "Yearly",
"Company Size": "1628 employees",
"Product Satisfaction Score": "62",
"Recommendation Likelihood": "9"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
},
{
"id": "jrb5iyzqvnkg9322ckhde3j4",
"attributes": {
"userId": "CYO511",
"email": "antonio@ibm.com",
"Name": "Antonio García",
"Role": "Designer",
"Company": "IBM",
"Experience": "1 years",
"Usage Frequency": "Weekly",
"Company Size": "4023 employees",
"Product Satisfaction Score": "77",
"Recommendation Likelihood": "4"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
},
]
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "Fields are missing or incorrectly formatted",
"details": {
"userId": ""
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Get Person {{ tag: 'GET', label: '/api/v1/management/people/<person-id>' }}
<Row>
<Col>
Get Person by ID
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/people/<person-id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/people/<person-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "jrb5iyzqvnkg9322ckhde3j4",
"attributes": {
"userId": "CYO511",
"email": "antonio@ibm.com",
"Name": "Antonio García",
"Role": "Designer",
"Company": "IBM",
"Experience": "1 years",
"Usage Frequency": "Weekly",
"Company Size": "4023 employees",
"Product Satisfaction Score": "77",
"Recommendation Likelihood": "4"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
}
}
```
```json {{ title: '404 Not Found' }}
{
"code": "not_found",
"message": "Person not found",
"details": {
"resource_id": "clmlmykc2000019vz5o3jglsa",
"resource_type": "Person"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Delete Person {{ tag: 'DELETE', label: '/api/v1/management/people/<person-id>' }}
<Row>
<Col>
Delete Person by ID
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/management/people/<person-id>">
```bash {{ title: 'cURL' }}
curl -X DELETE https://app.formbricks.com/api/v1/management/people/<person-id> \
--header 'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"success": "Person deleted successfully"
}
}
```
```json {{ title: '404 Not Found' }}
{
"code": "not_found",
"message": "Person not found",
"details": {
"resource_id": "clmlmykc2000019vz5o3jglsa",
"resource_type": "Person"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,379 @@
import { Fence } from "@/components/shared/Fence";
import { generateManagementApiMetadata } from "@/lib/utils";
export const metadata = generateManagementApiMetadata("Responses", ["Fetch", "Delete"]);
#### Management API
# Responses API
This set of API can be used to
- [List Responses](#list-all-responses)
- [List all Responses by surveyId](#list-all-responses-by-survey-id)
- [Get Response](#get-response-by-id)
- [Delete Response](#delete-a-response)
<Note>You will need an API Key to interact with these APIs.</Note>
---
## List all Responses {{ tag: 'GET', label: '/api/v1/management/responses' }}
<Row>
<Col>
Retrieve all the responses you have received in your environment.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/responses">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/responses' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data":[
{
"id": "cln8k0tqv00pcz87no4qrw333",
"createdAt": "2023-10-02T07:13:20.023Z",
"updatedAt": "2023-10-02T07:13:20.023Z",
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
"finished": true,
"data": {
"interview-prompt": "clicked"
},
"meta": {
"userAgent": {
"os": "MacOS",
"browser": "Chrome"
}
},
"personAttributes": null,
"person": {
"id": "e0x4i5tvsp8puxfztyrwykvn",
"attributes": {
"userId": "CYO675",
"email": "ravi@netflix.com",
"Name": "Ravi Kumar",
"Role": "Manager",
"Company": "Netflix",
"Experience": "6 years",
"Usage Frequency": "Monthly",
"Company Size": "4610 employees",
"Product Satisfaction Score": "43",
"Recommendation Likelihood": "4"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
},
"notes": [],
"tags": []
},
]
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## List all Responses by surveyId {{ tag: 'GET', label: '/api/v1/management/responses?surveyId=<survey-Id>' }}
<Row>
<Col>
Retrieve all the responses received in your survey.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/responses?surveyId=<survey-Id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/responses?surveyId=<survey-Id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data":[
{
"id": "cln8k0tqv00pcz87no4qrw333",
"createdAt": "2023-10-02T07:13:20.023Z",
"updatedAt": "2023-10-02T07:13:20.023Z",
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
"finished": true,
"data": {
"interview-prompt": "clicked"
},
"meta": {
"userAgent": {
"os": "MacOS",
"browser": "Chrome"
}
},
"personAttributes": null,
"person": {
"id": "e0x4i5tvsp8puxfztyrwykvn",
"attributes": {
"userId": "CYO675",
"email": "ravi@netflix.com",
"Name": "Ravi Kumar",
"Role": "Manager",
"Company": "Netflix",
"Experience": "6 years",
"Usage Frequency": "Monthly",
"Company Size": "4610 employees",
"Product Satisfaction Score": "43",
"Recommendation Likelihood": "4"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
},
"notes": [],
"tags": []
},
]
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Get Response by ID {{ tag: 'GET', label: '/api/v1/management/responses/<response-id>' }}
<Row>
<Col>
Retrieve a response by its ID.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/responses/<response-id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/responses/<response-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data":
{
"id": "cln8k0tqv00pbz87nwo5lr72b",
"createdAt": "2023-10-02T07:13:20.023Z",
"updatedAt": "2023-10-02T07:13:20.023Z",
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
"finished": true,
"data": {
"interview-prompt": "clicked"
},
"meta": {
"userAgent": {
"os": "Windows",
"browser": "Edge"
}
},
"personAttributes": null,
"person": {
"id": "hsx38f15v50ua8383uadagq5",
"attributes": {
"userId": "CYO278",
"email": "jorge@facebook.com",
"Name": "Jorge Sanchez",
"Role": "Product Manager",
"Company": "Facebook",
"Experience": "10 years",
"Usage Frequency": "Daily",
"Company Size": "1685 employees",
"Product Satisfaction Score": "84",
"Recommendation Likelihood": "6"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
},
"notes": [],
"tags": []
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Delete a response {{ tag: 'DELETE', label: '/api/v1/client/responses/<response-id>' }}
<Row>
<Col>
Delete Response by ID
### Mandatory Headers
<Properties>
<Property name="x-api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/client/responses/<response-id>">
```bash {{ title: 'cURL' }}
curl -X DELETE https://app.formbricks.com/api/v1/management/responses/<response-id> \
--header 'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {
"id": "cln8k0tqv00pbz87nwo5lr72b",
"createdAt": "2023-10-02T07:13:20.023Z",
"updatedAt": "2023-10-02T07:13:20.023Z",
"surveyId": "cln8k0tqu00p7z87nqr4thi3k",
"finished": true,
"data": {
"interview-prompt": "clicked"
},
"meta": {
"userAgent": {
"os": "Windows",
"browser": "Edge"
}
},
"personAttributes": null,
"person": {
"id": "hsx38f15v50ua8383uadagq5",
"attributes": {
"userId": "CYO278",
"email": "jorge@facebook.com",
"Name": "Jorge Sanchez",
"Role": "Product Manager",
"Company": "Facebook",
"Experience": "10 years",
"Usage Frequency": "Daily",
"Company Size": "1685 employees",
"Product Satisfaction Score": "84",
"Recommendation Likelihood": "6"
},
"environmentId": "cln8k0t47000fz87njmmu2bck",
"createdAt": "2023-10-02T07:13:19.444Z",
"updatedAt": "2023-10-02T07:13:19.444Z"
},
"notes": [],
"tags": []
}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "surveyId was not provided.",
"details": {
"surveyId": "This field is required."
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,789 @@
import { Fence } from "@/components/shared/Fence";
import { generateManagementApiMetadata } from "@/lib/utils";
export const metadata = generateManagementApiMetadata("Surveys", ["Fetch", "Create", "Update", "Delete"]);
#### Management API
# Surveys API
This set of API can be used to
- [List All Surveys](#list-all-surveys)
- [Get Survey](#get-survey-by-id)
- [Create Survey](#create-survey)
- [Update Survey](#update-survey-by-id)
- [Delete Survey](#delete-survey-by-id)
<Note>You will need an API Key to interact with these APIs.</Note>
---
## List all surveys {{ tag: 'GET', label: '/api/v1/management/surveys' }}
<Row>
<Col>
Retrieve all the surveys you have for the environment with pagination.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Query Parameters
<Properties>
<Property name="offset" type="number">
The number of surveys to skip before returning the results.
</Property>
<Property name="limit" type="number">
The number of surveys to return.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/surveys">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/surveys?offset=20&limit=10' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": [
{
"id": "cllnfy2780fromy0hy7uoxvtn",
"createdAt": "2023-08-23T07:56:20.516Z",
"updatedAt": "2023-08-23T07:56:26.947Z",
"name": "Product Market Fit (Superhuman)",
"type": "link",
"environmentId": "cll2m30r70004mx0huqkitgqv",
"status": "inProgress",
"attributeFilters": [],
"displayOption": "displayOnce",
"autoClose": null,
"triggers": [],
"redirectUrl": null,
"recontactDays": null,
"questions": [
{
"id": "gml6mgy71efgtq8np3s9je5p",
"type": "cta",
"headline": "You are one of our power users! Do you have 5 minutes?",
"required": false,
"buttonLabel": "Happy to help!",
"logic": [
{
"condition": "skipped",
"destination": "end"
}
],
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We would love to understand your user experience better. Sharing your insight helps a lot.</span></p>",
"buttonExternal": false,
"dismissButtonLabel": "No, thanks."
},
{
"id": "kp62fbqe8cfzmvy8qwpr81b2",
"type": "multipleChoiceSingle",
"headline": "How disappointed would you be if you could no longer use My Product?",
"subheader": "Please select one of the following options:",
"required": true,
"choices": [
{
"id": "bdgy1hnwd7uwmfxk1ljqp1n5",
"label": "Not at all disappointed"
},
{
"id": "poabnvgtwenp8rb2v70gj4hj",
"label": "Somewhat disappointed"
},
{
"id": "opfiqyqz8wrqn0i0f7t24d3n",
"label": "Very disappointed"
}
],
"shuffleOption": "none"
},
{
"id": "klvpwd4x08x8quesihvw5l92",
"type": "multipleChoiceSingle",
"headline": "What is your role?",
"subheader": "Please select one of the following options:",
"required": true,
"choices": [
{
"id": "c8nerw6l9gpsxcmqkn10f9hy",
"label": "Founder"
},
{
"id": "ebjqezei6a2axtuq86cleetn",
"label": "Executive"
},
{
"id": "ctiijjblyhlp22snypfamqt1",
"label": "Product Manager"
},
{
"id": "ibalyr0mhemfkkr82vypmg40",
"label": "Product Owner"
},
{
"id": "fipk606aegslbd0e7yhc0xjx",
"label": "Software Engineer"
}
],
"shuffleOption": "none"
},
{
"id": "ryo75306flyg72iaeditbv51",
"type": "openText",
"headline": "What type of people do you think would most benefit from My Product?",
"required": true
},
{
"id": "lkjaxb73ulydzeumhd51sx9g",
"type": "openText",
"headline": "What is the main benefit you receive from My Product?",
"required": true
},
{
"id": "ec7agikkr58j8uonhioinkyk",
"type": "openText",
"headline": "How can we improve My Product for you?",
"subheader": "Please be as specific as possible.",
"required": true
}
],
"thankYouCard": {
"enabled": true,
"headline": "Thank you!",
"subheader": "We appreciate your feedback."
},
"delay": 0,
"autoComplete": null,
"closeOnDate": null
}
]
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Get Survey by ID {{ tag: 'GET', label: '/api/v1/management/surveys/<survey-id>' }}
<Row>
<Col>
Get a specific survey by its ID.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/surveys/<survey-id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/surveys/<survey-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cln8k0tjz00n5z87nwq527h3z",
"createdAt": "2023-10-02T07:13:19.775Z",
"updatedAt": "2023-10-02T07:13:19.775Z",
"name": "Churn Survey",
"type": "link",
"environmentId": "cln8k0t47000fz87njmmu2bck",
"status": "inProgress",
"attributeFilters": [],
"displayOption": "displayOnce",
"autoClose": null,
"triggers": [],
"redirectUrl": null,
"recontactDays": null,
"questions": [
{
"id": "churn-reason",
"type": "multipleChoiceSingle",
"headline": "Why did you cancel your subscription?",
"subheader": "We're sorry to see you leave. Help us do better:",
"required": true,
"logic": [
{
"condition": "equals",
"value": "Difficult to use",
"destination": "easier-to-use"
},
{
"condition": "equals",
"value": "It's too expensive",
"destination": "30-off"
},
{
"condition": "equals",
"value": "I am missing features",
"destination": "missing-features"
},
{
"condition": "equals",
"value": "Poor customer service",
"destination": "poor-service"
},
{
"condition": "equals",
"value": "I just didn't need it anymore",
"destination": "end"
}
],
"choices": [
{
"id": "isud2xethsw63dlwl89kr4kj",
"label": "Difficult to use"
},
{
"id": "opuu4ba3dlele3n0gjkuh27c",
"label": "It's too expensive"
},
{
"id": "gnypapo0rhvkt8pwosrphvbl",
"label": "I am missing features"
},
{
"id": "wkgsrsrazd9kfunqhzjezx6t",
"label": "Poor customer service"
},
{
"id": "pykmgyyw74vg0gaeryj6bo4c",
"label": "I just didn't need it anymore"
}
]
},
{
"id": "easier-to-use",
"type": "openText",
"headline": "What would have made {{productName}} easier to use?",
"subheader": "",
"required": true,
"buttonLabel": "Send",
"logic": [
{
"condition": "submitted",
"destination": "end"
}
]
},
{
"id": "30-off",
"type": "cta",
"headline": "Get 30% off for the next year!",
"required": true,
"buttonLabel": "Get 30% off",
"logic": [
{
"condition": "clicked",
"destination": "end"
}
],
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We'd love to keep you as a customer. Happy to offer a 30% discount for the next year.</span></p>",
"buttonUrl": "https://formbricks.com",
"buttonExternal": true,
"dismissButtonLabel": "Skip"
},
{
"id": "missing-features",
"type": "openText",
"headline": "What features are you missing?",
"subheader": "",
"required": true,
"logic": [
{
"condition": "submitted",
"destination": "end"
}
]
},
{
"id": "poor-service",
"type": "cta",
"headline": "So sorry to hear 😔 Talk to our CEO directly!",
"required": true,
"buttonLabel": "Send email to CEO",
"logic": [
{
"condition": "clicked",
"destination": "end"
}
],
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We aim to provide the best possible customer service. Please email our CEO and she will personally handle your issue.</span></p>",
"buttonUrl": "mailto:ceo@company.com",
"buttonExternal": true,
"dismissButtonLabel": "Skip"
}
],
"thankYouCard": {
"enabled": false
},
"delay": 0,
"autoComplete": null,
"closeOnDate": null,
"surveyClosedMessage": null,
"verifyEmail": null
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Create Survey {{ tag: 'POST', label: '/api/v1/management/surveys' }}
<Row>
<Col>
Create a survey
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Body
<CodeGroup title="Request Body">
```json {{ title: 'cURL' }}
{
"environmentId": "clmlmwdqq0003196ufewo6ibg",
"type": "link",
"name": "My new Survey"
}
```
</CodeGroup>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/management/surveys">
```bash {{ title: 'cURL' }}
curl -X POST \
'https://app.formbricks.com/api/v1/management/surveys' \
--header \
'x-api-key: <your-api-key>'
```
```bash {{ title: 'cURL' }}
curl -X POST https://app.formbricks.com/api/v1/management/surveys/ \
--header 'Content-Type: application/json' \
--header 'x-api-key: <your-api-key>' \
-d '{"environmentId": "cln8k0t47000fz87njmmu2bck", "name": "My Survey from API", "type": "link"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "clna6bqnz000az8zubq3e757t",
"createdAt": "2023-10-03T10:25:26.975Z",
"updatedAt": "2023-10-03T10:25:26.975Z",
"name": "My new Survey",
"redirectUrl": null,
"type": "link",
"environmentId": "cln8k0t47000fz87njmmu2bck",
"status": "draft",
"questions": [],
"thankYouCard": {
"enabled": false
},
"displayOption": "displayOnce",
"recontactDays": null,
"autoClose": null,
"delay": 0,
"autoComplete": null,
"closeOnDate": null,
"surveyClosedMessage": null,
"verifyEmail": null
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Update Survey by ID {{ tag: 'PUT', label: '/api/v1/management/surveys/<survey-id>' }}
<Row>
<Col>
Update a survey by its ID
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Body
<CodeGroup title="Request Body">
```json {{ title: 'cURL' }}
{
"name": "My renamed Survey",
"redirectUrl":"https://formbricks.com",
"type":"web"
}
```
</CodeGroup>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="PUT" label="/api/v1/management/surveys/<survey-id>">
```bash {{ title: 'cURL' }}
curl -X PUT https://app.formbricks.com/api/v1/management/surveys/<survey-id> \
--header 'Content-Type: application/json' \
--header 'x-api-key: <your-api-key>' \
-d '{"name": "My renamed Survey"}'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cloqzeuu70000z8khcirufo60",
"createdAt": "2023-11-09T09:23:42.367Z",
"updatedAt": "2023-11-09T09:23:42.367Z",
"name": "My renamed Survey",
"redirectUrl": null,
"type": "link",
"environmentId": "clonzr6vc0009z8md7y06hipl",
"status": "inProgress",
"welcomeCard": {
"html": "Thanks for providing your feedback - let's go!",
"enabled": false,
"headline": "Welcome!",
"timeToFinish": false
},
"questions": [
{
"id": "l9rwn5nbk48y44tvnyyjcvca",
"type": "openText",
"headline": "Why did you leave the platform?",
"required": true,
"inputType": "text"
}
],
"thankYouCard": {
"enabled": true,
"headline": "Thank you!",
"subheader": "We appreciate your feedback."
},
"hiddenFields": {
"enabled": true,
"fieldIds": []
},
"displayOption": "displayOnce",
"recontactDays": null,
"autoClose": null,
"delay": 0,
"autoComplete": 50,
"closeOnDate": null,
"surveyClosedMessage": null,
"productOverwrites": null,
"singleUse": {
"enabled": false,
"isEncrypted": true
},
"verifyEmail": null,
"pin": null,
"triggers": [],
"attributeFilters": []
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Delete Survey by ID {{ tag: 'DELETE', label: '/api/v1/management/surveys/<survey-id>' }}
<Row>
<Col>
Delete a survey by its ID.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/management/surveys/<survey-id>">
```bash {{ title: 'cURL' }}
curl -X DELETE \
'https://app.formbricks.com/api/v1/management/surveys/<survey-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{title:'200 Success'}}
{
"data": {
"id": "cln8k0tjz00n5z87nwq527h3z",
"createdAt": "2023-10-02T07:13:19.775Z",
"updatedAt": "2023-10-02T07:13:19.775Z",
"name": "Churn Survey",
"type": "link",
"environmentId": "cln8k0t47000fz87njmmu2bck",
"status": "inProgress",
"attributeFilters": [],
"displayOption": "displayOnce",
"autoClose": null,
"triggers": [],
"redirectUrl": null,
"recontactDays": null,
"questions": [
{
"id": "churn-reason",
"type": "multipleChoiceSingle",
"headline": "Why did you cancel your subscription?",
"subheader": "We're sorry to see you leave. Help us do better:",
"required": true,
"logic": [
{
"condition": "equals",
"value": "Difficult to use",
"destination": "easier-to-use"
},
{
"condition": "equals",
"value": "It's too expensive",
"destination": "30-off"
},
{
"condition": "equals",
"value": "I am missing features",
"destination": "missing-features"
},
{
"condition": "equals",
"value": "Poor customer service",
"destination": "poor-service"
},
{
"condition": "equals",
"value": "I just didn't need it anymore",
"destination": "end"
}
],
"choices": [
{
"id": "isud2xethsw63dlwl89kr4kj",
"label": "Difficult to use"
},
{
"id": "opuu4ba3dlele3n0gjkuh27c",
"label": "It's too expensive"
},
{
"id": "gnypapo0rhvkt8pwosrphvbl",
"label": "I am missing features"
},
{
"id": "wkgsrsrazd9kfunqhzjezx6t",
"label": "Poor customer service"
},
{
"id": "pykmgyyw74vg0gaeryj6bo4c",
"label": "I just didn't need it anymore"
}
]
},
{
"id": "easier-to-use",
"type": "openText",
"headline": "What would have made {{productName}} easier to use?",
"subheader": "",
"required": true,
"buttonLabel": "Send",
"logic": [
{
"condition": "submitted",
"destination": "end"
}
]
},
{
"id": "30-off",
"type": "cta",
"headline": "Get 30% off for the next year!",
"required": true,
"buttonLabel": "Get 30% off",
"logic": [
{
"condition": "clicked",
"destination": "end"
}
],
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We'd love to keep you as a customer. Happy to offer a 30% discount for the next year.</span></p>",
"buttonUrl": "https://formbricks.com",
"buttonExternal": true,
"dismissButtonLabel": "Skip"
},
{
"id": "missing-features",
"type": "openText",
"headline": "What features are you missing?",
"subheader": "",
"required": true,
"logic": [
{
"condition": "submitted",
"destination": "end"
}
]
},
{
"id": "poor-service",
"type": "cta",
"headline": "So sorry to hear 😔 Talk to our CEO directly!",
"required": true,
"buttonLabel": "Send email to CEO",
"logic": [
{
"condition": "clicked",
"destination": "end"
}
],
"html": "<p class=\"fb-editor-paragraph\" dir=\"ltr\"><span>We aim to provide the best possible customer service. Please email our CEO and she will personally handle your issue.</span></p>",
"buttonUrl": "mailto:ceo@company.com",
"buttonExternal": true,
"dismissButtonLabel": "Skip"
}
],
"thankYouCard": {
"enabled": false
},
"delay": 0,
"autoComplete": null,
"closeOnDate": null,
"surveyClosedMessage": null,
"verifyEmail": null
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

@@ -0,0 +1,398 @@
import {generateManagementApiMetadata} from "@/lib/utils"
export const metadata = generateManagementApiMetadata("Webhook",["Fetch","Create","Delete"])
#### Management API
# Webhook API
Formbricks' Webhook API offers a powerful interface for interacting with webhooks. Webhooks allow you to receive real-time HTTP notifications of changes to specific objects in the Formbricks environment.
The behavior of the webhooks is determined by their trigger settings. The trigger determines which updates the webhook sends. Current available triggers include "responseCreated", "responseUpdated", and "responseFinished". This allows you to customize your webhooks to only send notifications for the events that are relevant to your application.
Webhooks are tied to a specific Formbricks environment. Once set, a webhook will receive updates from all surveys within this environment. This makes it easy to manage your data flow and ensure that all relevant updates are caught by the webhook.
This set of API can be used to
- [List All Webhooks](#list-webhooks)
- [Get Webhook](#retrieve-webhook-by-id)
- [Create Webhook](#create-webhook)
- [Delete Webhook](#delete-webhook-by-id)
And the detailed Webhook Payload is elaborated [here](#webhook-payload).
These APIs are designed to facilitate seamless integration of Formbricks with third-party systems. By making use of our webhook API, you can automate the process of sending data to these systems whenever significant events occur within your Formbricks environment.
<Note>You will need an API Key to interact with these APIs.</Note>
---
## List Webhooks {{ tag: 'GET', label: '/api/v1/webhooks' }}
<Row>
<Col>
Learn how to retrieve a list of all webhooks via API.
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/webhooks">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/webhooks' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": [
{
"id": "cliu1kdza000219zftad4ip6c",
"createdAt": "2023-06-13T08:49:04.198Z",
"updatedAt": "2023-06-13T08:49:04.198Z",
"url": "https://mysystem.com/myendpoint",
"environmentId": "clisypjy4000319t4imm289uo",
"triggers": [
"responseFinished"
]
}
]
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Retrieve Webhook by ID {{ tag: 'GET', label: '/api/v1/webhooks/<webhook-id>' }}
<Row>
<Col>
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/webhooks/<webhook-id>">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/webhooks/<webhook-id>' \
--header \
'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {
"id": "cliu167rk000019zfhbo68bar",
"createdAt": "2023-06-13T08:38:02.960Z",
"updatedAt": "2023-06-13T08:38:02.960Z",
"url": "https://mysystem.com/myendpoint",
"environmentId": "clisypjy4000319t4imm289uo",
"triggers": [
"responseFinished"
]
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Create Webhook {{ tag: 'POST', label: '/api/v1/webhooks' }}
Add a webhook to your product.
<Row>
<Col>
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
### Request Body Parameters
<Properties>
<Property name="url" type="string" required>
The URL where the webhook will send data to.
</Property>
<Property name="triggers" type="string[]" required>
List of events that will trigger the webhook.
</Property>
<Property name="surveyIds" type="string[]">
List of survey IDs that will trigger the webhook. If not provided, the webhook will be triggered for all surveys.
</Property>
</Properties>
| field name | required | default | description |
| ---------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
| url | yes | - | The endpoint that the webhook will send data to |
| trigger | yes | - | The event that will trigger the webhook ("responseCreated" or "responseUpdated" or "responseFinished") |
| surveyIds | no | - | List of survey IDs that will trigger the webhook. If not provided, the webhook will be triggered for all surveys. |
</Col>
<Col sticky>
<CodeGroup title="Request" tag="POST" label="/api/v1/webhooks">
```bash {{ title: 'cURL' }}
curl --location --request POST 'https://app.formbricks.com/api/v1/webhooks' \
--header 'x-api-key: <your-api-key>' \
--header 'Content-Type: application/json' \
--data-raw '{
"url": "https://mysystem.com/myendpoint",
"triggers": ["responseFinished"]
}'
```
```json {{ title: 'Example Request Body' }}
{
"url": "https://mysystem.com/myendpoint",
"triggers": ["responseFinished"]
}
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {
"id": "cliu1kdza000219zftad4ip6c",
"createdAt": "2023-06-13T08:49:04.198Z",
"updatedAt": "2023-06-13T08:49:04.198Z",
"url": "https://mysystem.com/myendpoint",
"environmentId": "clisypjy4000319t4imm289uo",
"triggers": ["responseFinished"],
"surveyIds": ["clisypjy4000319t4imm289uo"]
}
}
```
```json {{ title: '400 Bad Request' }}
{
"code": "bad_request",
"message": "Missing trigger",
"details": {
"missing_field": "trigger"
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Delete Webhook by ID {{ tag: 'DELETE', label: '/api/v1/webhooks/<webhook-id>' }}
<Row>
<Col>
### Mandatory Headers
<Properties>
<Property name="x-Api-Key" type="string">
Your Formbricks API key.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="DELETE" label="/api/v1/webhooks/<webhook-id>">
```bash {{ title: 'cURL' }}
curl --location --request DELETE 'https://app.formbricks.com/api/v1/webhooks/<webhook-id>' \
--header 'x-api-key: <your-api-key>'
```
</CodeGroup>
<CodeGroup title="Response">
```json {{ title: '200 Success' }}
{
"data": {
"id": "cliu167rk000019zfhbo68bar",
"createdAt": "2023-06-13T08:38:02.960Z",
"updatedAt": "2023-06-13T08:38:02.960Z",
"url": "https://mysystem.com/myendpoint",
"environmentId": "clisypjy4000319t4imm289uo",
"triggers": ["responseFinished"]
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
"message": "Not authenticated",
"details": {
"x-Api-Key": "Header not provided or API Key invalid"
}
}
```
```json {{ title: '404 Not Found' }}
{
"code": "not_found",
"message": "Webhook not found.",
"details": {
"webhookId": "The requested webhook does not exist."
}
}
```
</CodeGroup>
</Col>
</Row>
---
## Webhook Payload
This documentation helps understand the payload structure that will be received when the webhook is triggered in Formbricks.
<Row>
<Col sticky>
| Variable | Type | Description |
| --------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| webhookId | String | Webhook's Id |
| event | String | The name of the trigger event [responseCreated, responseUpdated, responseFinished] |
| data | Object | Contains the details of the newly created response. |
| data.id | String | Formbricks Response ID. |
| data.createdAt | String | The timestamp when the response was created. |
| data.updatedAt | String | The timestamp when the response was last updated. |
| data.surveyId | String | The identifier of the survey associated with this response. |
| data.finished | Boolean | A boolean value indicating whether the survey response is marked as finished. |
| data.data | Object | An object containing the response data, where keys are question identifiers, and values are the corresponding answers given by the respondent. |
| data.meta | Object | Additional metadata related to the response, such as the user's operating system and browser information. |
| data.personAttributes | Object | An object with attributes related to the respondent, such as their email and a user ID (if available). |
| data.person | Object | Information about the respondent, including their unique id, attributes, and creation/update timestamps. |
| data.notes | Array | An array of notes associated with the response (if any). |
| data.tags | Array | An array of tags assigned to the response (if any). |
</Col>
<Col>
### An example webhook payload
<CodeGroup title="Payload">
```json
{
"webhookId": "cljwxvjos0003qhnvj2jg4k5i",
"event": "responseCreated",
"data": {
"id": "cljwy2m8r0001qhclco1godnu",
"createdAt": "2023-07-10T14:14:17.115Z",
"updatedAt": "2023-07-10T14:14:17.115Z",
"surveyId": "cljsf3d7a000019cv9apt2t27",
"finished": false,
"data": {
"qumbk3fkr6cky8850bvvq5z1": "Executive"
},
"meta": {
"userAgent": {
"os": "Mac OS",
"browser": "Chrome"
}
},
"personAttributes": {
"email": "test@web.com",
"userId": "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING"
},
"person": {
"id": "cljold01t0000qh8ewzigzmjk",
"attributes": {
"email": "test@web.com",
"userId": "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING"
},
"createdAt": "2023-07-04T17:56:17.154Z",
"updatedAt": "2023-07-04T17:56:17.154Z"
},
"notes": [],
"tags": []
}
}
```
</CodeGroup>
</Col>
</Row>
---

View File

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

View File

@@ -90,7 +90,7 @@ Click on Create New Credentail button to add your host and API Key
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Now you need an API key. Please refer to the [API Key Setup](/docs/additional-features/api#how-to-generate-an-api-key) page to learn how to create one.
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.
Once you copied it in the API Key field, hit Save button to test the connection and save the credentials.

View File

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

View File

@@ -215,10 +215,7 @@ export const navigation: Array<NavGroup> = [
},
{
title: "Additional Features",
links: [
{ title: "Multi Language Surveys", href: "/docs/additional-features/multi-language-surveys" },
{ title: "API", href: "/docs/additional-features/api" },
],
links: [{ title: "Multi Language Surveys", href: "/docs/additional-features/multi-language-surveys" }],
},
{
title: "Best Practices",
@@ -270,6 +267,29 @@ export const navigation: Array<NavGroup> = [
{ title: "FAQ", href: "/docs/faq" },
],
},
{
title: "Client API",
links: [
{ title: "Overview", href: "/docs/api/client/overview" },
{ title: "Actions", href: "/docs/api/client/actions" },
{ title: "Displays", href: "/docs/api/client/displays" },
{ title: "People", href: "/docs/api/client/people" },
{ title: "Responses", href: "/docs/api/client/responses" },
],
},
{
title: "Management API",
links: [
{ title: "API Key Setup", href: "/docs/api/management/api-key-setup" },
{ title: "Action Classes", href: "/docs/api/management/action-classes" },
{ title: "Attribute Classes", href: "/docs/api/management/attribute-classes" },
{ title: "Me", href: "/docs/api/management/me" },
{ title: "People", href: "/docs/api/management/people" },
{ title: "Responses", href: "/docs/api/management/responses" },
{ title: "Surveys", href: "/docs/api/management/surveys" },
{ title: "Webhooks", href: "/docs/api/management/webhooks" },
],
},
];
export function Navigation(props: React.ComponentPropsWithoutRef<"nav">) {

View File

@@ -2,7 +2,6 @@ import CalLogoLight from "@/images/clients/cal-logo-light.svg";
import CrowdLogoLight from "@/images/clients/crowd-logo-light.svg";
import FlixbusLogo from "@/images/clients/flixbus-white.svg";
import GumtreeLogo from "@/images/clients/gumtree.png";
import LelyLogo from "@/images/clients/lely-logo.webp";
import NILogoDark from "@/images/clients/niLogoDark.svg";
import OpinodoLogo from "@/images/clients/opinodo.png";
import OptimoleLogo from "@/images/clients/optimole-logo.svg";
@@ -43,11 +42,10 @@ export const Hero: React.FC = ({}) => {
know what your customers need.
</span>
</p>
<div className="mx-auto mt-5 max-w-xl items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0 lg:max-w-5xl">
<div className="grid grid-cols-3 items-center gap-8 pt-2 md:grid-cols-3 lg:grid-cols-9">
<div className="mx-auto mt-5 max-w-xl items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0 lg:max-w-4xl">
<div className="grid grid-cols-2 items-center gap-8 pt-2 md:grid-cols-3 lg:grid-cols-8">
<Image src={FlixbusLogo} alt="Flixbus Flix Flixtrain Logo" className="rounded-md" width={200} />
<Image src={GumtreeLogo} alt="Gumtree Logo" width={200} />
<Image src={LelyLogo} alt="Lely Logo" width={200} />
<Image src={CalLogoLight} alt="Cal Logo" width={170} />
<Image src={ThemeisleLogo} alt="ThemeIsle Logo" width={200} />
<Image src={OpinodoLogo} alt="Opinodo.com Logo" width={200} />

View File

@@ -19,7 +19,7 @@ export const SurveyTypeSelection: React.FC = () => {
subheading="Follow individual feedback trails or zoom out for the big picture. All in one place."
/>
<div className="space-y-6 text-center md:flex md:space-x-8 md:space-y-0">
<div className="flex space-x-8 text-center">
<OptionCard
size="lg"
title="On your website"
@@ -32,7 +32,7 @@ export const SurveyTypeSelection: React.FC = () => {
<OptionCard
size="lg"
title="In emails"
description="Embed branded surveys in your emails."
description="Create on brand surveys, embedded in your emails."
onSelect={() => {
router.push("/open-source-form-builder");
}}>

View File

@@ -2,7 +2,6 @@ import CalLogoLight from "@/images/clients/cal-logo-light.svg";
import CrowdLogoLight from "@/images/clients/crowd-logo-light.svg";
import FlixbusLogo from "@/images/clients/flixbus-white.svg";
import GumtreeLogo from "@/images/clients/gumtree.png";
import LelyLogo from "@/images/clients/lely-logo.webp";
import NILogoDark from "@/images/clients/niLogoDark.svg";
import OpinodoLogo from "@/images/clients/opinodo.png";
import OptimoleLogo from "@/images/clients/optimole-logo.svg";
@@ -16,16 +15,35 @@ export default function LogoBar() {
10,000+ teams at the worlds best companies trust Formbricks
</p>
<div className="mt-5 items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0">
<div className="grid grid-cols-3 items-center gap-8 pt-2 md:grid-cols-3 md:gap-10 lg:grid-cols-9">
<Image src={FlixbusLogo} alt="Flixbus Flix Flixtrain Logo" width={200} />
<Image src={GumtreeLogo} alt="Gumtree Logo" width={200} />
<Image src={LelyLogo} alt="Lely Logo" width={200} />
<Image src={CalLogoLight} alt="Cal Logo" width={200} />
<Image src={ThemeisleLogo} alt="ThemeIsle Logo" width={200} />
<Image src={OpinodoLogo} alt="Crowd.dev Logo" width={200} />
<Image src={CrowdLogoLight} alt="Crowd.dev Logo" width={200} />
<Image src={OptimoleLogo} alt="Optimole Logo" width={200} />
<Image src={NILogoDark} alt="Neverinstall Logo" width={200} />
<div className="grid grid-cols-2 items-center gap-8 pt-2 md:grid-cols-2 md:gap-10 lg:grid-cols-8">
<Image
src={FlixbusLogo}
alt="Flixbus Flix Flixtrain Logo"
className="rounded-lg pb-1 "
width={200}
/>
<Image
src={GumtreeLogo}
alt="Flixbus Flix Flixtrain Logo"
className="rounded-lg pb-1 "
width={200}
/>
<Image src={CalLogoLight} alt="Cal Logo" className="block dark:hidden" width={170} />
<Image src={ThemeisleLogo} alt="ThemeIsle Logo" className="pb-1" width={200} />
<Image
src={OpinodoLogo}
alt="Crowd.dev Logo"
className="block rounded-lg pb-1 dark:hidden"
width={200}
/>
<Image
src={CrowdLogoLight}
alt="Crowd.dev Logo"
className="block rounded-lg pb-1 dark:hidden"
width={200}
/>
<Image src={OptimoleLogo} alt="Optimole Logo" className="pb-1" width={200} />
<Image src={NILogoDark} alt="Neverinstall Logo" className="block pb-1 dark:hidden" width={200} />
</div>
</div>
</div>

View File

@@ -1,11 +1,7 @@
import CCPALogo from "@/images/ccpa.svg";
import GPDRLogo from "@/images/gdpr.svg";
import Image from "next/image";
import Link from "next/link";
import { FaDiscord, FaGithub, FaXTwitter } from "react-icons/fa6";
import { FooterLogo } from "./Logo";
import SourceForgeBadge from "./SourceForgeBadge";
const navigation = {
products: [
@@ -93,11 +89,6 @@ export default function Footer() {
</Link>
))}
</div>
<div className="flex space-x-4">
<SourceForgeBadge />
<Image src={GPDRLogo} alt="GDPR Logo" width={50} />
<Image src={CCPALogo} alt="CCPA Logo" width={50} />
</div>
</div>
<div className="grid grid-cols-2 gap-8 lg:col-span-2 lg:grid-cols-4">
<div>

View File

@@ -1,34 +0,0 @@
import React, { useEffect } from "react";
const SourceForgeBadge: React.FC = () => {
useEffect(() => {
const script = document.createElement("script");
script.async = true;
script.src = "https://b.sf-syn.com/badge_js?sf_id=3747607&variant_id=sf";
const firstScript = document.getElementsByTagName("script")[0];
firstScript.parentNode?.insertBefore(script, firstScript);
return () => {
// Clean up the script when the component unmounts
firstScript.parentNode?.removeChild(script);
};
}, []);
return (
<div
data-id="3747607"
data-badge="heart-badge-white"
data-variant-id="sf"
style={{ width: "75px" }}
className="p-0.5">
<a
href="https://sourceforge.net/software/product/Formbricks/"
target="_blank"
rel="noopener noreferrer">
Formbricks Reviews
</a>
</div>
);
};
export default SourceForgeBadge;

View File

@@ -1,11 +0,0 @@
<svg width="32" height="50" viewBox="0 0 32 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.59947 48.1181C9.09296 48.1181 9.38736 47.8022 9.47458 47.4725L9.17472 47.3736C9.10928 47.5934 8.92393 47.8022 8.59947 47.8022C8.26962 47.8022 7.95884 47.5605 7.95884 47.1044C7.95884 46.6318 8.27784 46.3983 8.59677 46.3983C8.92117 46.3983 9.10112 46.5879 9.15834 46.8241L9.46642 46.7198C9.37921 46.3791 9.0875 46.0879 8.59677 46.0879C8.09519 46.0879 7.62354 46.4698 7.62354 47.1044C7.62354 47.739 8.07881 48.1181 8.59947 48.1181ZM10.0062 47.1016C10.0062 46.6319 10.3279 46.3983 10.6523 46.3983C10.9795 46.3983 11.3012 46.6319 11.3012 47.1016C11.3012 47.5715 10.9795 47.805 10.6523 47.805C10.3279 47.805 10.0062 47.5715 10.0062 47.1016ZM9.67091 47.1016C9.67091 47.7418 10.1453 48.1181 10.6523 48.1181C11.1593 48.1181 11.6365 47.7418 11.6365 47.1016C11.6365 46.4643 11.1593 46.0879 10.6523 46.0879C10.1453 46.0879 9.67091 46.4643 9.67091 47.1016ZM14.1244 48.0769V46.1291H13.68L13.0612 47.6154L12.4341 46.1291H11.9979V48.0769H12.3169V46.6483L12.914 48.0769H13.1974L13.7999 46.6428V48.0769H14.1244ZM14.9367 47.0247V46.4203H15.2775C15.4847 46.4203 15.6101 46.5385 15.6101 46.7253C15.6101 46.9093 15.4847 47.0247 15.2775 47.0247H14.9367ZM15.3266 47.316C15.6973 47.316 15.9427 47.066 15.9427 46.7225C15.9427 46.3819 15.6973 46.1291 15.3266 46.1291H14.6096V48.0769H14.9367V47.316H15.3266ZM17.4735 48.0769V47.7637H16.5874V46.1291H16.2603V48.0769H17.4735ZM18.0944 48.0769V46.1291H17.7619V48.0769H18.0944ZM19.8987 48.0769H20.2558L19.5033 46.1291H19.1244L18.372 48.0769H18.7182L18.8981 47.5879H19.716L19.8987 48.0769ZM19.307 46.4863L19.6015 47.283H19.0126L19.307 46.4863ZM22.1554 48.0769V46.1291H21.8282V47.511L20.9504 46.1291H20.5333V48.0769H20.8604V46.6016L21.8146 48.0769H22.1554ZM24.0293 46.4396V46.1291H22.4454V46.4396H23.0724V48.0769H23.3996V46.4396H24.0293Z" fill="#64748b"/>
<path d="M7.4822 43.2638C9.40139 43.2638 10.2215 41.9451 10.4221 41.0836L9.10479 40.7055C8.98269 41.1539 8.55521 41.8748 7.4822 41.8748C6.55747 41.8748 5.70255 41.1978 5.70255 40.0287C5.70255 38.7187 6.63596 38.121 7.46476 38.121C8.55521 38.121 8.9478 38.8506 9.04374 39.2814L10.3436 38.8682C10.1429 37.9715 9.32289 36.7671 7.46476 36.7671C5.73744 36.7671 4.27185 38.0858 4.27185 40.0287C4.27185 41.9715 5.70254 43.2638 7.4822 43.2638ZM13.8911 43.2638C15.8104 43.2638 16.6304 41.9451 16.831 41.0836L15.5138 40.7055C15.3916 41.1539 14.9641 41.8748 13.8911 41.8748C12.9664 41.8748 12.1115 41.1978 12.1115 40.0287C12.1115 38.7187 13.0449 38.121 13.8737 38.121C14.9641 38.121 15.3567 38.8506 15.4527 39.2814L16.7525 38.8682C16.5519 37.9715 15.7319 36.7671 13.8737 36.7671C12.1464 36.7671 10.6808 38.0858 10.6808 40.0287C10.6808 41.9715 12.1115 43.2638 13.8911 43.2638ZM18.8345 39.6946V38.1033H19.6371C20.1605 38.1033 20.5182 38.4023 20.5182 38.9033C20.5182 39.3868 20.1605 39.6946 19.6371 39.6946H18.8345ZM19.7767 40.8902C21.0329 40.8902 21.8965 40.0726 21.8965 38.8945C21.8965 37.7341 21.0329 36.899 19.7767 36.899H17.4474V43.1319H18.8258V40.8902H19.7767ZM26.1414 43.1319H27.6419L25.3388 36.899H23.7424L21.4132 43.1319H22.8613L23.3062 41.866H25.6965L26.1414 43.1319ZM24.5188 38.4462L25.2603 40.6H23.7599L24.5188 38.4462Z" fill="#64748b"/>
<path d="M14.393 7.2895V0.507324H6.22569V2.47134L5.29443 5.16282L6.22569 6.10842V8.80019L8.08849 10.6914V11.5645L9.73642 14.2559V15.4197L12.4588 18.9114V21.8213H15.3962L19.838 24.3672V26.3315L26.2859 25.3131V20.8757L21.7725 16.0466H12.3824V7.2895H14.393Z" fill="#64748b"/>
<path d="M15.1082 23.3091V22.4849H12.3821V23.3091H15.1082Z" fill="#64748b"/>
<path d="M15.3815 25.5071V24.4082H16.1993V25.5071H15.3815Z" fill="#64748b"/>
<path d="M16.1989 24.4082V23.584H17.0167V24.4082H16.1989Z" fill="#64748b"/>
<path d="M13.7455 9.20666C13.7455 8.90341 13.9895 8.65723 14.2907 8.65723H21.3787C21.68 8.65723 21.924 8.90341 21.924 9.20666V14.1284C21.924 14.432 21.68 14.6779 21.3787 14.6779H14.2907C13.9895 14.6779 13.7455 14.432 13.7455 14.1284V9.20666Z" fill="#64748b"/>
<path d="M17.0931 12.9479L16.017 11.9471C15.896 11.8345 15.896 11.6528 16.017 11.5402C16.1373 11.4284 16.3327 11.4278 16.4538 11.5386C16.4541 11.5391 16.4546 11.5394 16.4551 11.5399L17.3501 12.3699L19.4863 10.3833C19.6063 10.2718 19.8015 10.2713 19.9223 10.382C19.9231 10.3825 19.9239 10.3833 19.9247 10.3842C20.046 10.4977 20.0458 10.68 19.9239 10.7932L17.6072 12.9479C17.4663 13.079 17.237 13.0795 17.0953 12.9495C17.0944 12.949 17.0939 12.9485 17.0931 12.9479Z" fill="#131212"/>
<path d="M19.7595 8.79228V7.27003C19.7595 6.26615 18.9517 5.45215 17.9553 5.45215C16.9589 5.45215 16.1511 6.26615 16.1511 7.27003V8.79228" stroke="#64748b" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,19 +0,0 @@
<svg width="34" height="50" viewBox="0 0 34 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.1891 49.1803H12.5685L11.7691 47.2431H11.3665L10.567 49.1803H10.9349L11.126 48.694H11.995L12.1891 49.1803ZM11.5606 47.5983L11.8734 48.3907H11.2477L11.5606 47.5983ZM14.1523 49.1803V48.8688H13.2109V47.2431H12.8633V49.1803H14.1523ZM14.8121 49.1803V47.2431H14.4588V49.1803H14.8121ZM17.1461 49.1803V48.1995H16.141V48.4863H16.8188C16.8014 48.653 16.645 48.9208 16.2308 48.9208C15.8774 48.9208 15.553 48.683 15.553 48.2104C15.553 47.7295 15.9006 47.5055 16.2336 47.5055C16.561 47.5055 16.7638 47.6913 16.8332 47.9098L17.1606 47.7923C17.0505 47.4726 16.7319 47.2021 16.2336 47.2021C15.7007 47.2021 15.1997 47.5738 15.1997 48.2104C15.1997 48.8497 15.6746 49.2213 16.2105 49.2213C16.5378 49.2213 16.7492 49.0765 16.839 48.9344L16.8651 49.1803H17.1461ZM19.3023 49.1803V47.2431H18.9547V48.6175L18.022 47.2431H17.5788V49.1803H17.9264V47.7131L18.9402 49.1803H19.3023ZM21.0877 49.1803V48.8743H20.1666V48.3606H21.0008V48.0628H20.1666V47.5491H21.0877V47.2431H19.819V49.1803H21.0877ZM21.8469 48.8798V47.5437H22.2148C22.571 47.5437 22.8636 47.7623 22.8636 48.2158C22.8636 48.6639 22.5681 48.8798 22.2118 48.8798H21.8469ZM22.2234 49.1803C22.7796 49.1803 23.2256 48.8361 23.2256 48.2158C23.2256 47.5929 22.7854 47.2431 22.2263 47.2431H21.4993V49.1803H22.2234Z" fill="#64748b"/>
<path d="M11.1576 44.2621V40.9485H7.81151V42.1201H9.78584C9.70237 42.4698 9.24824 43.1343 8.16377 43.1343C7.09784 43.1343 6.18945 42.4611 6.18945 41.1671C6.18945 39.7856 7.23684 39.2261 8.12664 39.2261C9.22037 39.2261 9.65604 39.9256 9.75804 40.3103L11.1669 39.8469C10.8796 38.9813 10.0082 37.9321 8.12664 37.9321C6.27287 37.9321 4.70642 39.2173 4.70642 41.1671C4.70642 43.1256 6.20799 44.3933 8.03397 44.3933C8.97017 44.3933 9.59117 44.0261 9.87851 43.6326L9.97117 44.2621H11.1576ZM13.5866 42.9594V39.366H14.4394C15.4497 39.366 16.2746 39.9256 16.2746 41.1671C16.2746 42.4086 15.4497 42.9594 14.4394 42.9594H13.5866ZM14.495 44.2621C16.4786 44.2621 17.804 43.0818 17.804 41.1671C17.804 39.2523 16.4786 38.0633 14.5043 38.0633H12.1222V44.2621H14.495ZM20.1248 40.8436V39.2611H20.9775C21.5336 39.2611 21.9137 39.5583 21.9137 40.0567C21.9137 40.5376 21.5336 40.8436 20.9775 40.8436H20.1248ZM21.1258 42.0326C22.4606 42.0326 23.3782 41.2195 23.3782 40.0479C23.3782 38.8938 22.4606 38.0633 21.1258 38.0633H18.651V44.2621H20.1155V42.0326H21.1258ZM27.2181 44.2621H28.8309L27.4962 41.7529C28.3026 41.4818 28.7938 40.8436 28.7938 40.0043C28.7938 38.9026 27.9596 38.0633 26.662 38.0633H24.0574V44.2621H25.5219V41.9539H26.041L27.2181 44.2621ZM25.5219 40.7649V39.2611H26.3839C26.9864 39.2611 27.3108 39.5496 27.3108 40.0129C27.3108 40.4501 26.9864 40.7649 26.3839 40.7649H25.5219Z" fill="#64748b"/>
<path d="M16.8006 1.87061L17.3072 3.38312H18.9472L17.6206 4.31787L18.1272 5.83039L16.8006 4.89506L15.4739 5.83039L15.9808 4.31787L14.6536 3.38312H16.2936L16.8006 1.87061Z" fill="#64748b"/>
<path d="M16.8006 26.7607L17.3072 28.2736H18.9472L17.6206 29.2083L18.1272 30.7206L16.8006 29.7859L15.4739 30.7206L15.9808 29.2083L14.6536 28.2736H16.2936L16.8006 26.7607Z" fill="#64748b"/>
<path d="M23.5467 4.13281L24.053 5.6453H25.6933L24.3664 6.58044L24.8733 8.09257L23.5467 7.15744L22.2195 8.09257L22.7264 6.58044L21.3998 5.6453H23.0401L23.5467 4.13281Z" fill="#64748b"/>
<path d="M29.0669 8.42285L29.5731 9.93523H31.2135L29.8866 10.8702L30.3935 12.3827L29.0669 11.448L27.7399 12.3827L28.2468 10.8702L26.9199 9.93523H28.5597L29.0669 8.42285Z" fill="#64748b"/>
<path d="M29.0669 20.5381L29.5735 22.0512H31.2135L29.8866 22.9859L30.3937 24.498L29.0669 23.5635L27.7399 24.498L28.2468 22.9859L26.9199 22.0512H28.5599L29.0669 20.5381Z" fill="#64748b"/>
<path d="M4.53114 8.42285L5.03772 9.93523H6.67777L5.35101 10.8702L5.85758 12.3827L4.53114 11.448L3.20412 12.3827L3.71092 10.8702L2.38416 9.93523H4.02459L4.53114 8.42285Z" fill="#64748b"/>
<path d="M30.294 14.3154L30.8006 15.8279H32.4406L31.1138 16.7626L31.6206 18.2751L30.294 17.341L28.9671 18.2751L29.4743 16.7626L28.1471 15.8279H29.7868L30.294 14.3154Z" fill="#64748b"/>
<path d="M3.30594 14.3154L3.81249 15.8279H5.45246L4.12549 16.7626L4.63265 18.2751L3.30594 17.341L1.97889 18.2751L2.48578 16.7626L1.15881 15.8279H2.79905L3.30594 14.3154Z" fill="#64748b"/>
<path d="M4.53128 20.5381L5.0378 22.0512H6.67777L5.35118 22.9859L5.85764 24.498L4.53128 23.5635L3.20432 24.498L3.71112 22.9859L2.38416 22.0512H4.02448L4.53128 20.5381Z" fill="#64748b"/>
<path d="M24.7739 25.6294L25.2805 27.1417H26.9206L25.5934 28.077L26.1006 29.5893L24.7739 28.6546L23.4467 29.5893L23.9539 28.077L22.627 27.1417H24.2673L24.7739 25.6294Z" fill="#64748b"/>
<path d="M10.0512 4.13281L10.5576 5.6453H12.198L10.871 6.58044L11.3782 8.09257L10.0512 7.15744L8.7245 8.09257L9.2313 6.58044L7.9043 5.6453H9.5443L10.0512 4.13281Z" fill="#64748b"/>
<path d="M8.82574 25.6294L9.33248 27.1425H10.9727L9.64581 28.0767L10.1526 29.5893L8.82574 28.6551L7.49914 29.5893L8.00588 28.0767L6.67908 27.1425H8.31888L8.82574 25.6294Z" fill="#64748b"/>
<path d="M12.4551 15.3887C12.4551 15.0869 12.7145 14.8423 13.0344 14.8423H20.5654C20.8855 14.8423 21.1447 15.0869 21.1447 15.3887V20.2604C21.1447 20.5623 20.8855 20.8068 20.5654 20.8068H13.0344C12.7145 20.8068 12.4551 20.5623 12.4551 20.2604V15.3887Z" fill="#64748b"/>
<path d="M16.0118 19.1917L14.8686 18.2004C14.7399 18.0887 14.7399 17.9087 14.8686 17.7972C14.9962 17.6865 15.2039 17.6857 15.3325 17.7956C15.333 17.7961 15.3334 17.7964 15.334 17.7969L16.285 18.6193L18.5547 16.6513C18.6822 16.5406 18.8896 16.5401 19.0179 16.6496C19.0188 16.6504 19.0196 16.651 19.0205 16.6518C19.1494 16.7644 19.1491 16.9453 19.0196 17.0573L16.5581 19.1917C16.4084 19.3215 16.1648 19.3223 16.0142 19.1934C16.0133 19.1928 16.0127 19.1923 16.0118 19.1917Z" fill="#64748b"/>
<path d="M18.8449 14.9775V13.4766C18.8449 12.478 17.9867 11.6685 16.928 11.6685C15.8693 11.6685 15.0111 12.478 15.0111 13.4766V14.9775" stroke="#64748b" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -210,11 +210,6 @@ const nextConfig = {
destination: "/blog",
permanent: true,
},
{
source: '/docs/api/:slug*',
destination: '/docs/additional-features/api',
permanent: false,
}
];
},
};

View File

@@ -17,19 +17,19 @@
},
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",
"@storybook/addon-essentials": "^8.0.9",
"@storybook/addon-interactions": "^8.0.9",
"@storybook/addon-links": "^8.0.9",
"@storybook/addon-onboarding": "^8.0.9",
"@storybook/blocks": "^8.0.9",
"@storybook/react": "^8.0.9",
"@storybook/react-vite": "^8.0.9",
"@storybook/addon-essentials": "^8.0.8",
"@storybook/addon-interactions": "^8.0.8",
"@storybook/addon-links": "^8.0.8",
"@storybook/addon-onboarding": "^8.0.8",
"@storybook/blocks": "^8.0.8",
"@storybook/react": "^8.0.8",
"@storybook/react-vite": "^8.0.8",
"@storybook/testing-library": "^0.2.2",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"@typescript-eslint/eslint-plugin": "^7.7.0",
"@typescript-eslint/parser": "^7.7.0",
"@vitejs/plugin-react": "^4.2.1",
"esbuild": "^0.20.2",
"tsup": "^8.0.2",
"vite": "^5.2.10"
"vite": "^5.2.9"
}
}

View File

@@ -20,14 +20,9 @@ import {
interface ActivityTabProps {
actionClass: TActionClass;
environmentId: string;
isUserTargetingEnabled: boolean;
}
export default function EventActivityTab({
actionClass,
environmentId,
isUserTargetingEnabled,
}: ActivityTabProps) {
export default function EventActivityTab({ actionClass, environmentId }: ActivityTabProps) {
// const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClass.id);
const [numEventsLastHour, setNumEventsLastHour] = useState<number | undefined>();
@@ -51,9 +46,9 @@ export default function EventActivityTab({
numEventsLast7DaysData,
activeInactiveSurveys,
] = await Promise.all([
isUserTargetingEnabled ? getActionCountInLastHourAction(actionClass.id, environmentId) : 0,
isUserTargetingEnabled ? getActionCountInLast24HoursAction(actionClass.id, environmentId) : 0,
isUserTargetingEnabled ? getActionCountInLast7DaysAction(actionClass.id, environmentId) : 0,
getActionCountInLastHourAction(actionClass.id, environmentId),
getActionCountInLast24HoursAction(actionClass.id, environmentId),
getActionCountInLast7DaysAction(actionClass.id, environmentId),
getActiveInactiveSurveysAction(actionClass.id, environmentId),
]);
setNumEventsLastHour(numEventsLastHourData);
@@ -67,7 +62,7 @@ export default function EventActivityTab({
setLoading(false);
}
}
}, [actionClass.id, environmentId, isUserTargetingEnabled]);
}, [actionClass.id, environmentId]);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorComponent />;
@@ -75,25 +70,23 @@ export default function EventActivityTab({
return (
<div className="grid grid-cols-3 pb-2">
<div className="col-span-2 space-y-4 pr-6">
{isUserTargetingEnabled && (
<div>
<Label className="text-slate-500">Ocurrances</Label>
<div className="mt-1 grid w-fit grid-cols-3 rounded-lg border-slate-100 bg-slate-50">
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLastHour}</p>
<p className="text-xs text-slate-500">last hour</p>
</div>
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLast24Hours}</p>
<p className="text-xs text-slate-500">last 24 hours</p>
</div>
<div className="px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLast7Days}</p>
<p className="text-xs text-slate-500">last week</p>
</div>
<div>
<Label className="text-slate-500">Ocurrances</Label>
<div className="mt-1 grid w-fit grid-cols-3 rounded-lg border-slate-100 bg-slate-50">
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLastHour}</p>
<p className="text-xs text-slate-500">last hour</p>
</div>
<div className="border-r border-slate-200 px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLast24Hours}</p>
<p className="text-xs text-slate-500">last 24 hours</p>
</div>
<div className="px-4 py-2 text-center">
<p className="font-bold text-slate-800">{numEventsLast7Days}</p>
<p className="text-xs text-slate-500">last week</p>
</div>
</div>
)}
</div>
<div>
<Label className="text-slate-500">Active surveys</Label>

View File

@@ -12,19 +12,15 @@ import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import ActionDetailModal from "./ActionDetailModal";
import AddNoCodeActionModal from "./AddActionModal";
interface ActionClassesTableProps {
environmentId: string;
actionClasses: TActionClass[];
children: [JSX.Element, JSX.Element[]];
isUserTargetingEnabled: boolean;
}
export default function ActionClassesTable({
environmentId,
actionClasses,
children: [TableHeading, actionRows],
isUserTargetingEnabled,
}: ActionClassesTableProps) {
}: {
environmentId: string;
actionClasses: TActionClass[];
children: [JSX.Element, JSX.Element[]];
}) {
const [isActionDetailModalOpen, setActionDetailModalOpen] = useState(false);
const [isAddActionModalOpen, setAddActionModalOpen] = useState(false);
const { membershipRole, isLoading, error } = useMembershipRole(environmentId);
@@ -87,7 +83,6 @@ export default function ActionClassesTable({
setOpen={setActionDetailModalOpen}
actionClass={activeActionClass}
membershipRole={membershipRole}
isUserTargetingEnabled={isUserTargetingEnabled}
/>
<AddNoCodeActionModal
environmentId={environmentId}

View File

@@ -13,7 +13,6 @@ interface ActionDetailModalProps {
setOpen: (v: boolean) => void;
actionClass: TActionClass;
membershipRole?: TMembershipRole;
isUserTargetingEnabled: boolean;
}
export default function ActionDetailModal({
@@ -22,18 +21,11 @@ export default function ActionDetailModal({
setOpen,
actionClass,
membershipRole,
isUserTargetingEnabled,
}: ActionDetailModalProps) {
const tabs = [
{
title: "Activity",
children: (
<EventActivityTab
actionClass={actionClass}
environmentId={environmentId}
isUserTargetingEnabled={isUserTargetingEnabled}
/>
),
children: <EventActivityTab actionClass={actionClass} environmentId={environmentId} />,
},
{
title: "Settings",

View File

@@ -4,34 +4,16 @@ import ActionTableHeading from "@/app/(app)/environments/[environmentId]/(action
import { Metadata } from "next";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
export const metadata: Metadata = {
title: "Actions",
};
export default async function ActionClassesComponent({ params }) {
const [actionClasses, team] = await Promise.all([
getActionClasses(params.environmentId),
getTeamByEnvironmentId(params.environmentId),
]);
if (!team) {
throw new Error("Team not found");
}
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
? team.billing.features.userTargeting.status === "active"
: true;
let actionClasses = await getActionClasses(params.environmentId);
return (
<>
<ActionClassesTable
environmentId={params.environmentId}
actionClasses={actionClasses}
isUserTargetingEnabled={isUserTargetingEnabled}>
<ActionClassesTable environmentId={params.environmentId} actionClasses={actionClasses}>
<ActionTableHeading />
{actionClasses.map((actionClass) => (
<ActionClassDataRow key={actionClass.id} actionClass={actionClass} />

View File

@@ -1,9 +1,7 @@
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ActivityTimeline";
import { getActionsByPersonId } from "@formbricks/lib/action/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
export default async function ActivitySection({
environmentId,
@@ -12,20 +10,9 @@ export default async function ActivitySection({
environmentId: string;
personId: string;
}) {
const team = await getTeamByEnvironmentId(environmentId);
if (!team) {
throw new Error("Team not found");
}
// On Formbricks Cloud only render the timeline if the user targeting feature is booked
const isUserTargetingEnabled = IS_FORMBRICKS_CLOUD
? team.billing.features.userTargeting.status === "active"
: true;
const [environment, actions] = await Promise.all([
getEnvironment(environmentId),
isUserTargetingEnabled ? getActionsByPersonId(personId, 1) : [],
getActionsByPersonId(personId, 1),
]);
if (!environment) {
@@ -34,11 +21,7 @@ export default async function ActivitySection({
return (
<div className="md:col-span-1">
<ActivityTimeline
environment={environment}
actions={actions.slice(0, 10)}
isUserTargetingEnabled={isUserTargetingEnabled}
/>
<ActivityTimeline environment={environment} actions={actions.slice(0, 10)} />
</div>
);
}

View File

@@ -1,71 +1,58 @@
import { TAction } from "@formbricks/types/actions";
import { TEnvironment } from "@formbricks/types/environment";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
import { ActivityItemContent, ActivityItemIcon, ActivityItemPopover } from "./ActivityItemComponents";
interface IActivityTimelineProps {
environment: TEnvironment;
actions: TAction[];
isUserTargetingEnabled: boolean;
}
export default function ActivityTimeline({
environment,
actions,
isUserTargetingEnabled,
}: IActivityTimelineProps) {
}: {
environment: TEnvironment;
actions: TAction[];
}) {
return (
<>
<div className="flex items-center justify-between pb-6">
<h2 className="text-lg font-bold text-slate-700">Actions Timeline</h2>
</div>
{!isUserTargetingEnabled ? (
<UpgradePlanNotice
message="Upgrade to the User Targeting plan to store action history."
textForUrl="Upgrade now."
url={`/environments/${environment.id}/settings/billing`}
/>
) : (
<div className="relative">
{actions.length === 0 ? (
<EmptySpaceFiller type={"event"} environment={environment} />
) : (
<div>
{actions.map(
(actionItem, index) =>
actionItem && (
<li key={actionItem.id} className="list-none">
<div className="relative pb-12">
{index !== actions.length - 1 && (
<span
className="absolute left-6 top-4 -ml-px h-full w-0.5 bg-slate-200"
aria-hidden="true"
/>
)}
<div className="relative">
<ActivityItemPopover actionItem={actionItem}>
<div className="flex space-x-3 text-left">
<ActivityItemIcon actionItem={actionItem} />
<ActivityItemContent actionItem={actionItem} />
</div>
</ActivityItemPopover>
</div>
<div className="relative">
{actions.length === 0 ? (
<EmptySpaceFiller type={"event"} environment={environment} />
) : (
<div>
{actions.map(
(actionItem, index) =>
actionItem && (
<li key={actionItem.id} className="list-none">
<div className="relative pb-12">
{index !== actions.length - 1 && (
<span
className="absolute left-6 top-4 -ml-px h-full w-0.5 bg-slate-200"
aria-hidden="true"
/>
)}
<div className="relative">
<ActivityItemPopover actionItem={actionItem}>
<div className="flex space-x-3 text-left">
<ActivityItemIcon actionItem={actionItem} />
<ActivityItemContent actionItem={actionItem} />
</div>
</ActivityItemPopover>
</div>
</li>
)
</div>
</li>
)
)}
<div className="relative">
{actions.length === 10 && (
<div className="absolute bottom-0 flex h-56 w-full items-end justify-center bg-gradient-to-t from-slate-50 to-transparent"></div>
)}
<div className="relative">
{actions.length === 10 && (
<div className="absolute bottom-0 flex h-56 w-full items-end justify-center bg-gradient-to-t from-slate-50 to-transparent"></div>
)}
</div>
</div>
)}
</div>
)}
</div>
)}
</div>
</>
);
}

View File

@@ -1,10 +1,9 @@
import { getAttributes } from "@formbricks/lib/attribute/service";
import { getPerson } from "@formbricks/lib/person/service";
import { getResponsesByPersonId } from "@formbricks/lib/response/service";
import { capitalizeFirstLetter } from "@formbricks/lib/strings";
export default async function AttributesSection({ personId }: { personId: string }) {
const [person, attributes] = await Promise.all([getPerson(personId), getAttributes(personId)]);
const person = await getPerson(personId);
if (!person) {
throw new Error("No such person found");
}
@@ -19,8 +18,8 @@ export default async function AttributesSection({ personId }: { personId: string
<div>
<dt className="text-sm font-medium text-slate-500">Email</dt>
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
{attributes.email ? (
<span>{attributes.email}</span>
{person.attributes.email ? (
<span>{person.attributes.email}</span>
) : (
<span className="text-slate-300">Not provided</span>
)}
@@ -29,8 +28,8 @@ export default async function AttributesSection({ personId }: { personId: string
<div>
<dt className="text-sm font-medium text-slate-500">Language</dt>
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
{attributes.language ? (
<span>{attributes.language}</span>
{person.attributes.language ? (
<span>{person.attributes.language}</span>
) : (
<span className="text-slate-300">Not provided</span>
)}
@@ -51,8 +50,8 @@ export default async function AttributesSection({ personId }: { personId: string
<dd className="ph-no-capture mt-1 text-sm text-slate-900">{person.id}</dd>
</div>
{Object.entries(attributes)
.filter(([key, _]) => key !== "email" && key !== "userId" && key !== "language")
{Object.entries(person.attributes)
.filter(([key, _]) => key !== "email" && key !== "language")
.map(([key, value]) => (
<div key={key}>
<dt className="text-sm font-medium text-slate-500">{capitalizeFirstLetter(key.toString())}</dt>

View File

@@ -1,6 +1,5 @@
import { getServerSession } from "next-auth";
import { getAttributes } from "@formbricks/lib/attribute/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
@@ -32,9 +31,6 @@ export default async function HeadingSection({ environmentId, personId }: Headin
if (!person) {
throw new Error("No such person found");
}
const personAttributes = await getAttributes(person.id);
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
@@ -43,7 +39,7 @@ export default async function HeadingSection({ environmentId, personId }: Headin
<GoBackButton />
<div className="flex items-baseline justify-between border-b border-slate-200 pb-6 pt-4">
<h1 className="ph-no-capture text-4xl font-bold tracking-tight text-slate-900">
<span>{getPersonIdentifier(person, personAttributes)}</span>
<span>{getPersonIdentifier(person)}</span>
</h1>
{!isViewer && (
<div className="flex items-center space-x-3">

View File

@@ -8,19 +8,15 @@ import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
export default async function PersonPage({ params }) {
const [environment, environmentTags, product] = await Promise.all([
getEnvironment(params.environmentId),
getTagsByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
]);
const environment = await getEnvironment(params.environmentId);
const environmentTags = await getTagsByEnvironmentId(params.environmentId);
const product = await getProductByEnvironmentId(params.environmentId);
if (!product) {
throw new Error("Product not found");
}
if (!environment) {
throw new Error("Environment not found");
}
return (
<div>
<main className="mx-auto px-4 sm:px-6 lg:px-8">

View File

@@ -1,39 +0,0 @@
import Link from "next/link";
import React from "react";
import { getAttributes } from "@formbricks/lib/attribute/service";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { TPerson } from "@formbricks/types/people";
import { PersonAvatar } from "@formbricks/ui/Avatars";
export const PersonCard = async ({ person }: { person: TPerson }) => {
const attributes = await getAttributes(person.id);
return (
<Link
href={`/environments/${person.environmentId}/people/${person.id}`}
key={person.id}
className="w-full">
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg hover:bg-slate-100">
<div className="col-span-3 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="ph-no-capture h-10 w-10 flex-shrink-0">
<PersonAvatar personId={person.id} />
</div>
<div className="ml-4">
<div className="ph-no-capture font-medium text-slate-900">
<span>{getPersonIdentifier({ id: person.id, userId: person.userId }, attributes)}</span>
</div>
</div>
</div>
</div>
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
<div className="ph-no-capture text-slate-900">{person.userId}</div>
</div>
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
<div className="ph-no-capture text-slate-900">{attributes.email}</div>
</div>
</div>
</Link>
);
};

View File

@@ -1,13 +1,16 @@
import HowToAddPeopleButton from "@/app/(app)/environments/[environmentId]/components/HowToAddPeopleButton";
import Link from "next/link";
import { ITEMS_PER_PAGE } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getPeople, getPeopleCount } from "@formbricks/lib/person/service";
import { TPerson } from "@formbricks/types/people";
import { PersonAvatar } from "@formbricks/ui/Avatars";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import { Pagination } from "@formbricks/ui/Pagination";
import { PersonCard } from "./components/PersonCard";
const getAttributeValue = (person: TPerson, attributeName: string) =>
person.attributes[attributeName]?.toString();
export default async function PeoplePage({
params,
@@ -57,7 +60,35 @@ export default async function PeoplePage({
<div className="col-span-2 hidden text-center sm:block">Email</div>
</div>
{people.map((person) => (
<PersonCard person={person} />
<Link
href={`/environments/${params.environmentId}/people/${person.id}`}
key={person.id}
className="w-full">
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg hover:bg-slate-100">
<div className="col-span-3 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="ph-no-capture h-10 w-10 flex-shrink-0">
<PersonAvatar personId={person.id} />
</div>
<div className="ml-4">
<div className="ph-no-capture font-medium text-slate-900">
{getAttributeValue(person, "email") ? (
<span>{getAttributeValue(person, "email")}</span>
) : (
<span>{person.id}</span>
)}
</div>
</div>
</div>
</div>
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
<div className="ph-no-capture text-slate-900">{person.userId}</div>
</div>
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
<div className="ph-no-capture text-slate-900">{getAttributeValue(person, "email")}</div>
</div>
</div>
</Link>
))}
</div>
)}

View File

@@ -24,10 +24,9 @@ type ThemeStylingProps = {
product: TProduct;
environmentId: string;
colors: string[];
isUnsplashConfigured: boolean;
};
export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigured }: ThemeStylingProps) => {
export const ThemeStyling = ({ product, environmentId, colors }: ThemeStylingProps) => {
const router = useRouter();
const [localProduct, setLocalProduct] = useState(product);
const [previewSurveyType, setPreviewSurveyType] = useState<TSurveyType>("link");
@@ -212,7 +211,6 @@ export const ThemeStyling = ({ product, environmentId, colors, isUnsplashConfigu
colors={colors}
key={styling.background?.bg}
hideCheckmark
isUnsplashConfigured={isUnsplashConfigured}
/>
</div>
</div>

View File

@@ -6,7 +6,7 @@ import {
getRemoveLinkBrandingPermission,
} from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
import { SURVEY_BG_COLORS } from "@formbricks/lib/constants";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
@@ -53,12 +53,7 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro
title="Theme"
className="max-w-7xl"
description="Create a style theme for all surveys. You can enable custom styling for each survey.">
<ThemeStyling
environmentId={params.environmentId}
product={product}
colors={SURVEY_BG_COLORS}
isUnsplashConfigured={UNSPLASH_ACCESS_KEY ? true : false}
/>
<ThemeStyling environmentId={params.environmentId} product={product} colors={SURVEY_BG_COLORS} />
</SettingsCard>{" "}
<SettingsCard title="Logo" description="Upload your company logo to brand surveys and link previews.">
<EditLogo product={product} environmentId={params.environmentId} isViewer={isViewer} />

View File

@@ -2,7 +2,6 @@
import { getServerSession } from "next-auth";
import { sendInviteMemberEmail } from "@formbricks/email";
import { hasTeamAuthority } from "@formbricks/lib/auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { INVITE_DISABLED } from "@formbricks/lib/constants";
@@ -110,36 +109,8 @@ export const createInviteTokenAction = async (inviteId: string) => {
return { inviteToken: encodeURIComponent(inviteToken) };
};
export const resendInviteAction = async (inviteId: string, teamId: string) => {
const session = await getServerSession(authOptions);
if (!session) {
throw new AuthenticationError("Not authenticated");
}
const isUserAuthorized = await hasTeamAuthority(session.user.id, teamId);
if (INVITE_DISABLED) {
throw new AuthenticationError("Invite disabled");
}
if (!isUserAuthorized) {
throw new AuthenticationError("Not authorized");
}
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(teamId, session.user.id);
if (!hasCreateOrUpdateMembersAccess) {
throw new AuthenticationError("Not authorized");
}
const invite = await getInvite(inviteId);
const updatedInvite = await resendInvite(inviteId);
await sendInviteMemberEmail(
inviteId,
updatedInvite.email,
invite?.creator.name ?? "",
updatedInvite.name ?? ""
);
export const resendInviteAction = async (inviteId: string) => {
return await resendInvite(inviteId);
};
export const inviteUserAction = async (
@@ -179,10 +150,6 @@ export const inviteUserAction = async (
},
});
if (invite) {
await sendInviteMemberEmail(invite.id, email, session.user.name ?? "", name ?? "", false);
}
return invite;
};

View File

@@ -87,7 +87,7 @@ export default function MemberActions({ team, member, invite, showDeleteButton }
try {
if (!invite) return;
await resendInviteAction(invite.id, team.id);
await resendInviteAction(invite.id);
toast.success("Invitation sent once more.");
} catch (err) {
toast.error(`Error: ${err.message}`);

View File

@@ -4,8 +4,8 @@ import { getEmailTemplateHtml } from "@/app/(app)/environments/[environmentId]/s
import { customAlphabet } from "nanoid";
import { getServerSession } from "next-auth";
import { sendEmbedSurveyPreviewEmail } from "@formbricks/email";
import { authOptions } from "@formbricks/lib/authOptions";
import { sendEmbedSurveyPreviewEmail } from "@formbricks/lib/emails/emails";
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
import { formatSurveyDateFields } from "@formbricks/lib/survey/util";

View File

@@ -38,7 +38,7 @@ export const AddressSummary = ({ questionSummary, environmentId }: AddressSummar
<PersonAvatar personId={response.person.id} />
</div>
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{getPersonIdentifier(response.person, response.personAttributes)}
{getPersonIdentifier(response.person)}
</p>
</Link>
) : (

View File

@@ -1,5 +1,3 @@
import { InboxIcon } from "lucide-react";
import { TSurveyQuestionSummaryCta } from "@formbricks/types/surveys";
import { ProgressBar } from "@formbricks/ui/ProgressBar";
@@ -12,33 +10,12 @@ interface CTASummaryProps {
export const CTASummary = ({ questionSummary }: CTASummaryProps) => {
return (
<div className="rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
<QuestionSummaryHeader
questionSummary={questionSummary}
showResponses={false}
insights={
<>
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.impressionCount} Impressions`}
</div>
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.clickCount} Clicks`}
</div>
{!questionSummary.question.required && (
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.skipCount} Skips`}
</div>
)}
</>
}
/>
<div className=" rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} />
<div className="space-y-5 rounded-b-lg bg-white px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
<div className="text flex justify-between px-2 pb-2">
<div className="mr-8 flex space-x-1">
<p className="font-semibold text-slate-700">CTR</p>
<p className="font-semibold text-slate-700">Click-through rate (CTR)</p>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(questionSummary.ctr.percentage, 1)}%
@@ -46,7 +23,7 @@ export const CTASummary = ({ questionSummary }: CTASummaryProps) => {
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{questionSummary.ctr.count} {questionSummary.ctr.count === 1 ? "Click" : "Clicks"}
{questionSummary.ctr.count} {questionSummary.ctr.count === 1 ? "click" : "clicks"}
</p>
</div>
<ProgressBar barColor="bg-brand" progress={questionSummary.ctr.percentage / 100} />

View File

@@ -48,7 +48,7 @@ export const DateQuestionSummary = ({ questionSummary, environmentId }: DateQues
<PersonAvatar personId={response.person.id} />
</div>
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{getPersonIdentifier(response.person, response.personAttributes)}
{getPersonIdentifier(response.person)}
</p>
</Link>
) : (

View File

@@ -49,7 +49,7 @@ export const FileUploadSummary = ({ questionSummary, environmentId }: FileUpload
<PersonAvatar personId={response.person.id} />
</div>
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{getPersonIdentifier(response.person, response.personAttributes)}
{getPersonIdentifier(response.person)}
</p>
</Link>
) : (

View File

@@ -59,7 +59,7 @@ export const HiddenFieldsSummary = ({ environment, questionSummary }: HiddenFiel
<PersonAvatar personId={response.person.id} />
</div>
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{getPersonIdentifier(response.person, response.personAttributes)}
{getPersonIdentifier(response.person)}
</p>
</Link>
) : (

View File

@@ -97,7 +97,7 @@ export const MultipleChoiceSummary = ({
</div>
<div className="ph-no-capture col-span-1 flex items-center space-x-4 pl-6 font-medium text-slate-900">
{otherValue.person.id && <PersonAvatar personId={otherValue.person.id} />}
<span>{getPersonIdentifier(otherValue.person, otherValue.personAttributes)}</span>
<span>{getPersonIdentifier(otherValue.person)}</span>
</div>
</Link>
)}

View File

@@ -47,7 +47,7 @@ export const OpenTextSummary = ({ questionSummary, environmentId }: OpenTextSumm
<PersonAvatar personId={response.person.id} />
</div>
<p className="ph-no-capture break-all text-slate-600 group-hover:underline md:ml-2">
{getPersonIdentifier(response.person, response.personAttributes)}
{getPersonIdentifier(response.person)}
</p>
</Link>
) : (

View File

@@ -6,11 +6,9 @@ import { TSurveyQuestionSummary } from "@formbricks/types/surveys";
interface HeadProps {
questionSummary: TSurveyQuestionSummary;
showResponses?: boolean;
insights?: JSX.Element;
}
export const QuestionSummaryHeader = ({ questionSummary, insights, showResponses = true }: HeadProps) => {
export const QuestionSummaryHeader = ({ questionSummary }: HeadProps) => {
const questionType = questionTypes.find((type) => type.id === questionSummary.question.type);
return (
@@ -25,13 +23,10 @@ export const QuestionSummaryHeader = ({ questionSummary, insights, showResponses
{questionType && <questionType.icon className="mr-2 h-4 w-4 " />}
{questionType ? questionType.label : "Unknown Question Type"} Question
</div>
{showResponses && (
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.responseCount} Responses`}
</div>
)}
{insights}
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.responseCount} Responses`}
</div>
{!questionSummary.question.required && (
<div className="flex items-center rounded-lg bg-slate-100 p-2">Optional</div>
)}

View File

@@ -25,8 +25,8 @@ export const SummaryDropOffs = ({ dropOff }: SummaryDropOffsProps) => {
</Tooltip>
</TooltipProvider>
</div>
<div className="px-4 text-center md:px-6">Impressions</div>
<div className="pr-6 text-center md:pl-6">Drop-Offs</div>
<div className="px-4 text-center md:px-6">Views</div>
<div className="pr-6 text-center md:pl-6">Drop Offs</div>
</div>
{dropOff.map((quesDropOff) => (
<div
@@ -36,9 +36,9 @@ export const SummaryDropOffs = ({ dropOff }: SummaryDropOffsProps) => {
<div className="whitespace-pre-wrap text-center font-semibold">
{quesDropOff.ttc > 0 ? (quesDropOff.ttc / 1000).toFixed(2) + "s" : "N/A"}
</div>
<div className="whitespace-pre-wrap text-center font-semibold">{quesDropOff.impressions}</div>
<div className="whitespace-pre-wrap text-center font-semibold">{quesDropOff.views}</div>
<div className=" pl-6 text-center md:px-6">
<span className="mr-1.5 font-semibold">{quesDropOff.dropOffCount}</span>
<span className="font-semibold">{quesDropOff.dropOffCount}</span>
<span>({Math.round(quesDropOff.dropOffPercentage)}%)</span>
</div>
</div>

View File

@@ -1,7 +1,8 @@
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { timeSinceConditionally } from "@formbricks/lib/time";
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys";
import { TSurveySummary } from "@formbricks/types/surveys";
import { TSurvey } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
@@ -70,7 +71,7 @@ export const SummaryMetadata = ({
<div className="flex flex-col-reverse gap-y-2 lg:grid lg:grid-cols-3 lg:gap-x-2">
<div className="grid grid-cols-2 gap-4 md:grid-cols-5 md:gap-x-2 lg:col-span-2">
<StatCard
label="Impressions"
label="Displays"
percentage={null}
value={displayCount === 0 ? <span>-</span> : displayCount}
tooltipText="Number of times the survey has been viewed."
@@ -88,7 +89,7 @@ export const SummaryMetadata = ({
tooltipText="Number of times the survey has been completed."
/>
<StatCard
label="Drop-Offs"
label="Drop Offs"
percentage={`${Math.round(dropOffPercentage)}%`}
value={dropOffCount === 0 ? <span>-</span> : dropOffCount}
tooltipText="Number of times the survey has been started but not completed."
@@ -109,7 +110,7 @@ export const SummaryMetadata = ({
className="w-max self-start"
EndIcon={showDropOffs ? ChevronDownIcon : ChevronUpIcon}
onClick={() => setShowDropOffs(!showDropOffs)}>
Analyze Drop-Offs
Analyze Drop Offs
</Button>
</div>
</div>

View File

@@ -95,6 +95,10 @@ const SummaryPage = ({
updatedResponseCount = await getResponseCountAction(surveyId, filters);
}
setResponseCount(updatedResponseCount);
if (updatedResponseCount === 0) {
setSurveySummary(initialSurveySummary);
return;
}
let updatedSurveySummary;
if (isSharingPage) {

View File

@@ -1,8 +1,32 @@
import { getPreviewEmailTemplateHtml } from "@formbricks/email/components/survey/PreviewEmailTemplste";
import {
Column,
Container,
Button as EmailButton,
Img,
Link,
Row,
Section,
Tailwind,
Text,
} from "@react-email/components";
import { render } from "@react-email/render";
import { CalendarDaysIcon } from "lucide-react";
import { cn } from "@formbricks/lib/cn";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
import { getSurvey } from "@formbricks/lib/survey/service";
import { isLight } from "@formbricks/lib/utils";
import { TSurvey, TSurveyQuestionType } from "@formbricks/types/surveys";
import { RatingSmiley } from "@formbricks/ui/RatingSmiley";
interface EmailTemplateProps {
survey: TSurvey;
surveyUrl: string;
brandColor: string;
}
export const getEmailTemplateHtml = async (surveyId) => {
const survey = await getSurvey(surveyId);
@@ -15,10 +39,362 @@ export const getEmailTemplateHtml = async (surveyId) => {
}
const brandColor = product.styling.brandColor?.light || COLOR_DEFAULTS.brandColor;
const surveyUrl = WEBAPP_URL + "/s/" + survey.id;
const html = getPreviewEmailTemplateHtml(survey, surveyUrl, brandColor);
const html = render(<EmailTemplate survey={survey} surveyUrl={surveyUrl} brandColor={brandColor} />, {
pretty: true,
});
const doctype =
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
const htmlCleaned = html.toString().replace(doctype, "");
return htmlCleaned;
};
const EmailTemplate = ({ survey, surveyUrl, brandColor }: EmailTemplateProps) => {
const url = `${surveyUrl}?preview=true`;
const urlWithPrefilling = `${surveyUrl}?preview=true&`;
const defaultLanguageCode = "default";
const firstQuestion = survey.questions[0];
switch (firstQuestion.type) {
case TSurveyQuestionType.OpenText:
return (
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
</Text>
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
</Text>
<Section className="mt-4 block h-20 w-full rounded-lg border border-solid border-slate-200 bg-slate-50" />
<EmailFooter />
</EmailTemplateWrapper>
);
case TSurveyQuestionType.Consent:
return (
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
</Text>
<Container className="m-0 text-sm font-normal leading-6 text-slate-500">
<Text
className="m-0 p-0"
dangerouslySetInnerHTML={{
__html: getLocalizedValue(firstQuestion.html, defaultLanguageCode) || "",
}}></Text>
</Container>
<Container className="m-0 mt-4 block w-full max-w-none rounded-lg border border-solid border-slate-200 bg-slate-50 p-4 font-medium text-slate-800">
<Text className="m-0 inline-block">
{getLocalizedValue(firstQuestion.label, defaultLanguageCode)}
</Text>
</Container>
<Container className="mx-0 mt-4 flex max-w-none justify-end">
{!firstQuestion.required && (
<EmailButton
href={`${urlWithPrefilling}${firstQuestion.id}=dismissed`}
className="inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-black">
Reject
</EmailButton>
)}
<EmailButton
href={`${urlWithPrefilling}${firstQuestion.id}=accepted`}
className={cn(
"bg-brand-color ml-2 inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium",
isLight(brandColor) ? "text-black" : "text-white"
)}>
Accept
</EmailButton>
</Container>
<EmailFooter />
</EmailTemplateWrapper>
);
case TSurveyQuestionType.NPS:
return (
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
<Section>
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
</Text>
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
</Text>
<Container className="mx-0 mt-4 flex w-max flex-col">
<Section className="block overflow-hidden rounded-md border border-slate-200">
{Array.from({ length: 11 }, (_, i) => (
<EmailButton
key={i}
href={`${urlWithPrefilling}${firstQuestion.id}=${i}`}
className="m-0 inline-flex h-10 w-10 items-center justify-center border-slate-200 p-0 text-slate-800">
{i}
</EmailButton>
))}
</Section>
<Section className="mt-2 px-1.5 text-xs leading-6 text-slate-500">
<Row>
<Column>
<Text className="m-0 inline-block w-max p-0">
{getLocalizedValue(firstQuestion.lowerLabel, defaultLanguageCode)}
</Text>
</Column>
<Column className="text-right">
<Text className="m-0 inline-block w-max p-0 text-right">
{getLocalizedValue(firstQuestion.upperLabel, defaultLanguageCode)}
</Text>
</Column>
</Row>
</Section>
</Container>
<EmailFooter />
</Section>
</EmailTemplateWrapper>
);
case TSurveyQuestionType.CTA:
return (
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
</Text>
<Container className="mt-2 text-sm font-normal leading-6 text-slate-500">
<Text
className="m-0 p-0"
dangerouslySetInnerHTML={{
__html: getLocalizedValue(firstQuestion.html, defaultLanguageCode) || "",
}}></Text>
</Container>
<Container className="mx-0 mt-4 max-w-none">
{!firstQuestion.required && (
<EmailButton
href={`${urlWithPrefilling}${firstQuestion.id}=dismissed`}
className="inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium text-black">
{getLocalizedValue(firstQuestion.dismissButtonLabel, defaultLanguageCode) || "Skip"}
</EmailButton>
)}
<EmailButton
href={`${urlWithPrefilling}${firstQuestion.id}=clicked`}
className={cn(
"bg-brand-color inline-flex cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium",
isLight(brandColor) ? "text-black" : "text-white"
)}>
{getLocalizedValue(firstQuestion.buttonLabel, defaultLanguageCode)}
</EmailButton>
</Container>
<EmailFooter />
</EmailTemplateWrapper>
);
case TSurveyQuestionType.Rating:
return (
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
<Section className=" w-full">
<Text className="m-0 block text-base font-semibold leading-6 text-slate-800">
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
</Text>
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
</Text>
<Container className="mx-0 mt-4 w-full items-center justify-center">
<Section
className={cn("w-full overflow-hidden rounded-md", {
["border border-solid border-gray-200"]: firstQuestion.scale === "number",
})}>
<Column className="mb-4 flex w-full justify-around">
{Array.from({ length: firstQuestion.range }, (_, i) => (
<EmailButton
key={i}
href={`${urlWithPrefilling}${firstQuestion.id}=${i + 1}`}
className={cn(
" m-0 h-10 w-full p-0 text-center align-middle leading-10 text-slate-800",
{
["border border-solid border-gray-200"]: firstQuestion.scale === "number",
}
)}>
{firstQuestion.scale === "smiley" && (
<RatingSmiley active={false} idx={i} range={firstQuestion.range} />
)}
{firstQuestion.scale === "number" && (
<Text className="m-0 flex h-10 items-center">{i + 1}</Text>
)}
{firstQuestion.scale === "star" && <Text className="text-3xl"></Text>}
</EmailButton>
))}
</Column>
</Section>
<Section className="m-0 px-1.5 text-xs leading-6 text-slate-500">
<Row>
<Column>
<Text className="m-0 inline-block p-0">
{getLocalizedValue(firstQuestion.lowerLabel, defaultLanguageCode)}
</Text>
</Column>
<Column className="text-right">
<Text className="m-0 inline-block p-0 text-right">
{getLocalizedValue(firstQuestion.upperLabel, defaultLanguageCode)}
</Text>
</Column>
</Row>
</Section>
</Container>
<EmailFooter />
</Section>
</EmailTemplateWrapper>
);
case TSurveyQuestionType.MultipleChoiceMulti:
return (
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
</Text>
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
</Text>
<Container className="mx-0 max-w-none">
{firstQuestion.choices.map((choice) => (
<Section
className="mt-2 block w-full rounded-lg border border-solid border-slate-200 bg-slate-50 p-4 text-slate-800"
key={choice.id}>
{getLocalizedValue(choice.label, defaultLanguageCode)}
</Section>
))}
</Container>
<EmailFooter />
</EmailTemplateWrapper>
);
case TSurveyQuestionType.MultipleChoiceSingle:
return (
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
</Text>
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
</Text>
<Container className="mx-0 max-w-none">
{firstQuestion.choices.map((choice) => (
<Link
key={choice.id}
className="mt-2 block rounded-lg border border-solid border-slate-200 bg-slate-50 p-4 text-slate-800 hover:bg-slate-100"
href={`${urlWithPrefilling}${firstQuestion.id}=${getLocalizedValue(choice.label, defaultLanguageCode)}`}>
{getLocalizedValue(choice.label, defaultLanguageCode)}
</Link>
))}
</Container>
<EmailFooter />
</EmailTemplateWrapper>
);
case TSurveyQuestionType.PictureSelection:
return (
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
</Text>
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
</Text>
<Section className="mx-0">
{firstQuestion.choices.map((choice) =>
firstQuestion.allowMulti ? (
<Img
src={choice.imageUrl}
className="mb-1 mr-1 inline-block h-[110px] w-[220px] rounded-lg"
/>
) : (
<Link
href={`${urlWithPrefilling}${firstQuestion.id}=${choice.id}`}
target="_blank"
className="mb-1 mr-1 inline-block h-[110px] w-[220px] rounded-lg">
<Img src={choice.imageUrl} className="h-full w-full rounded-lg" />
</Link>
)
)}
</Section>
<EmailFooter />
</EmailTemplateWrapper>
);
case TSurveyQuestionType.Cal:
return (
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
<Container>
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
</Text>
<Text className="m-0 mb-2 block p-0 text-sm font-normal leading-6 text-slate-500">
You have been invited to schedule a meet via cal.com.
</Text>
<EmailButton
className={cn(
"bg-brand-color mx-auto block w-max cursor-pointer appearance-none rounded-md px-6 py-3 text-sm font-medium ",
isLight(brandColor) ? "text-black" : "text-white"
)}>
Schedule your meeting
</EmailButton>
</Container>
<EmailFooter />
</EmailTemplateWrapper>
);
case TSurveyQuestionType.Date:
return (
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
</Text>
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
</Text>
<Section className="mt-4 flex h-12 w-full items-center justify-center rounded-lg border border-solid border-slate-200 bg-white">
<CalendarDaysIcon className="mb-1 inline h-4 w-4" />
<Text className="inline text-sm font-medium">Select a date</Text>
</Section>
<EmailFooter />
</EmailTemplateWrapper>
);
case TSurveyQuestionType.Address:
return (
<EmailTemplateWrapper surveyUrl={url} brandColor={brandColor}>
<Text className="m-0 mr-8 block p-0 text-base font-semibold leading-6 text-slate-800">
{getLocalizedValue(firstQuestion.headline, defaultLanguageCode)}
</Text>
<Text className="m-0 block p-0 text-sm font-normal leading-6 text-slate-500">
{getLocalizedValue(firstQuestion.subheader, defaultLanguageCode)}
</Text>
{Array.from({ length: 6 }).map((_, index) => (
<Section
key={index}
className="mt-4 block h-10 w-full rounded-lg border border-solid border-slate-200 bg-slate-50"
/>
))}
<EmailFooter />
</EmailTemplateWrapper>
);
}
};
const EmailTemplateWrapper = ({ children, surveyUrl, brandColor }) => {
return (
<Tailwind
config={{
theme: {
extend: {
colors: {
"brand-color": brandColor,
},
},
},
}}>
<Link
href={surveyUrl}
target="_blank"
className="mx-0 my-2 block overflow-auto rounded-lg border border-solid border-slate-300 bg-white p-8 font-sans text-inherit">
{children}
</Link>
</Tailwind>
);
};
const EmailFooter = () => {
return (
<Container className="m-auto mt-8 text-center ">
<Link href="https://formbricks.com/" target="_blank" className="text-xs text-slate-400">
Powered by Formbricks
</Link>
</Container>
);
};

View File

@@ -3,7 +3,6 @@
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { canUserAccessProduct } from "@formbricks/lib/product/auth";
import { getProduct } from "@formbricks/lib/product/service";
@@ -200,64 +199,3 @@ export const resetBasicSegmentFiltersAction = async (surveyId: string) => {
return await resetSegmentInSurvey(surveyId);
};
export async function getImagesFromUnsplashAction(searchQuery: string, page: number = 1) {
if (!UNSPLASH_ACCESS_KEY) {
throw new Error("Unsplash access key is not set");
}
const baseUrl = "https://api.unsplash.com/search/photos";
const params = new URLSearchParams({
query: searchQuery,
client_id: UNSPLASH_ACCESS_KEY,
orientation: "landscape",
per_page: "9",
page: page.toString(),
});
try {
const response = await fetch(`${baseUrl}?${params}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to fetch images from Unsplash");
}
const { results } = await response.json();
return results.map((result) => {
const authorName = encodeURIComponent(result.user.first_name + " " + result.user.last_name);
const authorLink = encodeURIComponent(result.user.links.html);
return {
id: result.id,
alt_description: result.alt_description,
urls: {
regularWithAttribution: `${result.urls.regular}&dpr=2&authorLink=${authorLink}&authorName=${authorName}&utm_source=formbricks&utm_medium=referral`,
download: result.links.download_location,
},
};
});
} catch (error) {
throw new Error("Error getting images from Unsplash");
}
}
export async function triggerDownloadUnsplashImageAction(downloadUrl: string) {
try {
const response = await fetch(`${downloadUrl}/?client_id=${UNSPLASH_ACCESS_KEY}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || "Failed to download image from Unsplash");
}
return;
} catch (error) {
throw new Error("Error downloading image from Unsplash");
}
}

View File

@@ -20,7 +20,6 @@ interface BackgroundStylingCardProps {
hideCheckmark?: boolean;
disabled?: boolean;
environmentId: string;
isUnsplashConfigured: boolean;
}
export default function BackgroundStylingCard({
@@ -32,7 +31,6 @@ export default function BackgroundStylingCard({
hideCheckmark,
disabled,
environmentId,
isUnsplashConfigured,
}: BackgroundStylingCardProps) {
const { bgType, brightness } = styling?.background ?? {};
@@ -115,7 +113,6 @@ export default function BackgroundStylingCard({
colors={colors}
bgType={bgType}
environmentId={environmentId}
isUnsplashConfigured={isUnsplashConfigured}
/>
</div>

View File

@@ -109,7 +109,6 @@ export default function EditThankYouCard({
<form>
<QuestionFormInput
id="headline"
label="Headline"
value={localSurvey?.thankYouCard?.headline}
localSurvey={localSurvey}
questionIdx={localSurvey.questions.length}

View File

@@ -1,16 +1,12 @@
import { FileInput } from "@formbricks/ui/FileInput";
interface UploadImageSurveyBgProps {
interface ImageSurveyBgProps {
environmentId: string;
handleBgChange: (url: string, bgType: string) => void;
background: string;
}
export const UploadImageSurveyBg = ({
environmentId,
handleBgChange,
background,
}: UploadImageSurveyBgProps) => {
export const ImageSurveyBg = ({ environmentId, handleBgChange, background }: ImageSurveyBgProps) => {
return (
<div className="mt-2 w-full">
<div className="flex w-full items-center justify-center">
@@ -20,9 +16,9 @@ export const UploadImageSurveyBg = ({
environmentId={environmentId}
onFileUpload={(url: string[]) => {
if (url.length > 0) {
handleBgChange(url[0], "upload");
handleBgChange(url[0], "image");
} else {
handleBgChange("", "upload");
handleBgChange("", "image");
}
}}
fileUrl={background}

View File

@@ -23,7 +23,6 @@ type StylingViewProps = {
setStyling: React.Dispatch<React.SetStateAction<TSurveyStyling | null>>;
localStylingChanges: TSurveyStyling | null;
setLocalStylingChanges: React.Dispatch<React.SetStateAction<TSurveyStyling | null>>;
isUnsplashConfigured: boolean;
};
export const StylingView = ({
@@ -36,7 +35,6 @@ export const StylingView = ({
styling,
localStylingChanges,
setLocalStylingChanges,
isUnsplashConfigured,
}: StylingViewProps) => {
const [overwriteThemeStyling, setOverwriteThemeStyling] = useState(
localSurvey?.styling?.overwriteThemeStyling ?? false
@@ -164,7 +162,6 @@ export const StylingView = ({
environmentId={environment.id}
colors={colors}
disabled={!overwriteThemeStyling}
isUnsplashConfigured={isUnsplashConfigured}
/>
)}

View File

@@ -6,8 +6,7 @@ import { TabBar } from "@formbricks/ui/TabBar";
import { AnimatedSurveyBg } from "./AnimatedSurveyBg";
import { ColorSurveyBg } from "./ColorSurveyBg";
import { UploadImageSurveyBg } from "./ImageSurveyBg";
import { ImageFromUnsplashSurveyBg } from "./UnsplashImages";
import { ImageSurveyBg } from "./ImageSurveyBg";
interface SurveyBgSelectorTabProps {
handleBgChange: (bg: string, bgType: string) => void;
@@ -15,13 +14,11 @@ interface SurveyBgSelectorTabProps {
bgType: string | null | undefined;
environmentId: string;
styling: TSurveyStyling | TProductStyling | null;
isUnsplashConfigured: boolean;
}
const tabs = [
{ id: "color", label: "Color" },
{ id: "animation", label: "Animation" },
{ id: "upload", label: "Upload" },
{ id: "image", label: "Image" },
];
@@ -31,59 +28,52 @@ export default function SurveyBgSelectorTab({
colors,
bgType,
environmentId,
isUnsplashConfigured,
}: SurveyBgSelectorTabProps) {
const [activeTab, setActiveTab] = useState(bgType || "color");
const bgUrl = styling?.background?.bg || "";
const { background } = styling ?? {};
const [colorBackground, setColorBackground] = useState(bgUrl);
const [animationBackground, setAnimationBackground] = useState(bgUrl);
const [uploadBackground, setUploadBackground] = useState(bgUrl);
const [colorBackground, setColorBackground] = useState(background?.bg);
const [animationBackground, setAnimationBackground] = useState(background?.bg);
const [imageBackground, setImageBackground] = useState(background?.bg);
useEffect(() => {
const bgType = background?.bgType;
if (bgType === "color") {
setColorBackground(bgUrl);
setColorBackground(background?.bg);
setAnimationBackground("");
setUploadBackground("");
setImageBackground("");
}
if (bgType === "animation") {
setAnimationBackground(bgUrl);
setAnimationBackground(background?.bg);
setColorBackground("");
setUploadBackground("");
setImageBackground("");
}
if (isUnsplashConfigured && bgType === "image") {
setColorBackground("");
setAnimationBackground("");
setUploadBackground("");
}
if (bgType === "upload") {
setUploadBackground(bgUrl);
if (bgType === "image") {
setImageBackground(background?.bg);
setColorBackground("");
setAnimationBackground("");
}
}, [bgUrl, bgType, isUnsplashConfigured]);
}, [background?.bg, background?.bgType]);
const renderContent = () => {
switch (activeTab) {
case "color":
return <ColorSurveyBg handleBgChange={handleBgChange} colors={colors} background={colorBackground} />;
case "animation":
return <AnimatedSurveyBg handleBgChange={handleBgChange} background={animationBackground} />;
case "upload":
return (
<UploadImageSurveyBg
<ColorSurveyBg handleBgChange={handleBgChange} colors={colors} background={colorBackground ?? ""} />
);
case "animation":
return <AnimatedSurveyBg handleBgChange={handleBgChange} background={animationBackground ?? ""} />;
case "image":
return (
<ImageSurveyBg
environmentId={environmentId}
handleBgChange={handleBgChange}
background={uploadBackground}
background={imageBackground ?? ""}
/>
);
case "image":
if (isUnsplashConfigured) {
return <ImageFromUnsplashSurveyBg handleBgChange={handleBgChange} />;
}
default:
return null;
}
@@ -92,7 +82,7 @@ export default function SurveyBgSelectorTab({
return (
<div className="mt-4 flex flex-col items-center justify-center rounded-lg ">
<TabBar
tabs={tabs.filter((tab) => tab.id !== "image" || isUnsplashConfigured)}
tabs={tabs}
activeId={activeTab}
setActiveId={setActiveTab}
tabStyle="button"

View File

@@ -34,7 +34,6 @@ interface SurveyEditorProps {
isUserTargetingAllowed?: boolean;
isMultiLanguageAllowed?: boolean;
isFormbricksCloud: boolean;
isUnsplashConfigured: boolean;
}
export default function SurveyEditor({
@@ -50,7 +49,6 @@ export default function SurveyEditor({
isMultiLanguageAllowed,
isUserTargetingAllowed = false,
isFormbricksCloud,
isUnsplashConfigured,
}: SurveyEditorProps): JSX.Element {
const [activeView, setActiveView] = useState<TSurveyEditorTabs>("questions");
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
@@ -176,7 +174,6 @@ export default function SurveyEditor({
setStyling={setStyling}
localStylingChanges={localStylingChanges}
setLocalStylingChanges={setLocalStylingChanges}
isUnsplashConfigured={isUnsplashConfigured}
/>
)}

View File

@@ -11,7 +11,6 @@ import toast from "react-hot-toast";
import { createSegmentAction } from "@formbricks/ee/advancedTargeting/lib/actions";
import { TEnvironment } from "@formbricks/types/environment";
import { TProduct } from "@formbricks/types/product";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyEditorTabs } from "@formbricks/types/surveys";
import { AlertDialog } from "@formbricks/ui/AlertDialog";
import { Button } from "@formbricks/ui/Button";
@@ -117,10 +116,9 @@ export const SurveyMenuBar = ({
}
};
const handleTemporarySegment = async () => {
const handleSegmentWithIdTemp = async () => {
if (localSurvey.segment && localSurvey.type === "app" && localSurvey.segment?.id === "temp") {
const { filters } = localSurvey.segment;
// create a new private segment
const newSegment = await createSegmentAction({
environmentId: localSurvey.environmentId,
@@ -134,15 +132,6 @@ export const SurveyMenuBar = ({
}
};
const handleSegmentUpdate = async (): Promise<TSegment | null> => {
if (localSurvey.segment && localSurvey.segment.id === "temp") {
const segment = await handleTemporarySegment();
return segment ?? null;
}
return localSurvey.segment;
};
const handleSurveySave = async (shouldNavigateBack = false) => {
setIsSurveySaving(true);
try {
@@ -163,8 +152,7 @@ export const SurveyMenuBar = ({
const { isDraft, ...rest } = question;
return rest;
});
const segment = await handleSegmentUpdate();
const segment = (await handleSegmentWithIdTemp()) ?? null;
await updateSurveyAction({ ...localSurvey, segment });
setIsSurveySaving(false);
setLocalSurvey(localSurvey);
@@ -196,8 +184,7 @@ export const SurveyMenuBar = ({
return;
}
const status = localSurvey.runOnDate ? "scheduled" : "inProgress";
const segment = await handleSegmentUpdate();
const segment = (await handleSegmentWithIdTemp()) ?? null;
await updateSurveyAction({
...localSurvey,
status,

View File

@@ -1,234 +0,0 @@
"use client";
import { debounce } from "lodash";
import { SearchIcon } from "lucide-react";
import UnsplashImage from "next/image";
import { useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import { TSurveyBackgroundBgType } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import LoadingSpinner from "@formbricks/ui/LoadingSpinner";
import { getImagesFromUnsplashAction, triggerDownloadUnsplashImageAction } from "../actions";
interface ImageFromUnsplashSurveyBgProps {
handleBgChange: (url: string, bgType: TSurveyBackgroundBgType) => void;
}
interface UnsplashImage {
id: string;
alt_description: string;
urls: {
regularWithAttribution: string;
download?: string;
};
authorName?: string;
}
const defaultImages = [
{
id: "dog-1",
alt_description: "Dog",
urls: {
regularWithAttribution: "/image-backgrounds/dogs.webp",
},
},
{
id: "pencil",
alt_description: "Pencil",
urls: {
regularWithAttribution: "/image-backgrounds/pencil.webp",
},
},
{
id: "plant",
alt_description: "Plant",
urls: {
regularWithAttribution: "/image-backgrounds/plant.webp",
},
},
{
id: "dog-2",
alt_description: "Another Dog",
urls: {
regularWithAttribution: "/image-backgrounds/dog-2.webp",
},
},
{
id: "kitten-2",
alt_description: "Another Kitten",
urls: {
regularWithAttribution: "/image-backgrounds/kitten-2.webp",
},
},
{
id: "lollipop",
alt_description: "Lollipop",
urls: {
regularWithAttribution: "/image-backgrounds/lolipop.webp",
},
},
{
id: "oranges",
alt_description: "Oranges",
urls: {
regularWithAttribution: "/image-backgrounds/oranges.webp",
},
},
{
id: "flower",
alt_description: "Flower",
urls: {
regularWithAttribution: "/image-backgrounds/flowers.webp",
},
},
{
id: "supermario",
alt_description: "Super Mario",
urls: {
regularWithAttribution: "/image-backgrounds/supermario.webp",
},
},
{
id: "shapes",
alt_description: "Shapes",
urls: {
regularWithAttribution: "/image-backgrounds/shapes.webp",
},
},
{
id: "waves",
alt_description: "Waves",
urls: {
regularWithAttribution: "/image-backgrounds/waves.webp",
},
},
{
id: "kitten-1",
alt_description: "Kitten",
urls: {
regularWithAttribution: "/image-backgrounds/kittens.webp",
},
},
];
export const ImageFromUnsplashSurveyBg = ({ handleBgChange }: ImageFromUnsplashSurveyBgProps) => {
const inputFocus = useRef<HTMLInputElement>(null);
const [isLoading, setIsLoading] = useState(false);
const [query, setQuery] = useState("");
const [images, setImages] = useState<UnsplashImage[]>(defaultImages);
const [page, setPage] = useState(1);
useEffect(() => {
const fetchData = async (searchQuery: string, currentPage: number) => {
try {
setIsLoading(true);
const imagesFromUnsplash = await getImagesFromUnsplashAction(searchQuery, currentPage);
for (let i = 0; i < imagesFromUnsplash.length; i++) {
const authorName = new URL(imagesFromUnsplash[i].urls.regularWithAttribution).searchParams.get(
"authorName"
);
imagesFromUnsplash[i].authorName = authorName;
}
setImages((prevImages) => [...prevImages, ...imagesFromUnsplash]);
} catch (error) {
toast.error(error.message);
} finally {
setIsLoading(false);
}
};
const debouncedFetchData = debounce((q) => fetchData(q, page), 500);
if (query.trim() !== "") {
debouncedFetchData(query);
}
return () => {
debouncedFetchData.cancel();
};
}, [query, page, setImages]);
useEffect(() => {
inputFocus.current?.focus();
}, []);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
setPage(1);
setImages([]);
};
const handleImageSelected = async (imageUrl: string, downloadImageUrl?: string) => {
try {
handleBgChange(imageUrl, "image");
if (downloadImageUrl) {
await triggerDownloadUnsplashImageAction(downloadImageUrl);
}
} catch (error) {
toast.error(error.message);
}
};
const handleLoadMore = () => {
setPage((prevPage) => prevPage + 1);
};
return (
<div className="relative mt-2 w-full">
<div className="relative">
<SearchIcon className="absolute left-2 top-1/2 h-6 w-4 -translate-y-1/2 text-slate-500" />
<Input
value={query}
onChange={handleChange}
placeholder="Try 'lollipop' or 'mountain'..."
className="pl-8"
ref={inputFocus}
aria-label="Search for images"
/>
</div>
<div className="relative mt-4 grid grid-cols-3 gap-1">
{images.length > 0 &&
images.map((image) => (
<div key={image.id} className="group relative">
<UnsplashImage
width={300}
height={200}
src={image.urls.regularWithAttribution}
alt={image.alt_description}
onClick={() => handleImageSelected(image.urls.regularWithAttribution, image.urls.download)}
className="h-full cursor-pointer rounded-lg object-cover"
/>
{image.authorName && (
<span className="absolute bottom-1 right-1 hidden rounded bg-black bg-opacity-75 px-2 py-1 text-xs text-white group-hover:block">
{image.authorName}
</span>
)}
</div>
))}
{isLoading && (
<div className="col-span-3 flex items-center justify-center p-3">
<LoadingSpinner />
</div>
)}
{images.length > 0 && !isLoading && query.trim() !== "" && (
<Button
size="sm"
variant="secondary"
className="col-span-3 mt-3 flex items-center justify-center"
type="button"
onClick={handleLoadMore}>
Load More
</Button>
)}
{!isLoading && images.length === 0 && query.trim() !== "" && (
<div className="col-span-3 flex items-center justify-center text-sm text-slate-500">
No images found for &apos;{query}&apos;
</div>
)}
</div>
</div>
);
};

View File

@@ -4,7 +4,7 @@ import { getAdvancedTargetingPermission, getMultiLanguagePermission } from "@for
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD, SURVEY_BG_COLORS, UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
import { IS_FORMBRICKS_CLOUD, SURVEY_BG_COLORS } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
@@ -87,7 +87,6 @@ export default async function SurveysEditPage({ params }) {
isUserTargetingAllowed={isUserTargetingAllowed}
isMultiLanguageAllowed={isMultiLanguageAllowed}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
isUnsplashConfigured={UNSPLASH_ACCESS_KEY ? true : false}
/>
);
}

View File

@@ -103,10 +103,11 @@ export default function Modal({
}, [clickOutsideClose, scalingClasses.transformOrigin]);
const highlightBorderColorStyle = useMemo(() => {
if (!highlightBorderColor) return;
if (!highlightBorderColor) return { overflow: "auto" };
return {
border: `2px solid ${highlightBorderColor}`,
overflow: "auto",
};
}, [highlightBorderColor]);
@@ -154,7 +155,7 @@ export default function Modal({
}),
}}
className={cn(
"no-scrollbar pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm bg-white shadow-lg transition-all duration-500 ease-in-out ",
"no-scrollbar pointer-events-auto absolute h-fit max-h-[90%] w-full max-w-sm overflow-y-auto bg-white shadow-lg transition-all duration-500 ease-in-out ",
previewMode === "desktop" ? getPlacementStyle(placement) : "max-w-full",
slidingAnimationClass
)}>

View File

@@ -208,7 +208,7 @@ export const PreviewSurvey = ({
}
return (
<div className="flex h-full w-full flex-col items-center justify-items-center" id="survey-preview">
<div className="flex h-full w-full flex-col items-center justify-items-center">
<motion.div
variants={previewParentContainerVariant}
className="fixed hidden h-[95%] w-5/6"
@@ -260,13 +260,13 @@ export const PreviewSurvey = ({
/>
</Modal>
) : (
<div className="flex h-full w-full flex-col justify-end">
<div className="w-full px-3">
<div className="absolute left-5 top-5">
{!styling.isLogoHidden && product.logo?.url && (
<ClientLogo environmentId={environment.id} product={product} previewSurvey />
)}
</div>
<div className="no-scrollbar z-10 w-full border border-transparent">
<div className="no-scrollbar z-10 w-full max-w-md overflow-y-auto rounded-lg border border-transparent">
<SurveyInline
survey={survey}
isBrandingEnabled={product.linkSurveyBranding}

View File

@@ -2,7 +2,6 @@
import { getServerSession } from "next-auth";
import { sendInviteMemberEmail } from "@formbricks/email";
import { hasTeamAuthority } from "@formbricks/lib/auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { INVITE_DISABLED } from "@formbricks/lib/constants";
@@ -56,19 +55,10 @@ export const inviteTeamMateAction = async (
name: "",
role,
},
isOnboardingInvite: true,
inviteMessage: inviteMessage,
});
if (invite) {
await sendInviteMemberEmail(
invite.id,
email,
session.user.name ?? "",
"",
true, // is onboarding invite
inviteMessage
);
}
return invite;
};

View File

@@ -1,8 +1,8 @@
import { getServerSession } from "next-auth";
import { sendInviteAcceptedEmail } from "@formbricks/email";
import { authOptions } from "@formbricks/lib/authOptions";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { sendInviteAcceptedEmail } from "@formbricks/lib/emails/emails";
import { deleteInvite, getInvite } from "@formbricks/lib/invite/service";
import { verifyInviteToken } from "@formbricks/lib/jwt";
import { createMembership } from "@formbricks/lib/membership/service";

View File

@@ -0,0 +1,224 @@
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { withEmailTemplate } from "@formbricks/lib/emails/email-template";
import { sendEmail } from "@formbricks/lib/emails/emails";
import { Insights, NotificationResponse, Survey, SurveyResponse } from "./types";
const getEmailSubject = (productName: string) => {
return `${productName} User Insights - Last Week by Formbricks`;
};
const notificationHeader = (
productName: string,
startDate: string,
endDate: string,
startYear: number,
endYear: number
) =>
`
<div style="display: block; padding: 1rem 0rem;">
<div style="float: left; margin-top: 0.5rem;">
<h1 style="margin: 0rem;">Hey 👋</h1>
</div>
<div style="float: right;">
<p style="text-align: right; margin: 0; font-weight: 600;">Weekly Report for ${productName}</p>
${getNotificationHeaderimePeriod(startDate, endDate, startYear, endYear)}
</div>
</div>
<br/>
<br/>
`;
const getNotificationHeaderimePeriod = (
startDate: string,
endDate: string,
startYear: number,
endYear: number
) => {
if (startYear == endYear) {
return `<p style="text-align: right; margin: 0;">${startDate} - ${endDate} ${endYear}</p>`;
} else {
return `<p style="text-align: right; margin: 0;">${startDate} ${startYear} - ${endDate} ${endYear}</p>`;
}
};
const notificationInsight = (insights: Insights) =>
`<div style="display: block;">
<table style="background-color: #f1f5f9; border-radius:1em; margin-top:1em; margin-bottom:1em;">
<tr>
<td style="text-align:center;">
<p style="font-size:0.9em">Surveys</p>
<h1>${insights.numLiveSurvey}</h1>
</td>
<td style="text-align:center;">
<p style="font-size:0.9em">Displays</p>
<h1>${insights.totalDisplays}</h1>
</td>
<td style="text-align:center;">
<p style="font-size:0.9em">Responses</p>
<h1>${insights.totalResponses}</h1>
</td>
<td style="text-align:center;">
<p style="font-size:0.9em">Completed</p>
<h1>${insights.totalCompletedResponses}</h1>
</td>
${
insights.totalDisplays !== 0
? `<td style="text-align:center;">
<p style="font-size:0.9em">Completion %</p>
<h1>${Math.round(insights.completionRate)}%</h1>
</td>`
: ""
}
</tr>
</table>
</div>
`;
function convertSurveyStatus(status) {
const statusMap = {
inProgress: "Live",
paused: "Paused",
completed: "Completed",
};
return statusMap[status] || status;
}
const getButtonLabel = (count) => {
if (count === 1) {
return "View Response";
}
return `View ${count > 2 ? count - 1 : "1"} more Response${count > 2 ? "s" : ""}`;
};
const notificationLiveSurveys = (surveys: Survey[], environmentId: string) => {
if (!surveys.length) return ` `;
return surveys
.map((survey) => {
const displayStatus = convertSurveyStatus(survey.status);
const isLive = displayStatus === "Live";
const noResponseLastWeek = isLive && survey.responses.length === 0;
return `
<div style="display: block; margin-top: 3em;">
<a href="${WEBAPP_URL}/environments/${environmentId}/surveys/${survey.id}/responses?utm_source=weekly&utm_medium=email&utm_content=ViewResponsesCTA" style="color: #1e293b; text-decoration: none;">
<h2 style="display: inline; text-decoration: underline;">${survey.name}</h2>
</a>
<span style="display: inline; margin-left: 10px; background-color: ${isLive ? "#34D399" : "#cbd5e1"}; color: ${isLive ? "#F3F4F6" : "#1e293b"}; border-radius: 99px; padding: 2px 8px; font-size: 0.9em;">
${displayStatus}
</span>
${noResponseLastWeek ? "<p>No new response received this week 🕵️</p>" : createSurveyFields(survey.responses)}
${survey.responseCount > 0 ? `<a class="button" href="${WEBAPP_URL}/environments/${environmentId}/surveys/${survey.id}/responses?utm_source=weekly&utm_medium=email&utm_content=ViewResponsesCTA">${noResponseLastWeek ? "View previous responses" : getButtonLabel(survey.responseCount)}</a>` : ""}
</div>
<br/>
`;
})
.join("");
};
const createSurveyFields = (surveyResponses: SurveyResponse[]) => {
if (surveyResponses.length === 0)
return `<div style="margin-top:1em;">
<p style="font-weight: bold; margin:0px;">No Responses yet!</p>
</div>`;
let surveyFields = "";
const responseCount = surveyResponses.length;
surveyResponses.forEach((response, index) => {
if (!response) {
return;
}
for (const [headline, answer] of Object.entries(response)) {
surveyFields += `
<div style="margin-top:1em;">
<p style="margin:0px;">${headline}</p>
<p style="font-weight: bold; margin:0px;">${answer}</p>
</div>
`;
}
// Add <hr/> only when there are 2 or more responses to display, and it's not the last response
if (responseCount >= 2 && index < responseCount - 1) {
surveyFields += "<hr/>";
}
});
return surveyFields;
};
const notificationFooter = (environmentId: string) => {
return `
<p style="margin-bottom:0px; padding-top:1em; font-weight:500">All the best,</p>
<p style="margin-top:0px;">The Formbricks Team 🤍</p>
<div style="margin-top:0.8em; background-color:#f1f5f9; border-radius:8px; padding:0.01em 1.6em; text-align:center; font-size:0.8em; line-height:1.2em;">
<p><i>To halt Weekly Updates, <a href="${WEBAPP_URL}/environments/${environmentId}/settings/notifications">please turn them off</a> in your settings 🙏</i></p>
</div>
`;
};
const createReminderNotificationBody = (notificationData: NotificationResponse) => {
return `
<p>Wed love to send you a Weekly Summary, but currently there are no surveys running for ${notificationData.productName}.</p>
<p style="font-weight: bold; padding-top:1em;">Dont let a week pass without learning about your users:</p>
<a class="button" href="${WEBAPP_URL}/environments/${notificationData.environmentId}/surveys?utm_source=weekly&utm_medium=email&utm_content=SetupANewSurveyCTA">Setup a new survey</a>
<br/>
<p style="padding-top:1em;">Need help finding the right survey for your product? Pick a 15-minute slot <a href="https://cal.com/johannes/15">in our CEOs calendar</a> or reply to this email :)</p>
${notificationFooter(notificationData.environmentId)}
`;
};
export const sendWeeklySummaryNotificationEmail = async (
email: string,
notificationData: NotificationResponse
) => {
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const startDate = `${notificationData.lastWeekDate.getDate()} ${
monthNames[notificationData.lastWeekDate.getMonth()]
}`;
const endDate = `${notificationData.currentDate.getDate()} ${
monthNames[notificationData.currentDate.getMonth()]
}`;
const startYear = notificationData.lastWeekDate.getFullYear();
const endYear = notificationData.currentDate.getFullYear();
await sendEmail({
to: email,
subject: getEmailSubject(notificationData.productName),
html: withEmailTemplate(`
${notificationHeader(notificationData.productName, startDate, endDate, startYear, endYear)}
${notificationInsight(notificationData.insights)}
${notificationLiveSurveys(notificationData.surveys, notificationData.environmentId)}
${notificationFooter(notificationData.environmentId)}
`),
});
};
export const sendNoLiveSurveyNotificationEmail = async (
email: string,
notificationData: NotificationResponse
) => {
const monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const startDate = `${notificationData.lastWeekDate.getDate()} ${
monthNames[notificationData.lastWeekDate.getMonth()]
}`;
const endDate = `${notificationData.currentDate.getDate()} ${
monthNames[notificationData.currentDate.getMonth()]
}`;
const startYear = notificationData.lastWeekDate.getFullYear();
const endYear = notificationData.currentDate.getFullYear();
await sendEmail({
to: email,
subject: getEmailSubject(notificationData.productName),
html: withEmailTemplate(`
${notificationHeader(notificationData.productName, startDate, endDate, startYear, endYear)}
${createReminderNotificationBody(notificationData)}
`),
});
};

View File

@@ -2,18 +2,12 @@ import { responses } from "@/app/lib/api/response";
import { headers } from "next/headers";
import { prisma } from "@formbricks/database";
import { sendNoLiveSurveyNotificationEmail, sendWeeklySummaryNotificationEmail } from "@formbricks/email";
import { CRON_SECRET } from "@formbricks/lib/constants";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { convertResponseValue } from "@formbricks/lib/responses";
import { checkForRecallInHeadline } from "@formbricks/lib/utils/recall";
import {
TWeeklySummaryEnvironmentData,
TWeeklySummaryNotificationDataSurvey,
TWeeklySummaryNotificationResponse,
TWeeklySummaryProductData,
TWeeklySummarySurveyResponseData,
} from "@formbricks/types/weeklySummary";
import { sendNoLiveSurveyNotificationEmail, sendWeeklySummaryNotificationEmail } from "./email";
import { EnvironmentData, NotificationResponse, ProductData, Survey, SurveyResponse } from "./types";
const BATCH_SIZE = 500;
@@ -79,7 +73,7 @@ const getTeamIds = async (): Promise<string[]> => {
return teams.map((team) => team.id);
};
const getProductsByTeamId = async (teamId: string): Promise<TWeeklySummaryProductData[]> => {
const getProductsByTeamId = async (teamId: string): Promise<ProductData[]> => {
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
@@ -170,10 +164,7 @@ const getProductsByTeamId = async (teamId: string): Promise<TWeeklySummaryProduc
});
};
const getNotificationResponse = (
environment: TWeeklySummaryEnvironmentData,
productName: string
): TWeeklySummaryNotificationResponse => {
const getNotificationResponse = (environment: EnvironmentData, productName: string): NotificationResponse => {
const insights = {
totalCompletedResponses: 0,
totalDisplays: 0,
@@ -182,11 +173,11 @@ const getNotificationResponse = (
numLiveSurvey: 0,
};
const surveys: TWeeklySummaryNotificationDataSurvey[] = [];
const surveys: Survey[] = [];
// iterate through the surveys and calculate the overall insights
for (const survey of environment.surveys) {
const parsedSurvey = checkForRecallInHeadline(survey, "default");
const surveyData: TWeeklySummaryNotificationDataSurvey = {
const surveyData: Survey = {
id: parsedSurvey.id,
name: parsedSurvey.name,
status: parsedSurvey.status,
@@ -196,21 +187,19 @@ const getNotificationResponse = (
// iterate through the responses and calculate the survey insights
for (const response of parsedSurvey.responses) {
// only take the first 3 responses
if (surveyData.responses.length >= 3) {
if (surveyData.responses.length >= 1) {
break;
}
const surveyResponses: TWeeklySummarySurveyResponseData[] = [];
const surveyResponse: SurveyResponse = {};
for (const question of parsedSurvey.questions) {
const headline = question.headline;
const responseValue = convertResponseValue(response.data[question.id], question);
const surveyResponse: TWeeklySummarySurveyResponseData = {
headline: getLocalizedValue(headline, "default"),
responseValue,
questionType: question.type,
};
surveyResponses.push(surveyResponse);
const answer = response.data[question.id]?.toString() || null;
if (answer === null || answer === "" || answer?.length === 0) {
continue;
}
surveyResponse[getLocalizedValue(headline, "default")] = answer;
}
surveyData.responses = surveyResponses;
surveyData.responses.push(surveyResponse);
}
surveys.push(surveyData);
// calculate the overall insights

View File

@@ -0,0 +1,80 @@
import { TResponseData } from "@formbricks/types/responses";
import { TSurveyQuestion, TSurveyStatus } from "@formbricks/types/surveys";
import { TUserNotificationSettings } from "@formbricks/types/user";
export interface Insights {
totalCompletedResponses: number;
totalDisplays: number;
totalResponses: number;
completionRate: number;
numLiveSurvey: number;
}
export interface SurveyResponse {
[headline: string]: string | number | boolean | Date | string[];
}
export interface Survey {
id: string;
name: string;
responses: SurveyResponse[];
responseCount: number;
status: string;
}
export interface NotificationResponse {
environmentId: string;
currentDate: Date;
lastWeekDate: Date;
productName: string;
surveys: Survey[];
insights: Insights;
}
// Prisma Types
type ResponseData = {
id: string;
createdAt: Date;
updatedAt: Date;
finished: boolean;
data: TResponseData;
};
type DisplayData = {
id: string;
};
type SurveyData = {
id: string;
name: string;
questions: TSurveyQuestion[];
status: TSurveyStatus;
responses: ResponseData[];
displays: DisplayData[];
};
export type EnvironmentData = {
id: string;
surveys: SurveyData[];
};
type UserData = {
email: string;
notificationSettings: TUserNotificationSettings;
};
type MembershipData = {
user: UserData;
};
type TeamData = {
memberships: MembershipData[];
};
export type ProductData = {
id: string;
name: string;
environments: EnvironmentData[];
team: TeamData;
};

View File

@@ -3,8 +3,8 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { headers } from "next/headers";
import { prisma } from "@formbricks/database";
import { sendResponseFinishedEmail } from "@formbricks/email";
import { INTERNAL_SECRET } from "@formbricks/lib/constants";
import { sendResponseFinishedEmail } from "@formbricks/lib/emails/emails";
import { getIntegrations } from "@formbricks/lib/integration/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";

View File

@@ -2,14 +2,14 @@ import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { updateAttributes } from "@formbricks/lib/attribute/service";
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
import { personCache } from "@formbricks/lib/person/cache";
import { getPerson } from "@formbricks/lib/person/service";
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ZJsPeopleAttributeInput } from "@formbricks/types/js";
import { TJsAppStateSync, ZJsPeopleAttributeInput } from "@formbricks/types/js";
interface Context {
params: {
@@ -47,7 +47,19 @@ export async function POST(req: Request, context: Context): Promise<Response> {
return responses.notFoundResponse("Person", personId, true);
}
await updateAttributes(personId, { [key]: value });
let attributeClass = await getAttributeClassByName(environmentId, key);
// create new attribute class if not found
if (attributeClass === null) {
attributeClass = await createAttributeClass(environmentId, key, "code");
}
if (!attributeClass) {
return responses.internalServerErrorResponse("Unable to create attribute class", true);
}
// upsert attribute (update or create)
await updatePersonAttribute(personId, attributeClass.id, value);
personCache.revalidate({
id: personId,
@@ -75,7 +87,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
}
// return state
const state = {
const state: TJsAppStateSync = {
person: { id: person.id, userId: person.userId },
surveys,
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),

View File

@@ -2,14 +2,14 @@ import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { updateAttributes } from "@formbricks/lib/attribute/service";
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
import { personCache } from "@formbricks/lib/person/cache";
import { getPerson } from "@formbricks/lib/person/service";
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { getSyncSurveys } from "@formbricks/lib/survey/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ZJsPeopleAttributeInput } from "@formbricks/types/js";
import { TJsAppStateSync, ZJsPeopleAttributeInput } from "@formbricks/types/js";
interface Context {
params: {
@@ -46,7 +46,19 @@ export async function POST(req: Request, context: Context): Promise<Response> {
return responses.notFoundResponse("Person", personId, true);
}
await updateAttributes(personId, { [key]: value });
let attributeClass = await getAttributeClassByName(environmentId, key);
// create new attribute class if not found
if (attributeClass === null) {
attributeClass = await createAttributeClass(environmentId, key, "code");
}
if (!attributeClass) {
return responses.internalServerErrorResponse("Unable to create attribute class", true);
}
// upsert attribute (update or create)
await updatePersonAttribute(personId, attributeClass.id, value);
personCache.revalidate({
id: personId,
@@ -74,7 +86,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
}
// return state
const state = {
const state: TJsAppStateSync = {
person: { id: person.id, userId: person.userId },
surveys,
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),

View File

@@ -2,11 +2,12 @@ import { getUpdatedState } from "@/app/api/v1/(legacy)/js/sync/lib/sync";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { updateAttributes } from "@formbricks/lib/attribute/service";
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
import { personCache } from "@formbricks/lib/person/cache";
import { getPerson } from "@formbricks/lib/person/service";
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { ZJsPeopleLegacyAttributeInput } from "@formbricks/types/js";
import { TPersonClient } from "@formbricks/types/people";
export async function OPTIONS(): Promise<Response> {
return responses.successResponse({}, true);
@@ -41,7 +42,19 @@ export async function POST(req: Request, { params }): Promise<Response> {
return responses.notFoundResponse("Person", personId, true);
}
await updateAttributes(personId, { [key]: value });
let attributeClass = await getAttributeClassByName(environmentId, key);
// create new attribute class if not found
if (attributeClass === null) {
attributeClass = await createAttributeClass(environmentId, key, "code");
}
if (!attributeClass) {
return responses.internalServerErrorResponse("Unable to create attribute class", true);
}
// upsert attribute (update or create)
await updatePersonAttribute(personId, attributeClass.id, value);
personCache.revalidate({
id: personId,
@@ -54,7 +67,7 @@ export async function POST(req: Request, { params }): Promise<Response> {
const state = await getUpdatedState(environmentId, personId);
let person: { id: string; userId: string } | null = null;
let person: TPersonClient | null = null;
if (state.person && "id" in state.person && "userId" in state.person) {
person = {
id: state.person.id,

View File

@@ -3,6 +3,7 @@ import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { ZJsSyncLegacyInput } from "@formbricks/types/js";
import { TPersonClient } from "@formbricks/types/people";
export async function OPTIONS(): Promise<Response> {
return responses.successResponse({}, true);
@@ -27,7 +28,7 @@ export async function POST(req: Request): Promise<Response> {
const state = await getUpdatedState(environmentId, personId);
let person: { id: string; userId: string } | null = null;
let person: TPersonClient | null = null;
if (state.person && "id" in state.person && "userId" in state.person) {
person = {
id: state.person.id,

View File

@@ -2,8 +2,6 @@ import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { createAction } from "@formbricks/lib/action/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ZActionInput } from "@formbricks/types/actions";
interface Context {
@@ -34,16 +32,6 @@ export async function POST(req: Request, context: Context): Promise<Response> {
);
}
// Formbricks Cloud: Make sure environment is part of a paid plan
if (IS_FORMBRICKS_CLOUD) {
const team = await getTeamByEnvironmentId(context.params.environmentId);
if (!team || team.billing.features.userTargeting.status !== "active") {
// temporary return status code 200 to avoid CORS issues; will be changed to 400 in the future
return responses.successResponse({}, true);
//return responses.badRequestResponse("Storing actions is only possible in a paid plan", {}, true);
}
}
await createAction(inputValidation.data);
return responses.successResponse({}, true);

View File

@@ -5,7 +5,6 @@ import { transformErrorToDetails } from "@/app/lib/api/validator";
import { NextRequest, userAgent } from "next/server";
import { getActionClasses } from "@formbricks/lib/actionClass/service";
import { getAttribute } from "@formbricks/lib/attribute/service";
import {
IS_FORMBRICKS_CLOUD,
PRICING_APPSURVEYS_FREE_RESPONSES,
@@ -150,6 +149,18 @@ export async function GET(
if (!product) {
throw new Error("Product not found");
}
const languageAttribute = person.attributes.language;
const isLanguageAvailable = Boolean(languageAttribute);
const personData = version
? {
...(isLanguageAvailable && { attributes: { language: languageAttribute } }),
}
: {
id: person.id,
userId: person.userId,
...(isLanguageAvailable && { attributes: { language: languageAttribute } }),
};
// Define 'transformedSurveys' which can be an array of either TLegacySurvey or TSurvey.
let transformedSurveys: TLegacySurvey[] | TSurvey[];
@@ -178,14 +189,11 @@ export async function GET(
}),
};
const language = await getAttribute("language", person.id);
// return state
const state: TJsAppStateSync = {
...(version && !isVersionGreaterThanOrEqualTo(version, "2.0.0") && { person }),
person: personData,
surveys: !isInAppSurveyLimitReached ? transformedSurveys : [],
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
language,
product: updatedProduct,
};

View File

@@ -1,79 +0,0 @@
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { NextRequest } from "next/server";
import { getAttributesByUserId, updateAttributes } from "@formbricks/lib/attribute/service";
import { createPerson, getPersonByUserId } from "@formbricks/lib/person/service";
import { ZJsPeopleUpdateAttributeInput } from "@formbricks/types/js";
export async function OPTIONS() {
// cors headers
return responses.successResponse({}, true);
}
export async function PUT(req: NextRequest, context: { params: { environmentId: string; userId: string } }) {
try {
const environmentId = context.params.environmentId;
if (!environmentId) {
return responses.badRequestResponse("environmentId is required", { environmentId }, true);
}
const userId = context.params.userId;
if (!userId) {
return responses.badRequestResponse("userId is required", { userId }, true);
}
const jsonInput = await req.json();
const parsedInput = ZJsPeopleUpdateAttributeInput.safeParse(jsonInput);
if (!parsedInput.success) {
return responses.badRequestResponse(
"Fields are missing or incorrectly formatted",
transformErrorToDetails(parsedInput.error),
true
);
}
const { userId: userIdAttr, ...updatedAttributes } = parsedInput.data.attributes;
let person = await getPersonByUserId(environmentId, userId);
if (!person) {
// return responses.notFoundResponse("PersonByUserId", userId, true);
// HOTFIX: create person if not found to work around caching issue
person = await createPerson(environmentId, userId);
}
const oldAttributes = await getAttributesByUserId(environmentId, userId);
let isUpToDate = true;
for (const key in updatedAttributes) {
if (updatedAttributes[key] !== oldAttributes[key]) {
isUpToDate = false;
break;
}
}
if (isUpToDate) {
return responses.successResponse(
{
changed: false,
message: "No updates were necessary; the person is already up to date.",
},
true
);
}
await updateAttributes(person.id, updatedAttributes);
return responses.successResponse(
{
changed: true,
message: "The person was successfully updated.",
},
true
);
} catch (err) {
return responses.internalServerErrorResponse("Something went wrong", true);
}
}

View File

@@ -1,12 +1,8 @@
// Deprecated since 2024-04-13
// last supported js version 1.6.5
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { z } from "zod";
import { getAttributesByUserId, updateAttributes } from "@formbricks/lib/attribute/service";
import { createPerson, getPersonByUserId } from "@formbricks/lib/person/service";
import { ZAttributes } from "@formbricks/types/attributes";
import { createPerson, getPersonByUserId, updatePerson } from "@formbricks/lib/person/service";
import { ZPersonUpdateInput } from "@formbricks/types/people";
interface Context {
params: {
@@ -25,7 +21,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
const jsonInput = await req.json();
// validate using zod
const inputValidation = z.object({ attributes: ZAttributes }).safeParse(jsonInput);
const inputValidation = ZPersonUpdateInput.safeParse(jsonInput);
if (!inputValidation.success) {
return responses.badRequestResponse(
@@ -46,8 +42,8 @@ export async function POST(req: Request, context: Context): Promise<Response> {
person = await createPerson(environmentId, userId);
}
const oldAttributes = await getAttributesByUserId(environmentId, userId);
// Check if the person is already up to date
const oldAttributes = person.attributes;
let isUpToDate = true;
for (const key in updatedAttributes) {
if (updatedAttributes[key] !== oldAttributes[key]) {
@@ -55,7 +51,6 @@ export async function POST(req: Request, context: Context): Promise<Response> {
break;
}
}
if (isUpToDate) {
return responses.successResponse(
{
@@ -66,7 +61,7 @@ export async function POST(req: Request, context: Context): Promise<Response> {
);
}
await updateAttributes(person.id, updatedAttributes);
await updatePerson(person.id, inputValidation.data);
return responses.successResponse(
{

View File

@@ -1,5 +1,5 @@
import { prisma } from "@formbricks/database";
import { sendForgotPasswordEmail } from "@formbricks/email";
import { sendForgotPasswordEmail } from "@formbricks/lib/emails/emails";
export async function POST(request: Request) {
const { email } = await request.json();

View File

@@ -1,5 +1,5 @@
import { prisma } from "@formbricks/database";
import { sendPasswordResetNotifyEmail } from "@formbricks/email";
import { sendPasswordResetNotifyEmail } from "@formbricks/lib/emails/emails";
import { verifyToken } from "@formbricks/lib/jwt";
export async function POST(request: Request) {

View File

@@ -1,5 +1,4 @@
import { prisma } from "@formbricks/database";
import { sendInviteAcceptedEmail, sendVerificationEmail } from "@formbricks/email";
import {
DEFAULT_TEAM_ID,
DEFAULT_TEAM_ROLE,
@@ -8,6 +7,7 @@ import {
INVITE_DISABLED,
SIGNUP_ENABLED,
} from "@formbricks/lib/constants";
import { sendInviteAcceptedEmail, sendVerificationEmail } from "@formbricks/lib/emails/emails";
import { deleteInvite } from "@formbricks/lib/invite/service";
import { verifyInviteToken } from "@formbricks/lib/jwt";
import { createMembership } from "@formbricks/lib/membership/service";
@@ -20,13 +20,17 @@ export async function POST(request: Request) {
if (!EMAIL_AUTH_ENABLED || inviteToken ? INVITE_DISABLED : !SIGNUP_ENABLED) {
return Response.json({ error: "Signup disabled" }, { status: 403 });
}
user = { ...user, ...{ email: user.email.toLowerCase() } };
let inviteId;
try {
let invite;
let isInviteValid = false;
// create the user
user = await createUser(user);
// User is invited to team
if (inviteToken) {
let inviteTokenData = await verifyInviteToken(inviteToken);
inviteId = inviteTokenData?.inviteId;
@@ -41,20 +45,7 @@ export async function POST(request: Request) {
if (!invite) {
return Response.json({ error: "Invalid invite ID" }, { status: 400 });
}
isInviteValid = true;
}
user = {
...user,
...{ email: user.email.toLowerCase() },
onboardingCompleted: isInviteValid,
};
// create the user
user = await createUser(user);
// User is invited to team
if (isInviteValid) {
// assign user to existing team
await createMembership(invite.teamId, user.id, {
accepted: true,

View File

@@ -1,5 +1,5 @@
import { prisma } from "@formbricks/database";
import { sendVerificationEmail } from "@formbricks/email";
import { sendVerificationEmail } from "@formbricks/lib/emails/emails";
export async function POST(request: Request) {
const { email } = await request.json();

View File

@@ -56,6 +56,12 @@
}
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: #e2e8f0;
}
/* Chrome, Edge, and Safari */
*::-webkit-scrollbar {
width: 10px;

View File

@@ -2,7 +2,7 @@
import { TSurveyPinValidationResponseError } from "@/app/s/[surveyId]/types";
import { LinkSurveyEmailData, sendLinkSurveyToVerifiedEmail } from "@formbricks/email";
import { LinkSurveyEmailData, sendLinkSurveyToVerifiedEmail } from "@formbricks/lib/emails/emails";
import { verifyTokenForLinkSurvey } from "@formbricks/lib/jwt";
import { getSurvey } from "@formbricks/lib/survey/service";
import { TSurvey } from "@formbricks/types/surveys";

View File

@@ -24,8 +24,8 @@ export default function LegalFooter({
};
return (
<div className="absolute bottom-0 h-10 w-full">
<div className="mx-auto max-w-lg p-2 text-center text-xs text-slate-400 text-opacity-50">
<div className="sticky top-[100vh] h-12 w-full">
<div className="mx-auto max-w-lg p-3 text-center text-xs text-slate-400">
{IMPRINT_URL && (
<Link href={IMPRINT_URL} target="_blank" className="hover:underline">
Imprint

View File

@@ -117,10 +117,6 @@ export default function LinkSurvey({
if (window.self === window.top) {
setAutofocus(true);
}
// For safari on mobile devices, scroll is a bit off due to dynamic height of address bar, so on inital load, we scroll to the bottom
window.scrollTo({
top: document.body.scrollHeight,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -185,9 +181,9 @@ export default function LinkSurvey({
};
return (
<div className="flex h-screen items-end justify-center md:items-center">
<div className="flex h-screen items-center justify-center">
{!determineStyling().isLogoHidden && product.logo?.url && <ClientLogo product={product} />}
<ContentWrapper className="w-full p-0 md:max-w-md">
<ContentWrapper className="w-11/12 p-0 md:max-w-md">
{isPreview && (
<div className="fixed left-0 top-0 flex w-full items-center justify-between bg-slate-600 p-2 px-4 text-center text-sm text-white shadow-sm">
<div />

View File

@@ -1,6 +1,5 @@
"use client";
import Link from "next/link";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { TProduct } from "@formbricks/types/product";
@@ -25,7 +24,6 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
}) => {
const animatedBackgroundRef = useRef<HTMLVideoElement>(null);
const [backgroundLoaded, setBackgroundLoaded] = useState(false);
const [authorDetailsForUnsplash, setAuthorDetailsForUnsplash] = useState({ authorName: "", authorURL: "" });
// get the background from either the survey or the product styling
const background = useMemo(() => {
@@ -57,18 +55,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
// Cleanup
return () => video.removeEventListener("canplaythrough", onCanPlayThrough);
} else if ((background?.bgType === "image" || background?.bgType === "upload") && background?.bg) {
if (background?.bgType === "image") {
// To not set for Default Images as they have relative URL & are not from Unsplash
if (!background?.bg.startsWith("/")) {
setAuthorDetailsForUnsplash({
authorName: new URL(background?.bg!).searchParams.get("authorName") || "",
authorURL: new URL(background?.bg!).searchParams.get("authorLink") || "",
});
} else {
setAuthorDetailsForUnsplash({ authorName: "", authorURL: "" });
}
}
} else if (background?.bgType === "image" && background?.bg) {
// For images, we create a new Image object to listen for the 'load' event
const img = new Image();
img.onload = () => setBackgroundLoaded(true);
@@ -111,40 +98,10 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
</video>
);
case "image":
return (
<>
<div
className={`${baseClasses} ${loadedClass} bg-cover bg-center`}
style={{ backgroundImage: `url(${background?.bg})`, filter: `${filterStyle}` }}></div>
<div className={`absolute bottom-6 z-10 h-12 w-full lg:bottom-0`}>
<div className="mx-auto max-w-full p-3 text-center text-xs text-slate-400 lg:text-right">
{authorDetailsForUnsplash.authorName && (
<div className="ml-auto w-max">
<span>Photo by </span>
<Link
href={authorDetailsForUnsplash.authorURL + "?utm_source=formbricks&utm_medium=referral"}
target="_blank"
className="hover:underline">
{authorDetailsForUnsplash.authorName}
</Link>
<span> on </span>
<Link
href="https://unsplash.com/?utm_source=formbricks&utm_medium=referral"
target="_blank"
className="hover:underline">
Unsplash
</Link>
</div>
)}
</div>
</div>
</>
);
case "upload":
return (
<div
className={`${baseClasses} ${loadedClass} bg-cover bg-center`}
style={{ backgroundImage: `url(${survey.styling?.background?.bg})`, filter: `${filterStyle}` }}
style={{ backgroundImage: `url(${background?.bg})`, filter: `${filterStyle}` }}
/>
);
default:
@@ -153,7 +110,9 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
};
const renderContent = () => (
<div className="no-scrollbar absolute flex h-full w-full items-center justify-center">{children}</div>
<div className="no-scrollbar absolute flex h-full w-full items-center justify-center overflow-y-auto">
{children}
</div>
);
if (isMobilePreview) {
@@ -169,7 +128,7 @@ export const MediaBackground: React.FC<MediaBackgroundProps> = ({
);
} else if (isEditorView) {
return (
<div ref={ContentRef} className="flex flex-grow flex-col rounded-b-lg">
<div ref={ContentRef} className="flex flex-grow flex-col overflow-y-auto rounded-b-lg">
<div className="relative flex w-full flex-grow flex-col items-center justify-center p-4 py-6">
{renderBackground()}
<div className="flex h-full w-full items-center justify-center">{children}</div>

View File

@@ -53,10 +53,6 @@ const nextConfig = {
protocol: "https",
hostname: "formbricks-cdn.s3.eu-central-1.amazonaws.com",
},
{
protocol: "https",
hostname: "images.unsplash.com",
},
],
},
async rewrites() {

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