Compare commits

...

22 Commits

Author SHA1 Message Date
pandeymangg
4327319d75 fix: placeholder 2024-07-03 12:33:05 +05:30
pandeymangg
dc4f146983 fixed the cal.com host not configured 2024-07-03 12:18:24 +05:30
pandeymangg
0ed6401df7 Merge remote-tracking branch 'origin/main' into aschaber-cal 2024-07-03 11:11:19 +05:30
Laurens Nienhaus
d7c211d98e docs: Fix documentation link (#2839) 2024-07-02 11:54:46 +00:00
Anshuman Pandey
c32a358f43 fix: cache headers (#2834) 2024-07-02 06:46:26 +00:00
Anshuman Pandey
1f4b23b105 fix: surveys package tailwind (#2827)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2024-07-02 06:44:15 +00:00
Anshuman Pandey
58df9c6edb fix: surveys management api input types (#2837) 2024-07-01 12:04:22 +00:00
Matti Nannt
ce4578a829 fix: identify Posthog users in product onboarding (#2836) 2024-07-01 12:51:15 +02:00
Dhruwang Jariwala
1885b3ac2e fix: onboarding survey alignment (#2833) 2024-07-01 08:27:43 +00:00
Johannes
205ddc88cb fix: remove deal cloud (#2829) 2024-07-01 08:24:07 +00:00
Johannes
9da065e1ec docs: tweaking contributor docs (#2818) 2024-06-27 15:14:25 +00:00
Piyush Gupta
a40846f6ed chore: improve NPSQuestion component styling (#2825) 2024-06-27 15:10:54 +00:00
Dhruwang Jariwala
c3ff6fadc9 fix: spacing in delete dialog (#2823) 2024-06-27 08:53:47 +00:00
Matthias Nannt
601bd5d6e7 docs: fix wrong language code in migration guide 2024-06-26 18:29:19 +02:00
Matthias Nannt
8731f2afe5 docs: fix typo in 2.3 migration guide 2024-06-26 18:13:58 +02:00
Matthias Nannt
323df36a97 docs: fix typo in 2.3 migration guide 2024-06-26 18:13:03 +02:00
Matti Nannt
bcf71b583c chore: prepare 2.3 release (#2819) 2024-06-26 18:11:30 +02:00
Anshuman Pandey
9268407429 fix: ee license info banner (#2790)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-06-26 16:00:50 +00:00
Matti Nannt
1ff87d27ca chore: move redis cache over to redis-strings from redis-stack (#2817) 2024-06-26 13:38:17 +02:00
Piyush Gupta
d6e4b7700f fix: UX improvement (#2791)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Dhruwang <dhruwangjariwala18@gmail.com>
2024-06-26 10:29:09 +00:00
Matti Nannt
e856006e04 Merge branch 'main' into feature/custom-calcom-host 2024-06-25 15:34:10 +02:00
Alexander Schaber
01210dba3f feat(calcom): add custom cal.com field
Closes: #2654
2024-06-23 16:50:14 +02:00
99 changed files with 15849 additions and 12586 deletions

View File

@@ -13,7 +13,7 @@
"dependencies": {
"@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*",
"lucide-react": "^0.395.0",
"lucide-react": "^0.397.0",
"next": "14.2.4",
"react": "18.3.1",
"react-dom": "18.3.1"

View File

@@ -0,0 +1,66 @@
import { MdxImage } from "@/components/MdxImage";
import GithubCodespaceLoading from "./images/loading.webp";
import GithubCodespaceNew from "./images/new.webp";
import GithubCodespacePorts from "./images/ports.webp";
export const metadata = {
title: "Formbricks Open Source Contribution Guide: How to Enhance yourself and Contribute to Formbricks",
description:
"Join the Formbricks community and learn how to effectively contribute. From raising issues and feature requests to creating PRs, discover the best practices and communicate with our responsive team on Discord",
};
#### Contributing
# Github Codespaces Guide
1. After clicking the one-click setup button, you will be redirected to the Github Codespaces page. Review the configuration and click on the 'Create Codespace' button to create a new Codespace.
<MdxImage
src={GithubCodespaceNew}
alt="New Github Codespace"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2. This will start loading the Codespace. Keep in mind this might take a few minutes to complete depending on your internet connection and the instance availability.
<MdxImage
src={GithubCodespaceLoading}
alt="Loading Github Codespace"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
3. Once the Codespace is loaded, you will be redirected to the VSCode editor. You can start working on your project in this environment.
4. Monitor the logs in the terminal and once you see the following, you are good to go!
<Col>
<CodeGroup title="The WebApp is running">
```bash
@formbricks/web:dev: ▲ Next.js 13.5.6
@formbricks/web:dev: - Local: http://localhost:3000
@formbricks/web:dev: - Environments: .env
@formbricks/web:dev: - Experiments (use at your own risk):
@formbricks/web:dev: · serverActions
@formbricks/web:dev:
@formbricks/web:dev: ✓ Ready in 9.4s
```
</CodeGroup>
</Col>
5. Right next to the Terminal, you will see a **Ports** tab, click on it to see the ports and their respective URLs. Now access the Forwarded Address for port 3000 and you should be able to visit your Formbricks App!
<MdxImage
src={GithubCodespacePorts}
alt="Github Codespace Ports"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Now make the changes you want to and see them live in action!

View File

@@ -0,0 +1,152 @@
import { MdxImage } from "@/components/MdxImage";
export const metadata = {
title: "Formbricks Open Source Contribution Guide: How to Enhance yourself and Contribute to Formbricks",
description:
"Join the Formbricks community and learn how to effectively contribute. From raising issues and feature requests to creating PRs, discover the best practices and communicate with our responsive team on Discord",
};
#### Contributing
# Get started
We are so happy that you are interested in contributing to Formbricks 🤗 There are many ways to contribute to Formbricks like writing issues, fixing bugs, building new features or updating the docs.
- **Issues**: Spotted a bug? Has deployment gone wrong? Do you have user feedback? [Raise an issue](https://github.com/formbricks/formbricks/issues/new/choose) for the fastest response.
- **Feature requests**: Raise an issue for these and tag it as an Enhancement. We love every idea. Please [open a feature request](https://github.com/formbricks/formbricks/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.yml&title=%5BFEATURE%5D) clearly describing the problem you want to solve.
- **How we Code at Formbricks**: [View this Notion document](https://formbricks.notion.site/How-we-code-at-Formbricks-8bcaa0304a20445db4871831149c0cf5?pvs=4) and understand the coding practises we follow so that you can adhere to them for uniformity.
- **Creating a PR**: Please fork the repository, make your changes and create a new pull request if you want to make an update.
- **E2E Tests**: [Understand how we write E2E tests](https://formbricks.notion.site/Formbricks-End-to-End-Tests-06dc830d71604deaa8da24714540f7ab?pvs=4) and make sure to consider writing a test whenever you ship a feature.
- **New Question Types**:[Follow this guide](https://formbricks.notion.site/Guidelines-for-Implementing-a-New-Question-Type-9ac0d1c362714addb24b9abeb326d1c1?pvs=4) to keep everything in mind when you want to add a new question type.
- **How to create a service**: [Read this document to understand how we use services](https://formbricks.notion.site/How-to-create-a-service-8e0c035704bb40cb9ea5e5beeeeabd67?pvs=4). This is particulalry important when you need to write a new one.
## Talk to us first
We highly recommend connecting with us on [Discord server](https://formbricks.com/discord) before you ship a contribution. This will increase the likelihood of your PR being merged. And it will decrease the likelihood of you wasting your time :)
## Contributor License Agreement (CLA)
To be able to keep working on Formbricks over the coming years, we need to collect a CLA from all relevant contributors.
Once you open a PR, you will get a message from the CLA bot to fill out the form. Please note that we can only get your contribution merged when we have a CLA signed by you.
## Setup Dev Environment
We currently officially support the below methods to set up your development environment for Formbricks:
- [Gitpod](/docs/developer-docs/contributing/gitpod)
- [GitHub Codespaces](/docs/developer-docs/contributing/codespaces)
- [Local Machine Setup](#local-machine-setup)
Both Gitpod and GitHub Codespaces have a **generous free tier** to explore and develop. For junior developers we suggest using either of these, because you can dive into coding within minutes, not hours.
## Local Machine Setup
<Note>
The below only works for **Mac**, **Linux** & **WSL2** on Windows (not on pure Windows)!
This method is recommended **only for advanced users** & we won't be able to provide official support for this.
</Note>
To get the project running locally on your machine you need to have the following development tools installed:
- Node.JS (we recommend v20)
- [pnpm](https://pnpm.io/)
- [Docker](https://www.docker.com/) (to run PostgreSQL / MailHog)
1. Clone the project & move into the directory:
<Col>
<CodeGroup title="Git clone Formbricks monorepo">
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
</CodeGroup>
</Col>
2. Install Node.JS packages via pnpm. Don't have pnpm? Get it [here](https://pnpm.io/installation)
<Col>
<CodeGroup title="Install dependencies via pnpm">
```bash
pnpm install
```
</CodeGroup>
</Col>
3. Create a `.env` file based on `.env.example`. It's already preset to work with the local development setup but you can also change values if needed.
<Col>
<CodeGroup title="Define environment variables">
```bash
cp .env.example .env
```
</CodeGroup>
</Col>
4. Generate & set some secret values mandatory for the `ENCRYPTION_KEY` & `NEXTAUTH_SECRET` in the .env file. You can use the following command to generate the random string of required length:
<Col>
<CodeGroup title="Set value of ENCRYPTION_KEY">
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
```
</CodeGroup>
</Col>
5. Make sure you have [`Docker`](https://docs.docker.com/compose/) & [`docker-compose`](https://docs.docker.com/compose/) installed and running on your machine. Then run the following command to start the Formbricks dev setup:
<Col>
<CodeGroup title="Start Formbricks Dev Setup">
```bash
pnpm go
```
</CodeGroup>
</Col>
This starts the Formbricks main app (plus all its dependencies) as well as the following services using Docker:
- A `postgres` container for hosting your database,
- A `mailhog` container that acts as a mock SMTP server and shows received mails in a web UI (forwarded to your host's `localhost:8025`)
- Demo App at [http://localhost:3002](http://localhost:3002)
- Landing Page at [http://localhost:3001](http://localhost:3001)
<Note>
**WSL2 users**: If you encounter connection issues with Prisma, ensure your WSL2 instance's PostgreSQL
service is stopped before running `pnpm go`. Use the command `sudo systemctl stop postgresql` to stop the
service.
</Note>
**You can now access the Formbricks app on [http://localhost:3000](http://localhost:3000)**. You will be automatically redirected to the login. To use your local installation of formbricks, create a new account.
{" "}
<Note>
A fresh setup does not have a default account. Please create a new account and proceed accordingly.
</Note>
For viewing the emails sent by the system, you can access mailhog at [http://localhost:8025](http://localhost:8025)
### Build
To build all apps and packages and check for build errors, run the following command:
<Col>
<CodeGroup title="Build Formbricks stack">
```bash
pnpm build
```
</CodeGroup>
</Col>

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -0,0 +1,172 @@
import { MdxImage } from "@/components/MdxImage";
import GitpodAuth from "./images/auth.webp";
import GitpodNewWorkspace from "./images/new-workspace.webp";
import GitpodPorts from "./images/ports.webp";
import GitpodPreparing from "./images/preparing.webp";
import GitpodRunning from "./images/running.webp";
export const metadata = {
title: "Formbricks Open Source Contribution Guide: How to Enhance yourself and Contribute to Formbricks",
description:
"Join the Formbricks community and learn how to effectively contribute. From raising issues and feature requests to creating PRs, discover the best practices and communicate with our responsive team on Discord",
};
#### Contributing
# Gitpod Guide
**Building custom image for the workspace:**
- This includes : Installing `yq` and `turbo` globally before the workspace starts. This is accomplished within the `.gitpod.Dockerfile` along with starting upon a base custom image building on [workspace-full](https://hub.docker.com/r/workspace-full/dockerfile).
**Initialization of Formbricks:**
- During the prebuilds phase, we initialize Formbricks by performing the following tasks:
1. Setting up environment variables.
2. Installing monorepo dependencies.
3. Installing Docker images by extracting them from the `packages/database/docker-compose.yml` file.
4. Building the @formbricks/js component.
- When the workspace starts:
1. Wait for the web and demo apps to launch on Gitpod. This automatically opens the `apps/demo/.env` file. Utilize dynamic localhost URLs (e.g., `localhost:3000` for signup and `localhost:8025` for email confirmation) to configure `NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID`. After creating your account and finding the `ID` in the URL at `localhost:3000`, replace `YOUR_ENVIRONMENT_ID` in the `.env` file located in `app/demo`.
**Web Component Initialization:**
- We initialize the @formbricks/web component during prebuilds. This involves:
1. Installing build dependencies for the `@formbricks/web#go` task from turbo.json in prebuilds to save time.
2. Starting PostgreSQL and Mailhog containers for running migrations in prebuilds.
3. To prevent the "Init" task from running indefinitely due to prebuild rules, a cleanup `docker compose down` step i.e. `db:down` is added to `turbo.json`. This step is designed to halt the execution of containers that are currently running.
- When the workspace starts:
1. Initializing environment variables.
2. Replacing `NEXT_PUBLIC_WEBAPP_URL` and `NEXTAUTH_URL` to take in Gitpod URL's ports when running on VSCode browser.
3. Starting the `@formbricks/web` dev environment.
**Demo Component Initialization:**
- Similar to the web component, the demo component is also initialized during prebuilds. This includes:
1. Installing build dependencies for the `formbricks/demo#go` task from turbo.json in prebuilds to save time.
2. Caching hits and replaying builds from the `@formbricks/js` component.
- When the workspace starts:
1. Initializing environment variables.
2. Replaces `NEXT_PUBLIC_FORMBRICKS_API_HOST` to take in Gitpod URL's ports when running on VSCode browser.
3. Starting the `@formbricks/demo` dev environment.
**Github Prebuilds Configuration:**
- This configures Github Prebuilds for the master branch, pull requests, and adding comments. This helps automate the prebuild process for the specified branches and actions.
**VSCode Extensions:**
- This includes a list of VSCode extensions that are added to the configuration when using Gitpod. These extensions can enhance the development experience within Gitpod.
### 1. Browser Redirection
After clicking the one-click setup button, Gitpod will open a new tab or window. Please ensure that your browser allows redirection to successfully access the services:
### 2. Authorizing in Gitpod
<MdxImage
src={GitpodAuth}
alt="Gitpod Auth Page"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- This is the Gitpod Authentication Page. It appears when you click the "Open in GitPod" button and Gitpod
needs to authenticate your access to the workspace. Click on 'Continue With Github' to authorize your GitPod
session.
### 3. Creating a New Workspace
<MdxImage
src={GitpodNewWorkspace}
alt="Gitpod New workspace Page"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- After authentication, Gitpod asks to create a new workspace for you. This page displays the configurations
of your workspace. - You can use either choose either VS Code Browser or VS Code Desktop editor with the
'Standard Class' for your workspace class. - If you opt for the VS Code Desktop, follow the following steps 1.
Gitpod will prompt you to grant access to the VSCode app. Once approved, install the GitPod extension from the
VSCode Marketplace and follow the prompts to authorize the integration. 2. Change the `WEBAPP_URL` and the
`NEXTAUTH_URL` to `https://localhost:3000`
### 4. Gitpod preparing the created Workspace
<MdxImage
src={GitpodPreparing}
alt="Gitpod Preparing workspace Page"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- Gitpod is preparing your workspace with all the necessary dependencies and configurations. You will see this
page while Gitpod sets up your development environment.
### 5. Gitpod running the Workspace
<MdxImage
src={GitpodRunning}
alt="Gitpod Running Workspace Page"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- Once the workspace is fully prepared, voila, it enters the running state. You can start working on your
project in this environment.
### Ports and Services
Here are the ports and corresponding URLs for the services within your Gitpod environment:
- **Port 3000**:
- **Service**: Demo App
- **Description**: This port hosts the demo application of your project. You can access and interact with your application's demo by navigating to this port.
- **Port 3001**:
- **Service**: Formbricks website
- **Description**: This port hosts the [Formbricks](https://formbricks.com) website, which contains documents, pricing, blogs, best practices, and concierge service.
- **Port 3002**:
- **Service**: Formbricks In-product Survey Demo App
- **Description**: This app helps you test your app & website surveys. You can create and test user actions, create and update user attributes, etc.
- **Port 5432**:
- **Service**: PostgreSQL Database Server
- **Description**: The PostgreSQL DB is hosted on this port.
- **Port 1025**:
- **Service**: SMTP server
- **Description**: SMTP Server for sending and receiving email messages. This server is responsible for handling email communication.
- **Port 8025**:
- **Service**: Mailhog
### Accessing port URLs
1. **Direct URL Composition**:
- You can access the dedicated port URL by pre-pending the port number to the workspace URL.
- For example, if you want to access port 3000, you can use the URL format: `3000-yourworkspace.ws-eu45.gitpod.io`.
2. **Using [gp CLI](https://www.gitpod.io/docs/references-cli)**:
- Gitpod provides a convenient command, `gp url`, to quickly retrieve the URL for a specific port.
- Simply use the command followed by the desired port number. For example, to get the URL for port 3000, run: `gp url 3000`.
3. **Listing All Open Port URLs**:
- If you prefer to see a list of all open port URLs at once, you can use the `gp ports list` command.
- Running this command will display a list of ports along with their corresponding URLs.
4. **Viewing All Ports in Panel**:
- Gitpod also offers a user-friendly 'Ports' tab in the Gitpod panel.
- Click on the 'Ports' tab to view a list of all open ports and their respective URLs.
{" "}
<MdxImage
src={GitpodPorts}
alt="Gitpod Ports tab"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
These URLs and port numbers represent various services and endpoints within your Gitpod environment. You can access and interact with these services by the Port URL for the respective service.

View File

@@ -1,471 +0,0 @@
import { MdxImage } from "@/components/MdxImage";
import GitpodAuth from "./images/gitpod/auth.webp";
import GitpodNewWorkspace from "./images/gitpod/new-workspace.webp";
import GitpodPorts from "./images/gitpod/ports.webp";
import GitpodPreparing from "./images/gitpod/preparing.webp";
import GitpodRunning from "./images/gitpod/running.webp";
import GithubCodespaceLoading from "./images/github-codespaces/loading.webp";
import GithubCodespaceNew from "./images/github-codespaces/new.webp";
import GithubCodespacePorts from "./images/github-codespaces/ports.webp";
import ClearAppData from "./images/troubleshooting/clear-app-data.webp";
import Logout from "./images/troubleshooting/logout.webp";
import UncaughtPromise from "./images/troubleshooting/uncaught-promise.webp";
export const metadata = {
title: "Formbricks Open Source Contribution Guide: How to Enhance yourself and Contribute to Formbricks",
description:
"Join the Formbricks community and learn how to effectively contribute. From raising issues and feature requests to creating PRs, discover the best practices and communicate with our responsive team on Discord",
};
#### Contributing
# Overview
We are so happy that you are interested in contributing to Formbricks 🤗 There are many ways to contribute to Formbricks with writing Issues, fixing bugs, building new features or updating the docs.
- **Issues**: Spotted a bug? Has deployment gone wrong? Do you have user feedback? [Raise an issue](https://github.com/formbricks/formbricks/issues/new/choose) for the fastest response.
- **Feature requests**: Raise an issue for these and tag it as an Enhancement. We love every idea. Please give us as much context on the why as possible.
- **Creating a PR**: Please fork the repository, make your changes and create a new pull request if you want to make an update.
- **E2E Tests**: Understand how we write E2E tests and make sure to write whenever you ship a feature [here](https://formbricks.notion.site/Formbricks-End-to-End-Tests-06dc830d71604deaa8da24714540f7ab?pvs=4).
- **Introducing a new Question Type?**: Adding a new question type to our surveys? Follow this guide to make sure youre on the right track [here](https://formbricks.notion.site/Guidelines-for-Implementing-a-New-Question-Type-9ac0d1c362714addb24b9abeb326d1c1?pvs=4).
- **How we Code at Formbricks**: View this Notion document and understand the coding practises we follow so that you can adhere to them for uniformity.
- **How to create a service**: Services are our Database abstraction layer where we connect Prisma (our DB ORM) with logical methods that are reusable across the server. View this document to understand when & how to write them.
- **Roadmap**: Our roadmap is open on GitHub tickets and some customer and community tickets on GitHub.
If you want to speak to us before doing lots of work, please join our **[Discord server](https://formbricks.com/discord)** and tell us what you would like to work on - we're very responsive and friendly!
## Contributor License Agreement (CLA)
To be able to keep working on Formbricks over the coming years, we need to collect a CLA from all relevant contributors.
Once you open a PR, you will get a message from the CLA bot to fill out the form. Please note that we can only get your contribution merged when we have a CLA signed by you.
## Setup Dev Environment
We currently officially support the below methods to set up your development environment for Formbricks.
<Note>
Both the below cloud IDEs have a **generous free tier** to explore and develop! But make sure to not overuse
the machines as Formbricks will not be responsible for any charges incurred.
</Note>
### GitPod
This will open a fully configured workspace in your browser with all the necessary dependencies already installed. Click the button below to open this project in Gitpod. For a detailed guide, visit the **Gitpod Setup Guide** section below.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/formbricks/formbricks)
### Github Codespaces
This will open a Github VSCode Interface on the cloud for you. This setup will have the Formbricks codebase, all the dependencies installed & Formbricks running. Click the button below to configure your instance and open the project in Github Codespaces. For a detailed guide, visit the **Github Codespaces Setup Guide** section below.
[![Open in Github Codespaces](https://img.shields.io/badge/Open%20in-Github%20Codespaces-blue?logo=Github)](https://Github.com/codespaces/new?machine=standardLinux32gb&repo=500289888&ref=main&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=EastUs2)
### Local Machine
This will install the Formbricks codebase and all the dependencies on your local machine. Note that this method is recommended **only for advanced users**. If you're an advanced user, access the steps for **Local Machine Setup here** below.
<Note>
For a smooth experience, we suggest the above cloud IDE methods. Assistance with setup issues on your local
machine may be limited due to varying factors like OS and permissions.
</Note>
## Gitpod Guide
**Building custom image for the workspace:**
- This includes : Installing `yq` and `turbo` globally before the workspace starts. This is accomplished within the `.gitpod.Dockerfile` along with starting upon a base custom image building on [workspace-full](https://hub.docker.com/r/gitpod/workspace-full/dockerfile).
**Initialization of Formbricks:**
- During the prebuilds phase, we initialize Formbricks by performing the following tasks:
1. Setting up environment variables.
2. Installing monorepo dependencies.
3. Installing Docker images by extracting them from the `packages/database/docker-compose.yml` file.
4. Building the @formbricks/js component.
- When the workspace starts:
1. Wait for the web and demo apps to launch on Gitpod. This automatically opens the `apps/demo/.env` file. Utilize dynamic localhost URLs (e.g., `localhost:3000` for signup and `localhost:8025` for email confirmation) to configure `NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID`. After creating your account and finding the `ID` in the URL at `localhost:3000`, replace `YOUR_ENVIRONMENT_ID` in the `.env` file located in `app/demo`.
**Web Component Initialization:**
- We initialize the @formbricks/web component during prebuilds. This involves:
1. Installing build dependencies for the `@formbricks/web#go` task from turbo.json in prebuilds to save time.
2. Starting PostgreSQL and Mailhog containers for running migrations in prebuilds.
3. To prevent the "Init" task from running indefinitely due to prebuild rules, a cleanup `docker compose down` step i.e. `db:down` is added to `turbo.json`. This step is designed to halt the execution of containers that are currently running.
- When the workspace starts:
1. Initializing environment variables.
2. Replacing `NEXT_PUBLIC_WEBAPP_URL` and `NEXTAUTH_URL` to take in Gitpod URL's ports when running on VSCode browser.
3. Starting the `@formbricks/web` dev environment.
**Demo Component Initialization:**
- Similar to the web component, the demo component is also initialized during prebuilds. This includes:
1. Installing build dependencies for the `formbricks/demo#go` task from turbo.json in prebuilds to save time.
2. Caching hits and replaying builds from the `@formbricks/js` component.
- When the workspace starts:
1. Initializing environment variables.
2. Replaces `NEXT_PUBLIC_FORMBRICKS_API_HOST` to take in Gitpod URL's ports when running on VSCode browser.
3. Starting the `@formbricks/demo` dev environment.
**Github Prebuilds Configuration:**
- This configures Github Prebuilds for the master branch, pull requests, and adding comments. This helps automate the prebuild process for the specified branches and actions.
**VSCode Extensions:**
- This includes a list of VSCode extensions that are added to the configuration when using Gitpod. These extensions can enhance the development experience within Gitpod.
### 1. Browser Redirection
After clicking the one-click setup button, Gitpod will open a new tab or window. Please ensure that your browser allows redirection to successfully access the services:
### 2. Authorizing in Gitpod
<MdxImage
src={GitpodAuth}
alt="Gitpod Auth Page"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- This is the Gitpod Authentication Page. It appears when you click the "Open in GitPod" button and Gitpod
needs to authenticate your access to the workspace. Click on 'Continue With Github' to authorize your GitPod
session.
### 3. Creating a New Workspace
<MdxImage
src={GitpodNewWorkspace}
alt="Gitpod New workspace Page"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- After authentication, Gitpod asks to create a new workspace for you. This page displays the configurations
of your workspace. - You can use either choose either VS Code Browser or VS Code Desktop editor with the
'Standard Class' for your workspace class. - If you opt for the VS Code Desktop, follow the following steps 1.
Gitpod will prompt you to grant access to the VSCode app. Once approved, install the GitPod extension from the
VSCode Marketplace and follow the prompts to authorize the integration. 2. Change the `WEBAPP_URL` and the
`NEXTAUTH_URL` to `https://localhost:3000`
### 4. Gitpod preparing the created Workspace
<MdxImage
src={GitpodPreparing}
alt="Gitpod Preparing workspace Page"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- Gitpod is preparing your workspace with all the necessary dependencies and configurations. You will see this
page while Gitpod sets up your development environment.
### 5. Gitpod running the Workspace
<MdxImage
src={GitpodRunning}
alt="Gitpod Running Workspace Page"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- Once the workspace is fully prepared, voila, it enters the running state. You can start working on your
project in this environment.
### Ports and Services
Here are the ports and corresponding URLs for the services within your Gitpod environment:
- **Port 3000**:
- **Service**: Demo App
- **Description**: This port hosts the demo application of your project. You can access and interact with your application's demo by navigating to this port.
- **Port 3001**:
- **Service**: Formbricks website
- **Description**: This port hosts the [Formbricks](https://formbricks.com) website, which contains documents, pricing, blogs, best practices, and concierge service.
- **Port 3002**:
- **Service**: Formbricks In-product Survey Demo App
- **Description**: This app helps you test your app & website surveys. You can create and test user actions, create and update user attributes, etc.
- **Port 5432**:
- **Service**: PostgreSQL Database Server
- **Description**: The PostgreSQL DB is hosted on this port.
- **Port 1025**:
- **Service**: SMTP server
- **Description**: SMTP Server for sending and receiving email messages. This server is responsible for handling email communication.
- **Port 8025**:
- **Service**: Mailhog
### Accessing port URLs
1. **Direct URL Composition**:
- You can access the dedicated port URL by pre-pending the port number to the workspace URL.
- For example, if you want to access port 3000, you can use the URL format: `3000-yourworkspace.ws-eu45.gitpod.io`.
2. **Using [gp CLI](https://www.gitpod.io/docs/references/gitpod-cli)**:
- Gitpod provides a convenient command, `gp url`, to quickly retrieve the URL for a specific port.
- Simply use the command followed by the desired port number. For example, to get the URL for port 3000, run: `gp url 3000`.
3. **Listing All Open Port URLs**:
- If you prefer to see a list of all open port URLs at once, you can use the `gp ports list` command.
- Running this command will display a list of ports along with their corresponding URLs.
4. **Viewing All Ports in Panel**:
- Gitpod also offers a user-friendly 'Ports' tab in the Gitpod panel.
- Click on the 'Ports' tab to view a list of all open ports and their respective URLs.
{" "}
<MdxImage
src={GitpodPorts}
alt="Gitpod Ports tab"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
These URLs and port numbers represent various services and endpoints within your Gitpod environment. You can access and interact with these services by the Port URL for the respective service.
---
## Github Codespaces Guide
1. After clicking the one-click setup button, you will be redirected to the Github Codespaces page. Review the configuration and click on the 'Create Codespace' button to create a new Codespace.
<MdxImage
src={GithubCodespaceNew}
alt="New Github Codespace"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2. This will start loading the Codespace. Keep in mind this might take a few minutes to complete depending on your internet connection and the instance availability.
<MdxImage
src={GithubCodespaceLoading}
alt="Loading Github Codespace"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
3. Once the Codespace is loaded, you will be redirected to the VSCode editor. You can start working on your project in this environment.
4. Monitor the logs in the terminal and once you see the following, you are good to go!
<Col>
<CodeGroup title="The WebApp is running">
```bash
@formbricks/web:dev: ▲ Next.js 13.5.6
@formbricks/web:dev: - Local: http://localhost:3000
@formbricks/web:dev: - Environments: .env
@formbricks/web:dev: - Experiments (use at your own risk):
@formbricks/web:dev: · serverActions
@formbricks/web:dev:
@formbricks/web:dev: ✓ Ready in 9.4s
```
</CodeGroup>
</Col>
5. Right next to the Terminal, you will see a **Ports** tab, click on it to see the ports and their respective URLs. Now access the Forwarded Address for port 3000 and you should be able to visit your Formbricks App!
<MdxImage
src={GithubCodespacePorts}
alt="Github Codespace Ports"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
Now make the changes you want to and see them live in action!
---
## Local Machine Setup
<Note>
The below only works for **Mac**, **Linux** & **WSL2** on Windows (not on pure Windows)!
This method is recommended **only for advanced users** & we won't be able to provide official support for this.
</Note>
To get the project running locally on your machine you need to have the following development tools installed:
- Node.JS (we recommend v20)
- [pnpm](https://pnpm.io/)
- [Docker](https://www.docker.com/) (to run PostgreSQL / MailHog)
1. Clone the project & move into the directory:
<Col>
<CodeGroup title="Git clone Formbricks monorepo">
```bash
git clone https://github.com/formbricks/formbricks && cd formbricks
```
</CodeGroup>
</Col>
2. Install Node.JS packages via pnpm. Don't have pnpm? Get it [here](https://pnpm.io/installation)
<Col>
<CodeGroup title="Install dependencies via pnpm">
```bash
pnpm install
```
</CodeGroup>
</Col>
3. Create a `.env` file based on `.env.example`. It's already preset to work with the local development setup but you can also change values if needed.
<Col>
<CodeGroup title="Define environment variables">
```bash
cp .env.example .env
```
</CodeGroup>
</Col>
4. Generate & set some secret values mandatory for the `ENCRYPTION_KEY` & `NEXTAUTH_SECRET` in the .env file. You can use the following command to generate the random string of required length:
<Col>
<CodeGroup title="Set value of ENCRYPTION_KEY">
```bash
sed -i '/^ENCRYPTION_KEY=/c\ENCRYPTION_KEY='$(openssl rand -hex 32) .env
sed -i '/^NEXTAUTH_SECRET=/c\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env
```
</CodeGroup>
</Col>
5. Make sure you have [`Docker`](https://docs.docker.com/compose/) & [`docker-compose`](https://docs.docker.com/compose/) installed and running on your machine. Then run the following command to start the Formbricks dev setup:
<Col>
<CodeGroup title="Start Formbricks Dev Setup">
```bash
pnpm go
```
</CodeGroup>
</Col>
This starts the Formbricks main app (plus all its dependencies) as well as the following services using Docker:
- A `postgres` container for hosting your database,
- A `mailhog` container that acts as a mock SMTP server and shows received mails in a web UI (forwarded to your host's `localhost:8025`)
- Demo App at [http://localhost:3002](http://localhost:3002)
- Landing Page at [http://localhost:3001](http://localhost:3001)
<Note>
**WSL2 users**: If you encounter connection issues with Prisma, ensure your WSL2 instance's PostgreSQL
service is stopped before running `pnpm go`. Use the command `sudo systemctl stop postgresql` to stop the
service.
</Note>
**You can now access the Formbricks app on [http://localhost:3000](http://localhost:3000)**. You will be automatically redirected to the login. To use your local installation of formbricks, create a new account.
{" "}
<Note>
A fresh setup does not have a default account. Please create a new account and proceed accordingly.
</Note>
For viewing the emails sent by the system, you can access mailhog at [http://localhost:8025](http://localhost:8025)
### Build
To build all apps and packages and check for build errors, run the following command:
<Col>
<CodeGroup title="Build Formbricks stack">
```bash
pnpm build
```
</CodeGroup>
</Col>
---
# Troubleshooting
Here you'll find help with frequently recurring problems
## "The app doesn't work after doing a prisma migration"
This can happen but fear not, the fix is easy: Delete the application storage of your browser and reload the page. This will force the app to re-fetch the data from the server:
<MdxImage
src={ClearAppData}
alt="Demo App Preview"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## "I ran 'pnpm i' but there seems to be an error with the packages"
If nothing helps, run `pnpm clean` and then `pnpm i` again. This solves a lot.
## "I get a full-screen error with cryptic strings"
This usually happens when the Formbricks Widget wasn't correctly or completely built.
<Col>
<CodeGroup title="Build js library first and then run again">
```bash
pnpm build --filter=@formbricks/js
// Run the app again
pnpm dev
```
</CodeGroup>
</Col>
## My machine struggles with the repository
Since we're working with a monorepo structure, the repository can get quite big. If you're having trouble working with the repository, try the following:
<Col>
<CodeGroup title="Only run the required project">
```bash {{ title: 'Formbricks Web-App' }}
pnpm dev --filter=@formbricks/web...
```
```bash {{ title: 'Formbricks Docs' }}
pnpm dev --filter=@formbricks/docs...
```
```bash {{ title: 'Formbricks Demo App' }}
pnpm dev --filter=@formbricks/demo...
```
</CodeGroup>
</Col>
However, in our experience it's better to run `pnpm dev` than having two terminals open (one with the Formbricks app and one with the demo).
## Uncaught (in promise) SyntaxError: Unexpected token !DOCTYPE ... is not valid JSON
<MdxImage
src={UncaughtPromise}
alt="Uncaught promise"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
This happens when you're using the Demo App and delete the Person within the Formbricks app which the widget is currently connected with. We're fixing it, but you can also just logout your test person and reload the page to get rid of it.
<MdxImage src={Logout} alt="Logout Person" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />

View File

@@ -0,0 +1,84 @@
import { MdxImage } from "@/components/MdxImage";
import ClearAppData from "./images/clear-app-data.webp";
import Logout from "./images/logout.webp";
import UncaughtPromise from "./images/uncaught-promise.webp";
export const metadata = {
title: "Formbricks Open Source Contribution Guide: How to Enhance yourself and Contribute to Formbricks",
description:
"Join the Formbricks community and learn how to effectively contribute. From raising issues and feature requests to creating PRs, discover the best practices and communicate with our responsive team on Discord",
};
#### Contributing
# Troubleshooting
Here you'll find help with frequently recurring problems
## "The app doesn't work after doing a prisma migration"
This can happen but fear not, the fix is easy: Delete the application storage of your browser and reload the page. This will force the app to re-fetch the data from the server:
<MdxImage
src={ClearAppData}
alt="Demo App Preview"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
## "I ran 'pnpm i' but there seems to be an error with the packages"
If nothing helps, run `pnpm clean` and then `pnpm i` again. This solves a lot.
## "I get a full-screen error with cryptic strings"
This usually happens when the Formbricks Widget wasn't correctly or completely built.
<Col>
<CodeGroup title="Build js library first and then run again">
```bash
pnpm build --filter=@formbricks/js
// Run the app again
pnpm dev
```
</CodeGroup>
</Col>
## My machine struggles with the repository
Since we're working with a monorepo structure, the repository can get quite big. If you're having trouble working with the repository, try the following:
<Col>
<CodeGroup title="Only run the required project">
```bash {{ title: 'Formbricks Web-App' }}
pnpm dev --filter=@formbricks/web...
```
```bash {{ title: 'Formbricks Docs' }}
pnpm dev --filter=@formbricks/docs...
```
```bash {{ title: 'Formbricks Demo App' }}
pnpm dev --filter=@formbricks/demo...
```
</CodeGroup>
</Col>
However, in our experience it's better to run `pnpm dev` than having two terminals open (one with the Formbricks app and one with the demo).
## Uncaught (in promise) SyntaxError: Unexpected token !DOCTYPE ... is not valid JSON
<MdxImage
src={UncaughtPromise}
alt="Uncaught promise"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
This happens when you're using the Demo App and delete the Person within the Formbricks app which the widget is currently connected with. We're fixing it, but you can also just logout your test person and reload the page to get rid of it.
<MdxImage src={Logout} alt="Logout Person" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />

View File

@@ -209,7 +209,7 @@ You can close the logs again with `CTRL + C`.
<Note>
## Customizing environment variables
To edit any of the available environment variables, check out our [Configure](/self-hosting/configure) section!
To edit any of the available environment variables, check out our [Configure](/self-hosting/configuration) section!
</Note>

View File

@@ -8,6 +8,106 @@ export const metadata = {
# Migration Guide
## v2.3
Formbricks v2.3 includes new color options for rating questions, improved multi-language functionality for Chinese (Simplified & Traditional), and various bug fixes and performance improvements.
### Steps to Migrate
<Note>
You only need to run the data migration if you have multi language surveys set up in the Chinese language
(`zh`). If you don't have any surveys in Chinese, you can skip the data migration step.
</Note>
This guide is for users who are self-hosting Formbricks using our one-click setup. If you are using a different setup, you might adjust the commands accordingly.
To run all these steps, please navigate to the `formbricks` folder where your `docker-compose.yml` file is located.
1. **Backup your Database**: This is a crucial step. Please make sure to backup your database before proceeding with the upgrade. You can use the following command to backup your database:
<Col>
<CodeGroup title="Backup Postgres">
```bash
docker exec formbricks-postgres-1 pg_dump -Fc -U postgres -d formbricks > formbricks_pre_v2.3_$(date +%Y%m%d_%H%M%S).dump
```
</CodeGroup>
</Col>
<Note>
If you run into “No such container”, use `docker ps` to find your container name, e.g.
`formbricks_postgres_1`.
</Note>
<Note>
If you prefer storing the backup as an `*.sql` file remove the `-Fc` (custom format) option. In case of a
restore scenario you will need to use `psql` then with an empty `formbricks` database.
</Note>
2. Pull the latest version of Formbricks:
<Col>
<CodeGroup title="Stop the containers">
```bash
docker compose pull
```
</CodeGroup>
</Col>
3. Stop the running Formbricks instance & remove the related containers:
<Col>
<CodeGroup title="Stop the containers">
```bash
docker compose down
```
</CodeGroup>
</Col>
4. Restarting the containers with the latest version of Formbricks:
<Col>
<CodeGroup title="Restart the containers">
```bash
docker compose up -d
```
</CodeGroup>
</Col>
5. Now let's migrate the data to the latest schema:
<Note>To find your Docker Network name for your Postgres Database, find it using `docker network ls`</Note>
<Col>
<CodeGroup title="Migrate the data">
```bash
docker pull ghcr.io/formbricks/data-migrations:latest && \
docker run --rm \
--network=formbricks_default \
-e DATABASE_URL="postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" \
-e UPGRADE_TO_VERSION="v2.3" \
ghcr.io/formbricks/data-migrations:latest
```
</CodeGroup>
</Col>
The above command will migrate your data to the latest schema. This is a crucial step to migrate your existing data to the new structure. Only if the script runs successful, changes are made to the database. The script can safely run multiple times.
6. That's it! Once the migration is complete, you can **now access your Formbricks instance** at the same URL as before.
### Additional Updates
The feature to create short urls in Formbricks is now deprecated. Previously generated short urls will keep working for now but we recommend to use the long urls instead. Redirect support for short urls will be removed in a future release.
## v2.2
Formbricks v2.2 introduces XM research presets into your products with a brand new product onboarding. Our objective is to make user research “obviously easy”, industry by industry. And we're starting with Software-as-a-Service and E-Commerce.

View File

@@ -33,7 +33,7 @@ const Anchor = ({ id, inView, children }: { id: string; inView: boolean; childre
return (
<Link href={`#${id}`} className="group text-inherit no-underline hover:text-inherit">
{inView && (
<div className="absolute ml-[calc(-1*var(--width))] mt-1 hidden w-[var(--width)] opacity-0 transition [--width:calc(2.625rem+0.5px+50%-min(50%,calc(theme(maxWidth.lg)+theme(spacing.8))))] group-hover:opacity-100 group-focus:opacity-100 md:block lg:z-50 2xl:[--width:theme(spacing.10)]">
<div className="absolute ml-[calc(-1*var(--width))] mt-1 hidden w-[var(--width)] opacity-0 transition [--width:calc(1.35rem+0.85px+38%-min(38%,calc(theme(maxWidth.lg)+theme(spacing.2))))] group-hover:opacity-100 group-focus:opacity-100 md:block lg:z-50 2xl:[--width:theme(spacing.10)]">
<div className="group/anchor block h-5 w-5 rounded-lg bg-zinc-50 ring-1 ring-inset ring-zinc-300 transition hover:ring-zinc-500 dark:bg-zinc-800 dark:ring-zinc-700 dark:hover:bg-zinc-700 dark:hover:ring-zinc-600">
<AnchorIcon className="h-5 w-5 stroke-zinc-500 transition dark:stroke-zinc-400 dark:group-hover/anchor:stroke-white" />
</div>

View File

@@ -53,7 +53,7 @@ export const SideNavigation = ({ pathname }) => {
onClick={() => setSelectedId(heading.id)}
className={`${
heading.id === selectedId
? "text-brand font-medium text-opacity-35"
? "text-brand font-medium"
: "font-normal text-slate-600 hover:text-slate-950 dark:text-white dark:hover:text-slate-50"
}`}>
{heading.text}

View File

@@ -140,7 +140,15 @@ export const navigation: Array<NavGroup> = [
{ title: "SDK: Formbricks API", href: "/developer-docs/api-sdk" },
{ title: "REST API", href: "/developer-docs/rest-api" },
{ title: "Webhooks", href: "/developer-docs/webhooks" },
{ title: "Contributing", href: "/developer-docs/contributing" },
{
title: "Contributing",
children: [
{ title: "Get started", href: "/developer-docs/contributing/get-started" },
{ title: "Codespaces", href: "/developer-docs/contributing/codespaces" },
{ title: "Gitpod", href: "/developer-docs/contributing/gitpod" },
{ title: "Troubleshooting", href: "/developer-docs/contributing/troubleshooting" },
],
},
],
},
];

View File

@@ -19,7 +19,7 @@
"@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*",
"@headlessui/react": "^2.0.4",
"@headlessui/react": "^2.1.1",
"@headlessui/tailwindcss": "^0.2.1",
"@mapbox/rehype-prism": "^0.9.0",
"@mdx-js/loader": "^3.0.1",
@@ -33,10 +33,10 @@
"clsx": "^2.1.1",
"fast-glob": "^3.3.2",
"flexsearch": "^0.7.43",
"framer-motion": "11.2.11",
"framer-motion": "11.2.12",
"lottie-web": "^5.12.2",
"lucide": "^0.395.0",
"lucide-react": "^0.395.0",
"lucide": "^0.397.0",
"lucide-react": "^0.397.0",
"mdast-util-to-string": "^4.0.0",
"mdx-annotations": "^0.1.4",
"next": "14.2.4",
@@ -62,7 +62,7 @@
"tailwindcss": "^3.4.4",
"unist-util-filter": "^5.0.1",
"unist-util-visit": "^5.0.0",
"zustand": "^4.5.2"
"zustand": "^4.5.4"
},
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",

View File

@@ -1,8 +1,10 @@
import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
import { getOrganization } from "@formbricks/lib/organization/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { ToasterClient } from "@formbricks/ui/ToasterClient";
@@ -20,8 +22,19 @@ const ProductOnboardingLayout = async ({ children, params }) => {
const membership = await getMembershipByUserIdOrganizationId(session.user.id, params.organizationId);
if (!membership || membership.role === "viewer") return notFound();
const organization = await getOrganization(params.organizationId);
if (!organization) {
throw new Error("Organization not found");
}
return (
<div className="flex-1 bg-slate-50">
<PosthogIdentify
session={session}
organizationId={organization.id}
organizationName={organization.name}
organizationBilling={organization.billing}
/>
<ToasterClient />
{children}
</div>

View File

@@ -1,7 +1,5 @@
"use client";
import OnboardingSurveyBg from "@/images/onboarding-survey-bg.jpg";
import Image from "next/image";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import { TProductConfigChannel } from "@formbricks/types/product";
@@ -41,13 +39,10 @@ export const OnboardingSurvey = ({ organizationId, channel, userId }: Onboarding
return (
<div
className={`overflow relative flex h-[100vh] flex-col items-center justify-center ${fadeout ? "opacity-0 transition-opacity duration-1000" : "opacity-100"}`}>
<Image src={OnboardingSurveyBg} className="absolute inset-0 h-full w-full" alt="OnboardingSurveyBg" />
<div className="relative h-[60vh] w-[50vh] overflow-auto">
<iframe
onLoad={() => setIsIFrameVisible(true)}
src={`https://app.formbricks.com/s/clxcwr22p0cwlpvgekzdab2x5?embed=true&userId=${userId}`}
className="absolute left-0 top-0 h-full w-full overflow-visible border-0"></iframe>
</div>
<iframe
onLoad={() => setIsIFrameVisible(true)}
src={`https://app.formbricks.com/s/clxcwr22p0cwlpvgekzdab2x5?userId=${userId}`}
className="absolute left-0 top-0 h-full w-full overflow-visible border-0"></iframe>
</div>
);
};

View File

@@ -1,8 +1,10 @@
import { PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Checkbox } from "@formbricks/ui/Checkbox";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
@@ -30,6 +32,15 @@ export const CalQuestionForm = ({
attributeClasses,
}: CalQuestionFormProps): JSX.Element => {
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
const [isCalHostEnabled, setIsCalHostEnabled] = useState(!!question.calHost);
useEffect(() => {
if (!isCalHostEnabled) {
updateQuestion(questionIdx, { calHost: undefined });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCalHostEnabled]);
return (
<form>
@@ -80,15 +91,41 @@ export const CalQuestionForm = ({
Add Description
</Button>
)}
<div className="mt-3">
<Label htmlFor="calUserName">Add your Cal.com username or username/event</Label>
<div className="mt-2">
<Input
id="calUserName"
name="calUserName"
value={question.calUserName}
onChange={(e) => updateQuestion(questionIdx, { calUserName: e.target.value })}
/>
<div className="mt-3 flex flex-col gap-4">
<div className="flex flex-col gap-3">
<Label htmlFor="calUserName">Add your Cal.com username or username/event</Label>
<div>
<Input
id="calUserName"
name="calUserName"
value={question.calUserName}
onChange={(e) => updateQuestion(questionIdx, { calUserName: e.target.value })}
/>
</div>
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<Checkbox
id="calHost"
checked={isCalHostEnabled}
onCheckedChange={(checked: boolean) => setIsCalHostEnabled(checked)}
/>
<Label htmlFor="calHost">Do you have a self-hosted Cal.com instance?</Label>
</div>
{isCalHostEnabled && (
<div className="flex flex-col gap-2">
<Label htmlFor="calHost">Enter the hostname of your self-hosted Cal.com instance</Label>
<Input
id="calHost"
name="calHost"
placeholder="cal.com"
value={question.calHost}
onChange={(e) => updateQuestion(questionIdx, { calHost: e.target.value })}
/>
</div>
)}
</div>
</div>
</div>

View File

@@ -1,11 +1,13 @@
import { MainNavigation } from "@/app/(app)/environments/[environmentId]/components/MainNavigation";
import { TopControlBar } from "@/app/(app)/environments/[environmentId]/components/TopControlBar";
import type { Session } from "next-auth";
import { getIsMultiOrgEnabled } from "@formbricks/ee/lib/service";
import { getEnterpriseLicense } from "@formbricks/ee/lib/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getEnvironment, getEnvironments } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
import {
getMonthlyActiveOrganizationPeopleCount,
getMonthlyOrganizationResponseCount,
getOrganizationByEnvironmentId,
getOrganizationsByUserId,
} from "@formbricks/lib/organization/service";
@@ -13,6 +15,7 @@ import { getProducts } from "@formbricks/lib/product/service";
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import { LimitsReachedBanner } from "@formbricks/ui/LimitsReachedBanner";
import { PendingDowngradeBanner } from "@formbricks/ui/PendingDowngradeBanner";
interface EnvironmentLayoutProps {
environmentId: string;
@@ -41,16 +44,42 @@ export const EnvironmentLayout = async ({ environmentId, session, children }: En
}
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
const isMultiOrgEnabled = await getIsMultiOrgEnabled();
const { features, lastChecked, isPendingDowngrade, active } = await getEnterpriseLicense();
const isMultiOrgEnabled = features?.isMultiOrgEnabled ?? false;
const currentProductChannel =
products.find((product) => product.id === environment.productId)?.config.channel ?? null;
let peopleCount = 0;
let responseCount = 0;
if (IS_FORMBRICKS_CLOUD) {
[peopleCount, responseCount] = await Promise.all([
getMonthlyActiveOrganizationPeopleCount(organization.id),
getMonthlyOrganizationResponseCount(organization.id),
]);
}
return (
<div className="flex h-screen min-h-screen flex-col overflow-hidden">
<DevEnvironmentBanner environment={environment} />
{IS_FORMBRICKS_CLOUD && <LimitsReachedBanner organization={organization} />}
{IS_FORMBRICKS_CLOUD && (
<LimitsReachedBanner
organization={organization}
environmentId={environment.id}
peopleCount={peopleCount}
responseCount={responseCount}
/>
)}
<PendingDowngradeBanner
lastChecked={lastChecked}
isPendingDowngrade={isPendingDowngrade ?? false}
active={active}
environmentId={environment.id}
/>
<div className="flex h-full">
<MainNavigation

View File

@@ -291,7 +291,7 @@ export const AddIntegrationModal = ({
<div className="space-y-4">
<div>
<Label htmlFor="Surveys">Questions</Label>
<div className="mt-1 rounded-lg border border-slate-200">
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map(
(question) => (

View File

@@ -229,7 +229,7 @@ export const AddIntegrationModal = ({
<div className="space-y-4">
<div>
<Label htmlFor="Surveys">Questions</Label>
<div className="mt-1 rounded-lg border border-slate-200">
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions.map(
(question) => (

View File

@@ -231,7 +231,7 @@ export const AddChannelMappingModal = ({
<div>
<div>
<Label htmlFor="Surveys">Questions</Label>
<div className="mt-1 rounded-lg border border-slate-200">
<div className="mt-1 max-h-[15vh] overflow-y-auto rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
{replaceHeadlineRecall(selectedSurvey, "default", attributeClasses)?.questions?.map(
(question) => (

View File

@@ -216,9 +216,6 @@ export const PricingTable = ({
Yearly
</div>
</div>
<div className="mb-4 flex w-fit items-center overflow-hidden rounded-lg border border-orange-200 bg-orange-50 px-3 py-1 text-orange-950 lg:mb-0">
50% off for 6 months with PH50 - 24h only 🔥
</div>
</div>
<div className="relative mx-auto grid max-w-md grid-cols-1 gap-y-8 lg:mx-0 lg:-mb-14 lg:max-w-none lg:grid-cols-4">
<div

View File

@@ -2,7 +2,7 @@ import { OrganizationSettingsNavbar } from "@/app/(app)/environments/[environmen
import { CheckIcon } from "lucide-react";
import { getServerSession } from "next-auth";
import { notFound } from "next/navigation";
import { getIsEnterpriseEdition } from "@formbricks/ee/lib/service";
import { getEnterpriseLicense } from "@formbricks/ee/lib/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
@@ -37,7 +37,7 @@ const Page = async ({ params }) => {
notFound();
}
const isEnterpriseEdition = await getIsEnterpriseEdition();
const { active: isEnterpriseEdition } = await getEnterpriseLicense();
const paidFeatures = [
{

View File

@@ -1,6 +1,8 @@
import NextAuth from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
export const fetchCache = "force-no-store";
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

View File

@@ -16,7 +16,7 @@ export const GET = async (req: NextRequest) => {
const baseId = z.string().safeParse(queryParams.get("baseId"));
if (!baseId.success) {
return responses.missingFieldResponse("Base Id is Required");
return responses.badRequestResponse("Base Id is Required");
}
if (!session) {

View File

@@ -2,7 +2,7 @@ import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth";
import { responses } from "@/app/lib/api/response";
import { transformErrorToDetails } from "@/app/lib/api/validator";
import { deleteSurvey, getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
import { TSurvey, ZSurvey } from "@formbricks/types/surveys";
import { TSurvey, ZSurveyUpdateInput } from "@formbricks/types/surveys";
const fetchAndAuthorizeSurvey = async (authentication: any, surveyId: string): Promise<TSurvey | null> => {
const survey = await getSurvey(surveyId);
@@ -68,7 +68,7 @@ export const PUT = async (
console.error(`Error parsing JSON input: ${error}`);
return responses.badRequestResponse("Malformed JSON input, please check your request body");
}
const inputValidation = ZSurvey.safeParse({
const inputValidation = ZSurveyUpdateInput.safeParse({
...survey,
...surveyUpdate,
});

View File

@@ -30,8 +30,18 @@ const corsHeaders = {
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
const goneResponse = (message: string, details?: { [key: string]: string }, cors: boolean = false) =>
Response.json(
const goneResponse = (
message: string,
details?: { [key: string]: string },
cors: boolean = false,
cache: string = "private, no-store"
) => {
const headers = {
...(cors && corsHeaders),
"Cache-Control": cache,
};
return Response.json(
{
code: "gone",
message,
@@ -39,12 +49,23 @@ const goneResponse = (message: string, details?: { [key: string]: string }, cors
} as ApiErrorResponse,
{
status: 410,
...(cors && { headers: corsHeaders }),
headers,
}
);
};
const badRequestResponse = (message: string, details?: { [key: string]: string }, cors: boolean = false) =>
Response.json(
const badRequestResponse = (
message: string,
details?: { [key: string]: string },
cors: boolean = false,
cache: string = "private, no-store"
) => {
const headers = {
...(cors && corsHeaders),
"Cache-Control": cache,
};
return Response.json(
{
code: "bad_request",
message,
@@ -52,25 +73,23 @@ const badRequestResponse = (message: string, details?: { [key: string]: string }
} as ApiErrorResponse,
{
status: 400,
...(cors && { headers: corsHeaders }),
headers,
}
);
const missingFieldResponse = (field: string, cors: boolean = false) =>
badRequestResponse(
`Missing ${field}`,
{
missing_field: field,
},
cors
);
};
const methodNotAllowedResponse = (
res: CustomNextApiResponse,
allowedMethods: string[],
cors: boolean = false
) =>
Response.json(
cors: boolean = false,
cache: string = "private, no-store"
) => {
const headers = {
...(cors && corsHeaders),
"Cache-Control": cache,
};
return Response.json(
{
code: "method_not_allowed",
message: `The HTTP ${res.req?.method} method is not supported by this route.`,
@@ -80,9 +99,10 @@ const methodNotAllowedResponse = (
} as ApiErrorResponse,
{
status: 405,
...(cors && { headers: corsHeaders }),
headers,
}
);
};
const notFoundResponse = (
resourceType: string,
@@ -111,8 +131,13 @@ const notFoundResponse = (
);
};
const notAuthenticatedResponse = (cors: boolean = false) =>
Response.json(
const notAuthenticatedResponse = (cors: boolean = false, cache: string = "private, no-store") => {
const headers = {
...(cors && corsHeaders),
"Cache-Control": cache,
};
return Response.json(
{
code: "not_authenticated",
message: "Not authenticated",
@@ -122,12 +147,18 @@ const notAuthenticatedResponse = (cors: boolean = false) =>
} as ApiErrorResponse,
{
status: 401,
...(cors && { headers: corsHeaders }),
headers,
}
);
};
const unauthorizedResponse = (cors: boolean = false) =>
Response.json(
const unauthorizedResponse = (cors: boolean = false, cache: string = "private, no-store") => {
const headers = {
...(cors && corsHeaders),
"Cache-Control": cache,
};
return Response.json(
{
code: "unauthorized",
message: "You are not authorized to access this resource",
@@ -135,16 +166,23 @@ const unauthorizedResponse = (cors: boolean = false) =>
} as ApiErrorResponse,
{
status: 401,
...(cors && { headers: corsHeaders }),
headers,
}
);
};
const forbiddenResponse = (
message: string,
cors: boolean = false,
details: ApiErrorResponse["details"] = {}
) =>
Response.json(
details: ApiErrorResponse["details"] = {},
cache: string = "private, no-store"
) => {
const headers = {
...(cors && corsHeaders),
"Cache-Control": cache,
};
return Response.json(
{
code: "forbidden",
message,
@@ -152,9 +190,10 @@ const forbiddenResponse = (
} as ApiErrorResponse,
{
status: 403,
...(cors && { headers: corsHeaders }),
headers,
}
);
};
const successResponse = (data: Object, cors: boolean = false, cache: string = "private, no-store") => {
const headers = {
@@ -223,7 +262,6 @@ export const responses = {
goneResponse,
badRequestResponse,
internalServerErrorResponse,
missingFieldResponse,
methodNotAllowedResponse,
notAuthenticatedResponse,
unauthorizedResponse,

View File

@@ -185,6 +185,7 @@ export const questionTypes: TQuestion[] = [
preset: {
headline: { default: "Schedule a call with me" },
calUserName: "rick/get-rick-rolled",
calHost: "cal.com",
} as Partial<TSurveyCalQuestion>,
},
{

View File

@@ -1,6 +1,6 @@
import { CacheHandler } from "@neshca/cache-handler";
import createLruHandler from "@neshca/cache-handler/local-lru";
import createRedisHandler from "@neshca/cache-handler/redis-stack";
import createRedisHandler from "@neshca/cache-handler/redis-strings";
import { createClient } from "redis";
// Function to create a timeout promise
@@ -27,6 +27,7 @@ CacheHandler.onCreation(async () => {
if (client) {
try {
// Wait for the client to connect with a timeout of 5000ms.
const connectPromise = client.connect();
const timeoutPromise = createTimeoutPromise(5000, "Redis connection timed out"); // 5000ms timeout
await Promise.race([connectPromise, timeoutPromise]);
} catch (error) {
@@ -53,7 +54,7 @@ CacheHandler.onCreation(async () => {
// Create the `redis-stack` Handler if the client is available and connected.
handler = await createRedisHandler({
client,
keyPrefix: "formbricks:",
keyPrefix: "fb:",
timeoutMs: 1000,
});
} else {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@formbricks/web",
"version": "2.2.2",
"version": "2.3.0",
"private": true,
"scripts": {
"clean": "rimraf .turbo node_modules .next",
@@ -29,27 +29,27 @@
"@hookform/resolvers": "^3.6.0",
"@json2csv/node": "^7.0.6",
"@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-collapsible": "^1.1.0",
"@react-email/components": "^0.0.19",
"@sentry/nextjs": "^8.10.0",
"@sentry/nextjs": "^8.12.0",
"@vercel/og": "^0.6.2",
"@vercel/speed-insights": "^1.0.12",
"bcryptjs": "^2.4.3",
"dotenv": "^16.4.5",
"encoding": "^0.1.13",
"framer-motion": "11.2.11",
"googleapis": "^140.0.0",
"framer-motion": "11.2.12",
"googleapis": "^140.0.1",
"jiti": "^1.21.6",
"jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21",
"lru-cache": "^10.2.2",
"lucide-react": "^0.395.0",
"lucide-react": "^0.397.0",
"mime": "^4.0.3",
"next": "15.0.0-rc.0",
"optional": "^0.1.4",
"otplib": "^12.0.1",
"papaparse": "^5.4.1",
"posthog-js": "^1.139.3",
"posthog-js": "^1.141.4",
"prismjs": "^1.29.0",
"qrcode": "^1.5.3",
"react": "19.0.0-rc-935180c7e0-20240524",
@@ -59,7 +59,7 @@
"redis": "^4.6.14",
"sharp": "^0.33.4",
"ua-parser-js": "^1.0.38",
"webpack": "^5.92.0",
"webpack": "^5.92.1",
"xlsx": "^0.18.5"
},
"devDependencies": {

View File

@@ -32,14 +32,14 @@
"storybook": "turbo run storybook"
},
"devDependencies": {
"@playwright/test": "^1.44.1",
"@playwright/test": "^1.45.0",
"@formbricks/eslint-config": "workspace:*",
"eslint": "^8.57.0",
"husky": "^9.0.11",
"lint-staged": "^15.2.7",
"rimraf": "^5.0.7",
"tsx": "^4.15.6",
"turbo": "^2.0.4"
"tsx": "^4.15.7",
"turbo": "^2.0.5"
},
"lint-staged": {
"(apps|packages)/**/*.{js,ts,jsx,tsx}": [
@@ -63,7 +63,7 @@
"showDetails": true
},
"dependencies": {
"@changesets/cli": "^2.27.5",
"playwright": "^1.44.1"
"@changesets/cli": "^2.27.6",
"playwright": "^1.45.0"
}
}

View File

@@ -39,7 +39,8 @@
"data-migration:adds_app_and_website_status_indicator": "ts-node ./data-migrations/20240610055828_adds_app_and_website_status_indicators/data-migration.ts",
"data-migration:product-config": "ts-node ./data-migrations/20240612115151_adds_product_config/data-migration.ts",
"data-migration:v2.2": "pnpm data-migration:adds_app_and_website_status_indicator && pnpm data-migration:product-config && pnpm data-migration:pricing-v2",
"data-migration:zh-to-zh-Hans": "ts-node ./data-migrations/20240625101352_update_zh_to_zh-Hans/data-migration.ts"
"data-migration:zh-to-zh-Hans": "ts-node ./data-migrations/20240625101352_update_zh_to_zh-Hans/data-migration.ts",
"data-migration:v2.3": "pnpm data-migration:zh-to-zh-Hans"
},
"dependencies": {
"@prisma/client": "^5.15.1",

View File

@@ -28,8 +28,7 @@ export const CLOUD_PRICING_DATA = {
id: "startup",
featured: false,
description: "Everything in Free with additional features.",
offer: true,
price: { monthly: "€29", yearly: "€25" },
price: { monthly: "€59", yearly: "€49" },
mainFeatures: [
"Everything in Free",
"Remove Branding",
@@ -44,8 +43,7 @@ export const CLOUD_PRICING_DATA = {
id: "scale",
featured: true,
description: "Advanced features for scaling your business.",
offer: true,
price: { monthly: "€99", yearly: "€89" },
price: { monthly: "€199", yearly: "€179" },
mainFeatures: [
"Everything in Startup",
"Team Access Roles",

View File

@@ -19,7 +19,7 @@ const PREVIOUS_RESULTS_CACHE_TAG_KEY = `getPreviousResult-${hashedKey}` as const
// This function is used to get the previous result of the license check from the cache
// This might seem confusing at first since we only return the default value from this function,
// but since we are using a cache and the cache key is the same, the cache will return the previous result - so this functions as a cache getter
// but since we are using a cache and the cache key is the same, the cache will return the previous result - so this function acts as a cache getter
const getPreviousResult = (): Promise<{
active: boolean | null;
lastChecked: Date;
@@ -89,47 +89,87 @@ const fetchLicenseForE2ETesting = async (): Promise<{
}
};
export const getIsEnterpriseEdition = async (): Promise<boolean> => {
export const getEnterpriseLicense = async (): Promise<{
active: boolean;
features: TEnterpriseLicenseFeatures | null;
lastChecked: Date;
isPendingDowngrade?: boolean;
}> => {
if (!ENTERPRISE_LICENSE_KEY || ENTERPRISE_LICENSE_KEY.length === 0) {
return false;
return {
active: false,
features: null,
lastChecked: new Date(),
};
}
if (E2E_TESTING) {
const previousResult = await fetchLicenseForE2ETesting();
return previousResult && previousResult.active !== null ? previousResult.active : false;
return {
active: previousResult?.active ?? false,
features: previousResult ? previousResult.features : null,
lastChecked: previousResult ? previousResult.lastChecked : new Date(),
};
}
// if the server responds with a boolean, we return it
// if the server errors, we return null
// null signifies an error
const license = await fetchLicense();
const isValid = license ? license.status === "active" : null;
const threeDaysInMillis = 3 * 24 * 60 * 60 * 1000;
const currentTime = new Date();
const previousResult = await getPreviousResult();
// Case: First time checking license and the server errors out
if (previousResult.active === null) {
if (isValid === null) {
await setPreviousResult({
const newResult = {
active: false,
features: { isMultiOrgEnabled: false },
lastChecked: new Date(),
});
return false;
};
await setPreviousResult(newResult);
return newResult;
}
}
if (isValid !== null && license) {
await setPreviousResult({ active: isValid, features: license.features, lastChecked: new Date() });
return isValid;
const newResult = {
active: isValid,
features: license.features,
lastChecked: new Date(),
};
await setPreviousResult(newResult);
return newResult;
} else {
// if result is undefined -> error
// if the last check was less than 72 hours, return the previous value:
if (new Date().getTime() - previousResult.lastChecked.getTime() <= 3 * 24 * 60 * 60 * 1000) {
return previousResult.active !== null ? previousResult.active : false;
const elapsedTime = currentTime.getTime() - previousResult.lastChecked.getTime();
if (elapsedTime < threeDaysInMillis) {
return {
active: previousResult.active !== null ? previousResult.active : false,
features: previousResult.features,
lastChecked: previousResult.lastChecked,
isPendingDowngrade: true,
};
}
// if the last check was more than 72 hours, return false and log the error
// Log error only after 72 hours
console.error("Error while checking license: The license check failed");
return false;
return {
active: false,
features: null,
lastChecked: previousResult.lastChecked,
isPendingDowngrade: true,
};
}
};
@@ -139,14 +179,13 @@ export const getLicenseFeatures = async (): Promise<TEnterpriseLicenseFeatures |
return previousResult.features;
} else {
const license = await fetchLicense();
if (!license) return null;
const features = await license.features;
return features;
if (!license || !license.features) return null;
return license.features;
}
};
export const fetchLicense = async () => {
const licenseResult: TEnterpriseLicenseDetails | null = await cache(
export const fetchLicense = async (): Promise<TEnterpriseLicenseDetails | null> =>
await cache(
async () => {
if (!env.ENTERPRISE_LICENSE_KEY) return null;
try {
@@ -192,8 +231,6 @@ export const fetchLicense = async () => {
[`fetchLicense-${hashedKey}`],
{ revalidate: 60 * 60 * 24 }
)();
return licenseResult;
};
export const getRemoveInAppBrandingPermission = (organization: TOrganization): boolean => {
if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PRODUCT_FEATURE_KEYS.FREE;
@@ -213,7 +250,7 @@ export const getRoleManagementPermission = async (organization: TOrganization):
organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE ||
organization.billing.plan === PRODUCT_FEATURE_KEYS.ENTERPRISE
);
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
return false;
};
@@ -223,13 +260,13 @@ export const getAdvancedTargetingPermission = async (organization: TOrganization
organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE ||
organization.billing.plan === PRODUCT_FEATURE_KEYS.ENTERPRISE
);
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
else return false;
};
export const getBiggerUploadFileSizePermission = async (organization: TOrganization): Promise<boolean> => {
if (IS_FORMBRICKS_CLOUD) return organization.billing.plan !== PRODUCT_FEATURE_KEYS.FREE;
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
return false;
};
@@ -243,7 +280,7 @@ export const getMultiLanguagePermission = async (organization: TOrganization): P
organization.billing.plan === PRODUCT_FEATURE_KEYS.SCALE ||
organization.billing.plan === PRODUCT_FEATURE_KEYS.ENTERPRISE
);
else if (!IS_FORMBRICKS_CLOUD) return await getIsEnterpriseEdition();
else if (!IS_FORMBRICKS_CLOUD) return (await getEnterpriseLicense()).active;
return false;
};

View File

@@ -7,7 +7,7 @@
e.parentNode.insertBefore(t, e),
setTimeout(function () {
formbricks.init({
environmentId: "clxvg2bf40005m66n1mrqsi1d",
environmentId: "cly414cqm000cstwa5s8w0mg2",
userId: "RANDOM_USER_ID",
apiHost: "http://localhost:3000",
});

View File

@@ -228,6 +228,7 @@ export const mockCalQuestion: TSurveyCalQuestion = {
default: "Skip",
},
calUserName: "rick/get-rick-rolled",
calHost: "cal.com",
id: "o3bnux6p42u9ew9d02l14r26",
type: TSurveyQuestionTypeEnum.Cal,
isDraft: true,

View File

@@ -25,7 +25,8 @@ export const capturePosthogEnvironmentEvent = async (
host: env.NEXT_PUBLIC_POSTHOG_API_HOST,
});
client.capture({
distinctId: environmentId,
// workaround with a static string as exaplained in PostHog docs: https://posthog.com/docs/product-analytics/group-analytics
distinctId: "environmentEvents",
event: eventName,
groups: { environment: environmentId },
properties,

View File

@@ -37,13 +37,13 @@
},
"devDependencies": {
"@calcom/embed-snippet": "1.3.0",
"@formbricks/lib": "workspace:*",
"@formbricks/config-typescript": "workspace:*",
"@formbricks/eslint-config": "workspace:*",
"@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*",
"@preact/preset-vite": "^2.8.2",
"autoprefixer": "^10.4.19",
"concurrently": "8.2.2",
"@formbricks/eslint-config": "workspace:*",
"isomorphic-dompurify": "^2.12.0",
"postcss": "^8.4.38",
"preact": "^10.22.0",

View File

@@ -13,7 +13,7 @@ export const BackButton = ({ onClick, backButtonLabel, tabIndex = 2 }: BackButto
tabIndex={tabIndex}
type={"button"}
className={cn(
"border-back-button-border text-heading focus:ring-focus rounded-custom flex items-center border px-3 py-3 text-base font-medium leading-4 shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2"
"fb-border-back-button-border fb-text-heading focus:fb-ring-focus fb-rounded-custom fb-flex fb-items-center fb-border fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 fb-shadow-sm hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2"
)}
onClick={onClick}>
{backButtonLabel || "Back"}

View File

@@ -1,21 +1,21 @@
import { JSX } from "preact";
import { useCallback } from "preact/hooks";
interface SubmitButtonProps {
interface SubmitButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {
buttonLabel: string | undefined;
isLastQuestion: boolean;
onClick?: () => void;
focus?: boolean;
tabIndex?: number;
type?: "submit" | "button";
}
export const SubmitButton = ({
buttonLabel,
isLastQuestion,
onClick = () => {},
tabIndex = 1,
focus = false,
type = "submit",
onClick,
disabled,
type,
...props
}: SubmitButtonProps) => {
const buttonRef = useCallback(
(currentButton: HTMLButtonElement | null) => {
@@ -30,13 +30,15 @@ export const SubmitButton = ({
return (
<button
{...props}
dir="auto"
ref={buttonRef}
type={type}
tabIndex={tabIndex}
autoFocus={focus}
className="bg-brand border-submit-button-border text-on-brand focus:ring-focus rounded-custom flex items-center border px-3 py-3 text-base font-medium leading-4 shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2"
onClick={onClick}>
className="fb-bg-brand fb-border-submit-button-border fb-text-on-brand focus:fb-ring-focus fb-rounded-custom fb-flex fb-items-center fb-border fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 fb-shadow-sm hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2"
onClick={onClick}
disabled={disabled}>
{buttonLabel || (isLastQuestion ? "Finish" : "Next")}
</button>
);

View File

@@ -4,10 +4,10 @@ interface AutoCloseProgressBarProps {
export const AutoCloseProgressBar = ({ autoCloseTimeout }: AutoCloseProgressBarProps) => {
return (
<div className="bg-accent-bg h-2 w-full overflow-hidden rounded-full">
<div className="fb-bg-accent-bg fb-h-2 fb-w-full fb-overflow-hidden fb-rounded-full">
<div
key={autoCloseTimeout}
className="bg-brand z-20 h-2 rounded-full"
className="fb-bg-brand fb-z-20 fb-h-2 fb-rounded-full"
style={{
animation: `shrink-width-to-zero ${autoCloseTimeout}s linear forwards`,
}}></div>

View File

@@ -44,12 +44,16 @@ export const CalEmbed = ({ question, onSuccessfulBooking }: CalEmbedProps) => {
useEffect(() => {
// remove any existing cal-inline elements
document.querySelectorAll("cal-inline").forEach((el) => el.remove());
cal("inline", { elementOrSelector: "#fb-cal-embed", calLink: question.calUserName });
}, [cal, question.calUserName]);
cal("init", { calOrigin: question.calHost ? `https://${question.calHost}` : "https://cal.com" });
cal("inline", {
elementOrSelector: "#fb-cal-embed",
calLink: question.calUserName,
});
}, [cal, question.calHost, question.calUserName]);
return (
<div className="relative mt-4 overflow-auto">
<div id="fb-cal-embed" className={cn("border-border rounded-lg border")} />
<div className="fb-relative fb-mt-4 fb-overflow-auto">
<div id="fb-cal-embed" className={cn("fb-border-border fb-rounded-lg fb-border")} />
</div>
);
};

View File

@@ -147,27 +147,29 @@ export const FileInput = ({
return (
<div
className={`items-left bg-input-bg hover:bg-input-bg-selected border-border relative mt-3 flex w-full flex-col justify-center rounded-lg border-2 border-dashed dark:border-slate-600 dark:bg-slate-700 dark:hover:border-slate-500 dark:hover:bg-slate-800`}>
className={`fb-items-left fb-bg-input-bg hover:fb-bg-input-bg-selected fb-border-border fb-relative fb-mt-3 fb-flex fb-w-full fb-flex-col fb-justify-center fb-rounded-lg fb-border-2 fb-border-dashed dark:fb-border-slate-600 dark:fb-bg-slate-700 dark:hover:fb-border-slate-500 dark:hover:fb-bg-slate-800`}>
<div>
{fileUrls?.map((file, index) => {
const fileName = getOriginalFileNameFromUrl(file);
return (
<div key={index} className="bg-input-bg-selected border-border relative m-2 rounded-md border">
<div className="absolute right-0 top-0 m-2">
<div className="bg-survey-bg flex h-5 w-5 cursor-pointer items-center justify-center rounded-md">
<div
key={index}
className="fb-bg-input-bg-selected fb-border-border fb-relative fb-m-2 fb-rounded-md fb-border">
<div className="fb-absolute fb-right-0 fb-top-0 fb-m-2">
<div className="fb-bg-survey-bg fb-flex fb-h-5 fb-w-5 fb-cursor-pointer fb-items-center fb-justify-center fb-rounded-md">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 26 26"
strokeWidth={1}
stroke="currentColor"
className="text-heading h-5"
className="fb-text-heading fb-h-5"
onClick={(e) => handleDeleteFile(index, e)}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 9l10 10m0-10L9 19" />
</svg>
</div>
</div>
<div className="flex flex-col items-center justify-center p-2">
<div className="fb-flex fb-flex-col fb-items-center fb-justify-center fb-p-2">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
@@ -178,11 +180,11 @@ export const FileInput = ({
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-heading h-6">
className="fb-text-heading fb-h-6">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
<polyline points="14 2 14 8 20 8" />
</svg>
<p className="text-heading mt-1 w-full overflow-hidden overflow-ellipsis whitespace-nowrap px-2 text-center text-sm">
<p className="fb-text-heading fb-mt-1 fb-w-full fb-overflow-hidden fb-overflow-ellipsis fb-whitespace-nowrap fb-px-2 fb-text-center fb-text-sm">
{fileName}
</p>
</div>
@@ -193,8 +195,8 @@ export const FileInput = ({
<div>
{isUploading && (
<div className="inset-0 flex animate-pulse items-center justify-center rounded-lg py-4">
<label htmlFor={uniqueHtmlFor} className="text-subheading text-sm font-medium">
<div className="fb-inset-0 fb-flex fb-animate-pulse fb-items-center fb-justify-center fb-rounded-lg fb-py-4">
<label htmlFor={uniqueHtmlFor} className="fb-text-subheading fb-text-sm fb-font-medium">
Uploading...
</label>
</div>
@@ -203,7 +205,7 @@ export const FileInput = ({
<label htmlFor={uniqueHtmlFor} onDragOver={handleDragOver} onDrop={handleDrop}>
{showUploader && (
<div
className="focus:outline-brand flex flex-col items-center justify-center py-6 hover:cursor-pointer"
className="focus:fb-outline-brand fb-flex fb-flex-col fb-items-center fb-justify-center fb-py-6 hover:fb-cursor-pointer"
tabIndex={1}
onKeyDown={(e) => {
// Accessibility: if spacebar was pressed pass this down to the input
@@ -219,22 +221,22 @@ export const FileInput = ({
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="text-placeholder h-6">
className="fb-text-placeholder fb-h-6">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
/>
</svg>
<p className="text-placeholder mt-2 text-sm dark:text-slate-400">
<span className="font-medium">Click or drag to upload files.</span>
<p className="fb-text-placeholder fb-mt-2 fb-text-sm dark:fb-text-slate-400">
<span className="fb-font-medium">Click or drag to upload files.</span>
</p>
<input
type="file"
id={uniqueHtmlFor}
name={uniqueHtmlFor}
accept={allowedFileExtensions?.map((ext) => `.${ext}`).join(",")}
className="hidden"
className="fb-hidden"
onChange={(e) => {
const inputElement = e.target as HTMLInputElement;
if (inputElement.files) {

View File

@@ -4,11 +4,11 @@ export const FormbricksBranding = () => {
href="https://formbricks.com?utm_source=survey_branding"
target="_blank"
tabIndex={-1}
className="my-2 flex justify-center">
<p className="text-signature text-xs">
className="fb-my-2 fb-flex fb-justify-center">
<p className="fb-text-signature fb-text-xs">
Powered by{" "}
<b>
<span className="text-branding-text hover:text-signature">Formbricks</span>
<span className="fb-text-branding-text hover:fb-text-signature">Formbricks</span>
</b>
</p>
</a>

View File

@@ -12,14 +12,16 @@ export const Headline = ({
alignTextCenter = false,
}: HeadlineProps) => {
return (
<label htmlFor={questionId} className="text-heading mb-1.5 block text-base font-semibold leading-6">
<label
htmlFor={questionId}
className="fb-text-heading fb-mb-1.5 fb-block fb-text-base fb-font-semibold fb-leading-6">
<div
className={`flex items-center ${alignTextCenter ? "justify-center" : "justify-between"}`}
className={`fb-flex fb-items-center ${alignTextCenter ? "fb-justify-center" : "fb-justify-between"}`}
dir="auto">
{headline}
{!required && (
<span
className="text-heading mx-2 self-start text-sm font-normal leading-7 opacity-60"
className="fb-text-heading fb-mx-2 fb-self-start fb-text-sm fb-font-normal fb-leading-7 fb-opacity-60"
tabIndex={-1}>
Optional
</span>

View File

@@ -23,7 +23,7 @@ export const HtmlBody = ({ htmlString, questionId }: HtmlBodyProps) => {
return (
<label
htmlFor={questionId}
className={cn("fb-htmlbody break-words")} // styles are in global.css
className={cn("fb-htmlbody fb-break-words")} // styles are in global.css
dangerouslySetInnerHTML={{ __html: safeHtml }}
dir="auto"
/>

View File

@@ -37,20 +37,20 @@ export const LanguageSwitch = ({
useClickOutside(languageDropdownRef, () => setShowLanguageDropdown(false));
return (
<div class="z-[1001] flex w-fit items-center even:pr-1">
<div class="fb-z-[1001] fb-flex fb-w-fit fb-items-center even:fb-pr-1">
<button
title="Language switch"
type="button"
class="text-heading relative h-5 w-5 rounded-md hover:bg-black/5 focus:outline-none focus:ring-2 focus:ring-offset-2"
class="fb-text-heading fb-relative fb-h-5 fb-w-5 fb-rounded-md hover:fb-bg-black/5 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2"
onClick={toggleDropdown}
tabIndex={-1}
aria-haspopup="true"
aria-expanded={showLanguageDropdown}>
<GlobeIcon className="text-heading h-5 w-5 p-0.5" />
<GlobeIcon className="fb-text-heading fb-h-5 fb-w-5 fb-p-0.5" />
</button>
{showLanguageDropdown && (
<div
className="bg-brand text-on-brand absolute right-8 top-10 space-y-2 rounded-md p-2 text-xs"
className="fb-bg-brand fb-text-on-brand fb-absolute fb-right-8 fb-top-10 fb-space-y-2 fb-rounded-md fb-p-2 fb-text-xs"
ref={languageDropdownRef}>
{surveyLanguages.map((surveyLanguage) => {
if (!surveyLanguage.enabled) return;
@@ -58,7 +58,7 @@ export const LanguageSwitch = ({
<button
key={surveyLanguage.language.id}
type="button"
className="block w-full p-1.5 text-left hover:opacity-80"
className="fb-block fb-w-full fb-p-1.5 fb-text-left hover:fb-opacity-80"
onClick={() => changeLanguage(surveyLanguage.language.code)}>
{getLanguageLabel(surveyLanguage.language.code)}
</button>

View File

@@ -4,15 +4,21 @@ export const LoadingSpinner = ({ className }: { className?: string }) => {
return (
<div
data-testid="loading-spinner"
className={cn("flex h-full w-full items-center justify-center", className ?? "")}>
className={cn("fb-flex fb-h-full fb-w-full fb-items-center fb-justify-center", className ?? "")}>
<svg
className="m-2 h-6 w-6 animate-spin text-slate-700"
className="fb-m-2 fb-h-6 fb-w-6 fb-animate-spin fb-text-slate-700"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<circle
className="fb-opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"></circle>
<path
className="opacity-75"
className="fb-opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>

View File

@@ -1,8 +1,8 @@
export const Progress = ({ progress }: { progress: number }) => {
return (
<div className="bg-accent-bg h-2 w-full overflow-hidden rounded-full">
<div className="fb-bg-accent-bg fb-h-2 fb-w-full fb-overflow-hidden fb-rounded-full">
<div
className="transition-width bg-brand z-20 h-2 rounded-full duration-500"
className="fb-transition-width fb-bg-brand fb-z-20 fb-h-2 fb-rounded-full fb-duration-500"
style={{ width: `${Math.floor(progress * 100)}%` }}></div>
</div>
);

View File

@@ -29,5 +29,9 @@ export const ProgressBar = ({ survey, questionId }: ProgressBarProps) => {
return survey.questions.map((_, index) => calculateProgress(index, survey.questions.length));
}, [calculateProgress, survey]);
return <Progress progress={questionId === "end" ? 1 : progressArray[currentQuestionIdx]} />;
return (
<Progress
progress={questionId === "end" ? 1 : questionId === "start" ? 0 : progressArray[currentQuestionIdx]}
/>
);
};

View File

@@ -31,29 +31,29 @@ export const QuestionMedia = ({ imgUrl, videoUrl, altText = "Image" }: QuestionM
const [isLoading, setIsLoading] = useState(true);
return (
<div className="group/image relative mb-4 block min-h-40 rounded-md">
<div className="fb-group/image fb-relative fb-mb-4 fb-block fb-min-h-40 fb-rounded-md">
{isLoading && (
<div className="absolute inset-auto flex h-full w-full animate-pulse items-center justify-center rounded-md bg-slate-200"></div>
<div className="fb-absolute fb-inset-auto fb-flex fb-h-full fb-w-full fb-animate-pulse fb-items-center fb-justify-center fb-rounded-md fb-bg-slate-200" />
)}
{imgUrl && (
<img
key={imgUrl}
src={imgUrl}
alt={altText}
className="rounded-custom"
className="fb-rounded-custom"
onLoad={() => {
setIsLoading(false);
}}
/>
)}
{videoUrlWithParams && (
<div className="relative">
<div className="rounded-custom bg-black">
<div className="fb-relative">
<div className="fb-rounded-custom fb-bg-black">
<iframe
src={videoUrlWithParams}
title="Question Video"
frameborder="0"
className="rounded-custom aspect-video w-full"
className="fb-rounded-custom fb-aspect-video fb-w-full"
onLoad={() => setIsLoading(false)}
allow="accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"></iframe>
@@ -64,7 +64,7 @@ export const QuestionMedia = ({ imgUrl, videoUrl, altText = "Image" }: QuestionM
href={!!imgUrl ? imgUrl : parseVideoUrl(videoUrl ?? "")}
target="_blank"
rel="noreferrer"
className="absolute bottom-2 right-2 flex items-center gap-2 rounded-md bg-gray-800 bg-opacity-40 p-1.5 text-white opacity-0 backdrop-blur-lg transition duration-300 ease-in-out hover:bg-opacity-65 group-hover/image:opacity-100">
className="fb-absolute fb-bottom-2 fb-right-2 fb-flex fb-items-center fb-gap-2 fb-rounded-md fb-bg-gray-800 fb-bg-opacity-40 fb-p-1.5 fb-text-white fb-opacity-0 fb-backdrop-blur-lg fb-transition fb-duration-300 fb-ease-in-out hover:fb-bg-opacity-65 group-hover/image:fb-opacity-100">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"

View File

@@ -35,7 +35,7 @@ export const RedirectCountDown = ({ redirectUrl, isRedirectDisabled }: RedirectC
return (
<div>
<div className="bg-accent-bg text-subheading mt-10 rounded-md p-2 text-sm">
<div className="fb-bg-accent-bg fb-text-subheading fb-mt-10 fb-rounded-md fb-p-2 fb-text-sm">
<span>You&apos;re redirected in </span>
<span>{timeRemaining}</span>
</div>

View File

@@ -11,24 +11,24 @@ type ResponseErrorComponentProps = {
export const ResponseErrorComponent = ({ questions, responseData, onRetry }: ResponseErrorComponentProps) => {
return (
<div className={"flex flex-col bg-white p-4"}>
<span className={"mb-1.5 text-base font-bold leading-6 text-slate-900"}>
<div className={"fb-flex fb-flex-col fb-bg-white fb-p-4s"}>
<span className={"fb-mb-1.5 fb-text-base fb-font-bold fb-leading-6 fb-text-slate-900"}>
{"Your feedback is stuck :("}
</span>
<p className={"max-w-md text-sm font-normal leading-6 text-slate-600"}>
<p className={"fb-max-w-md fb-text-sm fb-font-normal fb-leading-6 fb-text-slate-600"}>
The servers cannot be reached at the moment.
<br />
Please retry now or try again later.
</p>
<div className={"mt-4 rounded-lg border border-slate-200 bg-slate-100 px-4 py-5"}>
<div className={"flex max-h-36 flex-1 flex-col space-y-3 overflow-y-scroll"}>
<div className={"fb-mt-4 fb-rounded-lg fb-border fb-border-slate-200 fb-bg-slate-100 fb-px-4 fb-py-5"}>
<div className={"fb-flex fb-max-h-36 fb-flex-1 fb-flex-col fb-space-y-3 fb-overflow-y-scroll"}>
{questions.map((question, index) => {
const response = responseData[question.id];
if (!response) return;
return (
<div className={"flex flex-col"}>
<span className={"text-sm leading-6 text-slate-900"}>{`Question ${index + 1}`}</span>
<span className={"mt-1 text-sm font-semibold leading-6 text-slate-900"}>
<div className={"fb-flex fb-flex-col"}>
<span className={"fb-text-sm fb-leading-6 fb-text-slate-900"}>{`Question ${index + 1}`}</span>
<span className={"fb-mt-1 fb-text-sm fb-font-semibold fb-leading-6 fb-text-slate-900"}>
{processResponseData(response)}
</span>
</div>
@@ -36,7 +36,7 @@ export const ResponseErrorComponent = ({ questions, responseData, onRetry }: Res
})}
</div>
</div>
<div className={"mt-4 flex flex-1 flex-row items-center justify-end space-x-2"}>
<div className={"fb-mt-4 fb-flex fb-flex-1 fb-flex-row fb-items-center fb-justify-end fb-space-x-2"}>
<SubmitButton tabIndex={2} buttonLabel="Retry" isLastQuestion={false} onClick={() => onRetry()} />
</div>
</div>

View File

@@ -7,7 +7,7 @@ export const Subheader = ({ subheader, questionId }: SubheaderProps) => {
return (
<p
htmlFor={questionId}
className="text-subheading block break-words text-sm font-normal leading-5"
className="fb-text-subheading fb-block fb-break-words fb-text-sm fb-font-normal fb-leading-5"
dir="auto">
{subheader}
</p>

View File

@@ -253,6 +253,7 @@ export const Survey = ({
if (questionIdx === -1) {
return (
<WelcomeCard
key="start"
headline={survey.welcomeCard.headline}
html={survey.welcomeCard.html}
fileUrl={survey.welcomeCard.fileUrl}
@@ -263,11 +264,13 @@ export const Survey = ({
responseCount={responseCount}
autoFocusEnabled={autoFocusEnabled}
replaceRecallInfo={replaceRecallInfo}
isCurrent={offset === 0}
/>
);
} else if (questionIdx === survey.questions.length) {
return (
<ThankYouCard
key="end"
headline={replaceRecallInfo(
getLocalizedValue(survey.thankYouCard.headline, selectedLanguage),
responseData
@@ -279,11 +282,13 @@ export const Survey = ({
isResponseSendingFinished={isResponseSendingFinished}
buttonLabel={getLocalizedValue(survey.thankYouCard.buttonLabel, selectedLanguage)}
buttonLink={survey.thankYouCard.buttonLink}
survey={survey}
imageUrl={survey.thankYouCard.imageUrl}
videoUrl={survey.thankYouCard.videoUrl}
redirectUrl={survey.redirectUrl}
isRedirectDisabled={isRedirectDisabled}
autoFocusEnabled={autoFocusEnabled}
isCurrent={offset === 0}
/>
);
} else {
@@ -291,6 +296,7 @@ export const Survey = ({
return (
question && (
<QuestionConditional
key={question.id}
surveyId={survey.id}
question={parseRecallInformation(question, selectedLanguage, responseData)}
value={responseData[question.id]}
@@ -317,11 +323,11 @@ export const Survey = ({
<AutoCloseWrapper survey={survey} onClose={onClose} offset={offset}>
<div
className={cn(
"no-scrollbar md:rounded-custom rounded-t-custom bg-survey-bg flex h-full w-full flex-col justify-between overflow-hidden transition-all duration-1000 ease-in-out",
"fb-no-scrollbar md:fb-rounded-custom fb-rounded-t-custom fb-bg-survey-bg fb-flex fb-h-full fb-w-full fb-flex-col fb-justify-between fb-overflow-hidden fb-transition-all fb-duration-1000 fb-ease-in-out",
cardArrangement === "simple" ? "fb-survey-shadow" : "",
offset === 0 || cardArrangement === "simple" ? "opacity-100" : "opacity-0"
offset === 0 || cardArrangement === "simple" ? "fb-opacity-100" : "fb-opacity-0"
)}>
<div className="flex h-6 justify-end pr-2 pt-2">
<div className="fb-flex fb-h-6 fb-justify-end fb-pr-2 fb-pt-2">
{getShowLanguageSwitch(offset) && (
<LanguageSwitch
surveyLanguages={survey.languages}
@@ -332,10 +338,13 @@ export const Survey = ({
</div>
<div
ref={contentRef}
className={cn(loadingElement ? "animate-pulse opacity-60" : "", fullSizeCards ? "" : "my-auto")}>
className={cn(
loadingElement ? "fb-animate-pulse fb-opacity-60" : "",
fullSizeCards ? "" : "fb-my-auto"
)}>
{content()}
</div>
<div className="mx-6 mb-10 mt-2 space-y-3 md:mb-6 md:mt-6">
<div className="fb-mx-6 fb-mb-10 fb-mt-2 fb-space-y-3 md:fb-mb-6 md:fb-mt-6">
{isBrandingEnabled && <FormbricksBranding />}
{showProgressBar && <ProgressBar survey={survey} questionId={questionId} />}
</div>

View File

@@ -4,13 +4,13 @@ interface SurveyCloseButtonProps {
export const SurveyCloseButton = ({ onClose }: SurveyCloseButtonProps) => {
return (
<div class="z-[1001] flex w-fit items-center even:border-l even:pl-1">
<div class="fb-z-[1001] fb-flex fb-w-fit fb-items-center even:fb-border-l even:fb-pl-1">
<button
type="button"
onClick={onClose}
class="text-heading relative h-5 w-5 rounded-md hover:bg-black/5 focus:outline-none focus:ring-2 focus:ring-offset-2">
class="fb-text-heading fb-relative fb-h-5 fb-w-5 fb-rounded-md hover:fb-bg-black/5 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
<svg
class="h-5 w-5 p-0.5"
class="fb-h-5 fb-w-5 fb-p-0.5"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1"

View File

@@ -3,7 +3,7 @@ import { Survey } from "./Survey";
export const SurveyInline = (props: SurveyInlineProps) => {
return (
<div id="fbjs" className="formbricks-form h-full w-full">
<div id="fbjs" className="fb-formbricks-form fb-h-full fb-w-full">
<Survey {...props} />
</div>
);

View File

@@ -36,7 +36,7 @@ export const SurveyModal = ({
const highlightBorderColor = styling?.highlightBorderColor?.light || null;
return (
<div id="fbjs" className="formbricks-form">
<div id="fbjs" className="fb-formbricks-form">
<Modal
placement={placement}
clickOutside={clickOutside}

View File

@@ -5,6 +5,8 @@ import { QuestionMedia } from "@/components/general/QuestionMedia";
import { RedirectCountDown } from "@/components/general/RedirectCountdown";
import { Subheader } from "@/components/general/Subheader";
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
import { useEffect } from "preact/hooks";
import { TSurvey } from "@formbricks/types/surveys";
interface ThankYouCardProps {
headline?: string;
@@ -17,6 +19,8 @@ interface ThankYouCardProps {
videoUrl?: string;
isResponseSendingFinished: boolean;
autoFocusEnabled: boolean;
isCurrent: boolean;
survey: TSurvey;
}
export const ThankYouCard = ({
@@ -30,30 +34,56 @@ export const ThankYouCard = ({
videoUrl,
isResponseSendingFinished,
autoFocusEnabled,
isCurrent,
survey,
}: ThankYouCardProps) => {
const media = imageUrl || videoUrl ? <QuestionMedia imgUrl={imageUrl} videoUrl={videoUrl} /> : null;
const checkmark = (
<div className="text-brand flex flex-col items-center justify-center">
<div className="fb-text-brand fb-flex fb-flex-col fb-items-center fb-justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
class="h-24 w-24">
class="fb-h-24 fb-w-24">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span className="bg-brand mb-[10px] inline-block h-1 w-16 rounded-[100%]"></span>
<span className="fb-bg-brand fb-mb-[10px] fb-inline-block fb-h-1 fb-w-16 fb-rounded-[100%]"></span>
</div>
);
const handleSubmit = () => {
if (buttonLink) window.location.replace(buttonLink);
};
useEffect(() => {
const handleEnter = (e: KeyboardEvent) => {
if (e.key === "Enter") {
handleSubmit();
}
};
if (isCurrent && survey.type === "link") {
document.addEventListener("keydown", handleEnter);
} else {
document.removeEventListener("keydown", handleEnter);
}
return () => {
document.removeEventListener("keydown", handleEnter);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCurrent]);
return (
<ScrollableContainer>
<div className="text-center">
<div className="fb-text-center">
{isResponseSendingFinished ? (
<>
{media || checkmark}
@@ -61,25 +91,22 @@ export const ThankYouCard = ({
<Subheader subheader={subheader} questionId="thankYouCard" />
<RedirectCountDown redirectUrl={redirectUrl} isRedirectDisabled={isRedirectDisabled} />
{buttonLabel && (
<div className="mt-6 flex w-full flex-col items-center justify-center space-y-4">
<div className="fb-mt-6 fb-flex fb-w-full fb-flex-col fb-items-center fb-justify-center fb-space-y-4">
<SubmitButton
buttonLabel={buttonLabel}
isLastQuestion={false}
focus={autoFocusEnabled}
onClick={() => {
if (!buttonLink) return;
window.location.replace(buttonLink);
}}
onClick={handleSubmit}
/>
</div>
)}
</>
) : (
<>
<div className="my-3">
<div className="fb-my-3">
<LoadingSpinner />
</div>
<h1 className="text-brand">Sending responses...</h1>
<h1 className="fb-text-brand">Sending responses...</h1>
</>
)}
</div>

View File

@@ -1,6 +1,7 @@
import { SubmitButton } from "@/components/buttons/SubmitButton";
import { ScrollableContainer } from "@/components/wrappers/ScrollableContainer";
import { calculateElementIdx } from "@/lib/utils";
import { useEffect } from "preact/hooks";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { TResponseData, TResponseTtc } from "@formbricks/types/responses";
import { TI18nString, TSurvey } from "@formbricks/types/surveys";
@@ -18,11 +19,12 @@ interface WelcomeCardProps {
responseCount?: number;
autoFocusEnabled: boolean;
replaceRecallInfo: (text: string, responseData: TResponseData) => string;
isCurrent: boolean;
}
const TimerIcon = () => {
return (
<div className="mr-1">
<div className="fb-mr-1">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
@@ -39,14 +41,14 @@ const TimerIcon = () => {
const UsersIcon = () => {
return (
<div className="mr-1">
<div className="fb-mr-1">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
class="h-4 w-4">
class="fb-h-4 fb-w-4">
<path
strokeLinecap="round"
strokeLinejoin="round"
@@ -68,6 +70,7 @@ export const WelcomeCard = ({
responseCount,
autoFocusEnabled,
replaceRecallInfo,
isCurrent,
}: WelcomeCardProps) => {
const calculateTimeToComplete = () => {
let idx = calculateElementIdx(survey, 0);
@@ -100,12 +103,40 @@ export const WelcomeCard = ({
const timeToFinish = survey.welcomeCard.timeToFinish;
const showResponseCount = survey.welcomeCard.showResponseCount;
const handleSubmit = () => {
onSubmit({ ["welcomeCard"]: "clicked" }, {});
};
useEffect(() => {
const handleEnter = (e: KeyboardEvent) => {
if (e.key === "Enter") {
handleSubmit();
}
};
if (isCurrent && survey.type === "link") {
document.addEventListener("keydown", handleEnter);
} else {
document.removeEventListener("keydown", handleEnter);
}
return () => {
document.removeEventListener("keydown", handleEnter);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCurrent]);
return (
<div>
<ScrollableContainer>
<div>
{fileUrl && (
<img src={fileUrl} className="mb-8 max-h-96 w-1/3 rounded-lg object-contain" alt="Company Logo" />
<img
src={fileUrl}
className="fb-mb-8 fb-max-h-96 fb-w-1/3 fb-rounded-lg fb-object-contain"
alt="Company Logo"
/>
)}
<Headline
@@ -119,36 +150,35 @@ export const WelcomeCard = ({
</div>
</ScrollableContainer>
<div className="mx-6 mt-4 flex gap-4 py-4">
<div className="fb-mx-6 fb-mt-4 fb-flex fb-gap-4 fb-py-4">
<SubmitButton
buttonLabel={getLocalizedValue(buttonLabel, languageCode)}
isLastQuestion={false}
focus={autoFocusEnabled}
onClick={() => {
onSubmit({ ["welcomeCard"]: "clicked" }, {});
}}
onClick={handleSubmit}
type="button"
onKeyDown={(e) => e.key === "Enter" && e.preventDefault()}
/>
</div>
{timeToFinish && !showResponseCount ? (
<div className="item-center text-subheading my-4 ml-6 flex">
<div className="fb-items-center fb-text-subheading fb-my-4 fb-ml-6 fb-flex">
<TimerIcon />
<p className="pt-1 text-xs">
<p className="fb-pt-1 fb-text-xs">
<span> Takes {calculateTimeToComplete()} </span>
</p>
</div>
) : showResponseCount && !timeToFinish && responseCount && responseCount > 3 ? (
<div className="item-center text-subheading my-4 ml-6 flex">
<div className="fb-items-center fb-text-subheading fb-my-4 fb-ml-6 fb-flex">
<UsersIcon />
<p className="pt-1 text-xs">
<p className="fb-pt-1 fb-text-xs">
<span>{`${responseCount} people responded`}</span>
</p>
</div>
) : timeToFinish && showResponseCount ? (
<div className="item-center text-subheading my-4 ml-6 flex">
<div className="fb-items-center fb-text-subheading fb-my-4 fb-ml-6 fb-flex">
<TimerIcon />
<p className="pt-1 text-xs">
<p className="fb-pt-1 fb-text-xs">
<span> Takes {calculateTimeToComplete()} </span>
<span>{responseCount && responseCount > 3 ? `${responseCount} people responded` : ""}</span>
</p>

View File

@@ -130,7 +130,7 @@ export const AddressQuestion = ({
);
return (
<form key={question.id} onSubmit={handleSubmit} className="w-full" ref={formRef}>
<form key={question.id} onSubmit={handleSubmit} className="fb-w-full" ref={formRef}>
<ScrollableContainer>
<div>
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
@@ -143,7 +143,7 @@ export const AddressQuestion = ({
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
questionId={question.id}
/>
<div className="mt-4 space-y-2">
<div className="fb-mt-4 fb-space-y-2">
{inputConfig.map(({ name, placeholder, required }, index) => (
<input
ref={index === 0 ? addressTextRef : null}
@@ -158,13 +158,13 @@ export const AddressQuestion = ({
value={safeValue[index] || ""}
onInput={(e) => handleInputChange(e.currentTarget.value, index)}
autoFocus={autoFocusEnabled && index === 0}
className="border-border focus:border-brand placeholder:text-placeholder text-subheading bg-input-bg rounded-custom block w-full border p-2 shadow-sm sm:text-sm"
className="fb-border-border focus:fb-border-brand placeholder:fb-text-placeholder fb-text-subheading fb-bg-input-bg fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm sm:fb-text-sm"
/>
))}
</div>
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
{!isFirstQuestion && (
<BackButton
tabIndex={8}

View File

@@ -57,7 +57,7 @@ export const CTAQuestion = ({
<HtmlBody htmlString={getLocalizedValue(question.html, languageCode)} questionId={question.id} />
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
{!isFirstQuestion && (
<BackButton
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}
@@ -69,7 +69,7 @@ export const CTAQuestion = ({
}}
/>
)}
<div className="flex w-full justify-end">
<div className="fb-flex fb-w-full fb-justify-end">
{!question.required && (
<button
dir="auto"
@@ -81,7 +81,7 @@ export const CTAQuestion = ({
onSubmit({ [question.id]: "dismissed" }, updatedTtcObj);
onChange({ [question.id]: "dismissed" });
}}
className="text-heading focus:ring-focus mr-4 flex items-center rounded-md px-3 py-3 text-base font-medium leading-4 hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2">
className="fb-text-heading focus:fb-ring-focus fb-mr-4 fb-flex fb-items-center fb-rounded-md fb-px-3 fb-py-3 fb-text-base fb-font-medium fb-leading-4 hover:fb-opacity-90 focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
{getLocalizedValue(question.dismissButtonLabel, languageCode) || "Skip"}
</button>
)}

View File

@@ -68,7 +68,7 @@ export const CalQuestion = ({
onChange({ [question.id]: value });
onSubmit({ [question.id]: value }, updatedttc);
}}
className="w-full">
className="fb-w-full">
<ScrollableContainer>
<div>
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
@@ -82,12 +82,12 @@ export const CalQuestion = ({
questionId={question.id}
/>
<>
{errorMessage && <span className="text-red-500">{errorMessage}</span>}
{errorMessage && <span className="fb-text-red-500">{errorMessage}</span>}
<CalEmbed key={question.id} question={question} onSuccessfulBooking={onSuccessfulBooking} />
</>
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
{!isFirstQuestion && (
<BackButton
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}

View File

@@ -64,7 +64,7 @@ export const ConsentQuestion = ({
htmlString={getLocalizedValue(question.html, languageCode) || ""}
questionId={question.id}
/>
<div className="bg-survey-bg sticky -bottom-2 z-10 w-full px-1 py-1">
<div className="fb-bg-survey-bg fb-sticky -fb-bottom-2 fb-z-10 fb-w-full fb-px-1 fb-py-1">
<label
dir="auto"
tabIndex={1}
@@ -77,7 +77,7 @@ export const ConsentQuestion = ({
document.getElementById(`${question.id}-label`)?.focus();
}
}}
className="border-border bg-input-bg text-heading hover:bg-input-bg-selected focus:bg-input-bg-selected focus:ring-brand rounded-custom relative z-10 my-2 flex w-full cursor-pointer items-center border p-4 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2">
className="fb-border-border fb-bg-input-bg fb-text-heading hover:fb-bg-input-bg-selected focus:fb-bg-input-bg-selected focus:fb-ring-brand fb-rounded-custom fb-relative fb-z-10 fb-my-2 fb-flex fb-w-full fb-cursor-pointer fb-items-center fb-border fb-p-4 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2">
<input
type="checkbox"
id={question.id}
@@ -91,11 +91,11 @@ export const ConsentQuestion = ({
}
}}
checked={value === "accepted"}
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
aria-labelledby={`${question.id}-label`}
required={question.required}
/>
<span id={`${question.id}-label`} className="ml-3 mr-3 font-medium">
<span id={`${question.id}-label`} className="fb-ml-3 fb-mr-3 fb-font-medium">
{getLocalizedValue(question.label, languageCode)}
</span>
</label>
@@ -103,7 +103,7 @@ export const ConsentQuestion = ({
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
{!isFirstQuestion && (
<BackButton
tabIndex={3}

View File

@@ -139,7 +139,7 @@ export const DateQuestion = ({
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value }, updatedTtcObj);
}}
className="w-full">
className="fb-w-full">
<ScrollableContainer>
<div>
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
@@ -152,13 +152,13 @@ export const DateQuestion = ({
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
questionId={question.id}
/>
<div className={"text-red-600"}>
<div className={"fb-text-red-600"}>
<span>{errorMessage}</span>
</div>
<div
className={cn("mt-4 w-full", errorMessage && "rounded-lg border-2 border-red-500")}
className={cn("fb-mt-4 fb-w-full", errorMessage && "fb-rounded-lg fb-border-2 fb-border-red-500")}
id="date-picker-root">
<div className="relative">
<div className="fb-relative">
{!datePickerOpen && (
<div
onClick={() => setDatePickerOpen(true)}
@@ -166,14 +166,14 @@ export const DateQuestion = ({
onKeyDown={(e) => {
if (e.key === " ") setDatePickerOpen(true);
}}
className="focus:outline-brand bg-input-bg hover:bg-input-bg-selected border-border text-heading rounded-custom relative flex h-[12dvh] w-full cursor-pointer appearance-none items-center justify-center border text-left text-base font-normal">
<div className="flex items-center gap-2">
className="focus:fb-outline-brand fb-bg-input-bg hover:fb-bg-input-bg-selected fb-border-border fb-text-heading fb-rounded-custom fb-relative fb-flex fb-h-[12dvh] fb-w-full fb-cursor-pointer fb-appearance-none fb-items-center fb-justify-center fb-border fb-text-left fb-text-base fb-font-normal">
<div className="fb-flex fb-items-center fb-gap-2">
{selectedDate ? (
<div className="flex items-center gap-2">
<div className="fb-flex fb-items-center fb-gap-2">
<CalendarCheckIcon /> <span>{formattedDate}</span>
</div>
) : (
<div className="flex items-center gap-2">
<div className="fb-flex fb-items-center fb-gap-2">
<CalendarIcon /> <span>Select a date</span>
</div>
)}
@@ -208,8 +208,8 @@ export const DateQuestion = ({
monthPlaceholder="MM"
yearPlaceholder="YYYY"
format={question.format ?? "M-d-y"}
className={`dp-input-root rounded-custom wrapper-hide ${!datePickerOpen ? "" : "h-[46dvh] sm:h-[34dvh]"} ${hideInvalid ? "hide-invalid" : ""} `}
calendarClassName="calendar-root !bg-input-bg border border-border rounded-custom p-3 h-[46dvh] sm:h-[33dvh] overflow-auto"
className={`dp-input-root fb-rounded-custom wrapper-hide ${!datePickerOpen ? "" : "fb-h-[46dvh] sm:fb-h-[34dvh]"} ${hideInvalid ? "hide-invalid" : ""} `}
calendarClassName="calendar-root !fb-bg-input-bg fb-border fb-border-border fb-rounded-custom fb-p-3 fb-h-[46dvh] sm:fb-h-[33dvh] fb-overflow-auto"
clearIcon={null}
onCalendarOpen={() => {
setDatePickerOpen(true);
@@ -223,14 +223,14 @@ export const DateQuestion = ({
calendarIcon={<CalendarIcon />}
tileClassName={({ date }: { date: Date }) => {
const baseClass =
"hover:bg-input-bg-selected rounded-custom h-9 p-0 mt-1 font-normal text-heading aria-selected:opacity-100 focus:ring-2 focus:bg-slate-200";
"hover:fb-bg-input-bg-selected fb-rounded-custom fb-h-9 fb-p-0 fb-mt-1 fb-font-normal fb-text-heading aria-selected:fb-opacity-100 focus:fb-ring-2 focus:fb-bg-slate-200";
// today's date class
if (
date.getDate() === new Date().getDate() &&
date.getMonth() === new Date().getMonth() &&
date.getFullYear() === new Date().getFullYear()
) {
return `${baseClass} !bg-brand !border-border-highlight !text-heading focus:ring-2 focus:bg-slate-200`;
return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-heading focus:fb-ring-2 focus:fb-bg-slate-200`;
}
// active date class
if (
@@ -238,7 +238,7 @@ export const DateQuestion = ({
date.getMonth() === selectedDate?.getMonth() &&
date.getFullYear() === selectedDate?.getFullYear()
) {
return `${baseClass} !bg-brand !border-border-highlight !text-heading`;
return `${baseClass} !fb-bg-brand !fb-border-border-highlight !fb-text-heading`;
}
return baseClass;
@@ -253,7 +253,7 @@ export const DateQuestion = ({
</div>
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
<div>
{!isFirstQuestion && (
<BackButton

View File

@@ -69,7 +69,7 @@ export const FileUploadQuestion = ({
}
}
}}
className="w-full">
className="fb-w-full">
<ScrollableContainer>
<div>
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
@@ -102,7 +102,7 @@ export const FileUploadQuestion = ({
/>
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
{!isFirstQuestion && (
<BackButton
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}

View File

@@ -84,7 +84,10 @@ export const MatrixQuestion = ({
const columnsHeaders = useMemo(
() =>
question.columns.map((column, index) => (
<th key={index} className="text-heading max-w-40 break-words px-4 py-2 font-normal" dir="auto">
<th
key={index}
className="fb-text-heading fb-max-w-40 fb-break-words fb-px-4 fb-py-2 fb-font-normal"
dir="auto">
{getLocalizedValue(column, languageCode)}
</th>
)),
@@ -92,7 +95,7 @@ export const MatrixQuestion = ({
);
return (
<form key={question.id} onSubmit={handleSubmit} className="w-full">
<form key={question.id} onSubmit={handleSubmit} className="fb-w-full">
<ScrollableContainer>
<div>
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
@@ -105,11 +108,11 @@ export const MatrixQuestion = ({
subheader={getLocalizedValue(question.subheader, languageCode)}
questionId={question.id}
/>
<div className="overflow-x-auto py-4">
<table className="no-scrollbar min-w-full table-auto border-collapse text-sm">
<div className="fb-overflow-x-auto fb-py-4">
<table className="fb-no-scrollbar fb-min-w-full fb-table-auto fb-border-collapse fb-text-sm">
<thead>
<tr>
<th className="px-4 py-2"></th>
<th className="fb-px-4 fb-py-2"></th>
{columnsHeaders}
</tr>
</thead>
@@ -117,14 +120,16 @@ export const MatrixQuestion = ({
{question.rows.map((row, rowIndex) => (
// Table rows
<tr className={`${rowIndex % 2 === 0 ? "bg-input-bg" : ""}`}>
<td className="text-heading rounded-l-custom max-w-40 break-words px-4 py-2" dir="auto">
<td
className="fb-text-heading fb-rounded-l-custom fb-max-w-40 fb-break-words fb-px-4 fb-py-2"
dir="auto">
{getLocalizedValue(row, languageCode)}
</td>
{question.columns.map((column, columnIndex) => (
<td
key={columnIndex}
tabIndex={0}
className={`outline-brand px-4 py-2 text-gray-800 ${columnIndex === question.columns.length - 1 ? "rounded-r-custom" : ""}`}
className={`fb-outline-brand fb-px-4 fb-py-2 fb-text-gray-800 ${columnIndex === question.columns.length - 1 ? "fb-rounded-r-custom" : ""}`}
onClick={() =>
handleSelect(
getLocalizedValue(column, languageCode),
@@ -141,7 +146,7 @@ export const MatrixQuestion = ({
}
}}
dir="auto">
<div className="flex items-center justify-center p-2">
<div className="fb-flex fb-items-center fb-justify-center fb-p-2">
{/* radio input */}
<input
dir="auto"
@@ -156,7 +161,7 @@ export const MatrixQuestion = ({
getLocalizedValue(column, languageCode)
: false
}
className="border-brand text-brand h-5 w-5 border focus:ring-0 focus:ring-offset-0"
className="fb-border-brand fb-text-brand fb-h-5 fb-w-5 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
/>
</div>
</td>
@@ -168,7 +173,7 @@ export const MatrixQuestion = ({
</div>
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
{!isFirstQuestion && (
<BackButton
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}

View File

@@ -143,7 +143,7 @@ export const MultipleChoiceMultiQuestion = ({
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value }, updatedTtcObj);
}}
className="w-full">
className="fb-w-full">
<ScrollableContainer>
<div>
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
@@ -156,10 +156,10 @@ export const MultipleChoiceMultiQuestion = ({
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
questionId={question.id}
/>
<div className="mt-4">
<div className="fb-mt-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="bg-survey-bg relative space-y-2" ref={choicesContainerRef}>
<legend className="fb-sr-only">Options</legend>
<div className="fb-bg-survey-bg fb-relative fb-space-y-2" ref={choicesContainerRef}>
{questionChoices.map((choice, idx) => {
if (!choice || choice.id === "other") return;
return (
@@ -168,9 +168,9 @@ export const MultipleChoiceMultiQuestion = ({
tabIndex={idx + 1}
className={cn(
value.includes(getLocalizedValue(choice.label, languageCode))
? "border-border bg-input-selected-bg z-10"
: "border-border",
"text-heading bg-input-bg focus-within:border-brand hover:bg-input-bg-selected focus:bg-input-bg-selected rounded-custom relative flex cursor-pointer flex-col border p-4 focus:outline-none"
? "fb-border-brand fb-bg-input-bg-selected fb-z-10"
: "fb-border-border",
"fb-text-heading fb-bg-input-bg focus-within:fb-border-brand hover:fb-bg-input-bg-selected focus:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
)}
onKeyDown={(e) => {
// Accessibility: if spacebar was pressed pass this down to the input
@@ -181,14 +181,14 @@ export const MultipleChoiceMultiQuestion = ({
}
}}
autoFocus={idx === 0 && autoFocusEnabled}>
<span className="flex items-center text-sm" dir="auto">
<span className="fb-flex fb-items-center fb-text-sm" dir="auto">
<input
type="checkbox"
id={choice.id}
name={question.id}
tabIndex={-1}
value={getLocalizedValue(choice.label, languageCode)}
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
aria-labelledby={`${choice.id}-label`}
onChange={(e) => {
if ((e.target as HTMLInputElement)?.checked) {
@@ -207,7 +207,7 @@ export const MultipleChoiceMultiQuestion = ({
: question.required
}
/>
<span id={`${choice.id}-label`} className="ml-3 mr-3 grow font-medium">
<span id={`${choice.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
{getLocalizedValue(choice.label, languageCode)}
</span>
</span>
@@ -219,9 +219,9 @@ export const MultipleChoiceMultiQuestion = ({
tabIndex={questionChoices.length + 1}
className={cn(
value.includes(getLocalizedValue(otherOption.label, languageCode))
? "border-border bg-input-selected-bg z-10"
: "border-border",
"text-heading focus-within:border-brand bg-input-bg focus-within:bg-input-bg-selected hover:bg-input-bg-selected rounded-custom relative flex cursor-pointer flex-col border p-4 focus:outline-none"
? "fb-border-brand fb-bg-input-bg-selected fb-z-10"
: "fb-border-border",
"fb-text-heading focus-within:fb-border-brand fb-bg-input-bg focus-within:fb-bg-input-bg-selected hover:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
)}
onKeyDown={(e) => {
// Accessibility: if spacebar was pressed pass this down to the input
@@ -231,14 +231,14 @@ export const MultipleChoiceMultiQuestion = ({
document.getElementById(otherOption.id)?.focus();
}
}}>
<span className="flex items-center text-sm" dir="auto">
<span className="fb-flex fb-items-center fb-text-sm" dir="auto">
<input
type="checkbox"
tabIndex={-1}
id={otherOption.id}
name={question.id}
value={getLocalizedValue(otherOption.label, languageCode)}
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
aria-labelledby={`${otherOption.id}-label`}
onChange={() => {
setOtherSelected(!otherSelected);
@@ -250,7 +250,7 @@ export const MultipleChoiceMultiQuestion = ({
}}
checked={otherSelected}
/>
<span id={`${otherOption.id}-label`} className="ml-3 mr-3 grow font-medium">
<span id={`${otherOption.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
{getLocalizedValue(otherOption.label, languageCode)}
</span>
</span>
@@ -266,7 +266,7 @@ export const MultipleChoiceMultiQuestion = ({
setOtherValue(e.currentTarget.value);
addItem(e.currentTarget.value);
}}
className="placeholder:text-placeholder border-border bg-survey-bg text-heading focus:ring-focus rounded-custom mt-3 flex h-10 w-full border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
className="placeholder:fb-text-placeholder fb-border-border fb-bg-survey-bg fb-text-heading focus:fb-ring-focus fb-rounded-custom fb-mt-3 fb-flex fb-h-10 fb-w-full fb-border fb-px-3 fb-py-2 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50"
placeholder={
getLocalizedValue(question.otherOptionPlaceholder, languageCode) ?? "Please specify"
}
@@ -282,7 +282,7 @@ export const MultipleChoiceMultiQuestion = ({
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
{!isFirstQuestion && (
<BackButton
tabIndex={questionChoices.length + 3}

View File

@@ -106,7 +106,7 @@ export const MultipleChoiceSingleQuestion = ({
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value ?? "" }, updatedTtcObj);
}}
className="w-full">
className="fb-w-full">
<ScrollableContainer>
<div>
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
@@ -119,11 +119,14 @@ export const MultipleChoiceSingleQuestion = ({
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
questionId={question.id}
/>
<div className="mt-4">
<div className="fb-mt-4">
<fieldset>
<legend className="sr-only">Options</legend>
<legend className="fb-sr-only">Options</legend>
<div className="bg-survey-bg relative space-y-2" role="radiogroup" ref={choicesContainerRef}>
<div
className="fb-bg-survey-bg fb-relative fb-space-y-2"
role="radiogroup"
ref={choicesContainerRef}>
{questionChoices.map((choice, idx) => {
if (!choice || choice.id === "other") return;
return (
@@ -133,9 +136,9 @@ export const MultipleChoiceSingleQuestion = ({
key={choice.id}
className={cn(
value === getLocalizedValue(choice.label, languageCode)
? "border-brand z-10"
: "border-border",
"text-heading bg-input-bg focus-within:border-brand focus-within:bg-input-bg-selected hover:bg-input-bg-selected rounded-custom relative flex cursor-pointer flex-col border p-4 focus:outline-none"
? "fb-border-brand fb-bg-input-bg-selected fb-z-10"
: "fb-border-border",
"fb-text-heading fb-bg-input-bg focus-within:fb-border-brand focus-within:fb-bg-input-bg-selected hover:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
)}
onKeyDown={(e) => {
// Accessibility: if spacebar was pressed pass this down to the input
@@ -146,7 +149,7 @@ export const MultipleChoiceSingleQuestion = ({
}
}}
autoFocus={idx === 0 && autoFocusEnabled}>
<span className="flex items-center text-sm">
<span className="fb-flex fb-items-center fb-text-sm">
<input
tabIndex={-1}
type="radio"
@@ -154,7 +157,7 @@ export const MultipleChoiceSingleQuestion = ({
name={question.id}
value={getLocalizedValue(choice.label, languageCode)}
dir="auto"
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
aria-labelledby={`${choice.id}-label`}
onChange={() => {
setOtherSelected(false);
@@ -163,7 +166,7 @@ export const MultipleChoiceSingleQuestion = ({
checked={value === getLocalizedValue(choice.label, languageCode)}
required={question.required && idx === 0}
/>
<span id={`${choice.id}-label`} className="ml-3 mr-3 grow font-medium">
<span id={`${choice.id}-label`} className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium">
{getLocalizedValue(choice.label, languageCode)}
</span>
</span>
@@ -176,9 +179,9 @@ export const MultipleChoiceSingleQuestion = ({
tabIndex={questionChoices.length + 1}
className={cn(
value === getLocalizedValue(otherOption.label, languageCode)
? "border-border bg-input-bg-selected z-10"
: "border-border",
"text-heading focus-within:border-brand bg-input-bg focus-within:bg-input-bg-selected hover:bg-input-bg-selected rounded-custom relative flex cursor-pointer flex-col border p-4 focus:outline-none"
? "fb-border-brand fb-bg-input-bg-selected fb-z-10"
: "fb-border-border",
"fb-text-heading focus-within:fb-border-brand fb-bg-input-bg focus-within:fb-bg-input-bg-selected hover:fb-bg-input-bg-selected fb-rounded-custom fb-relative fb-flex fb-cursor-pointer fb-flex-col fb-border fb-p-4 focus:fb-outline-none"
)}
onKeyDown={(e) => {
// Accessibility: if spacebar was pressed pass this down to the input
@@ -188,7 +191,7 @@ export const MultipleChoiceSingleQuestion = ({
document.getElementById(otherOption.id)?.focus();
}
}}>
<span className="flex items-center text-sm">
<span className="fb-flex fb-items-center fb-text-sm">
<input
dir="auto"
type="radio"
@@ -196,7 +199,7 @@ export const MultipleChoiceSingleQuestion = ({
tabIndex={-1}
name={question.id}
value={getLocalizedValue(otherOption.label, languageCode)}
className="border-brand text-brand h-4 w-4 border focus:ring-0 focus:ring-offset-0"
className="fb-border-brand fb-text-brand fb-h-4 fb-w-4 fb-border focus:fb-ring-0 focus:fb-ring-offset-0"
aria-labelledby={`${otherOption.id}-label`}
onChange={() => {
setOtherSelected(!otherSelected);
@@ -204,7 +207,10 @@ export const MultipleChoiceSingleQuestion = ({
}}
checked={otherSelected}
/>
<span id={`${otherOption.id}-label`} className="ml-3 mr-3 grow font-medium" dir="auto">
<span
id={`${otherOption.id}-label`}
className="fb-ml-3 fb-mr-3 fb-grow fb-font-medium"
dir="auto">
{getLocalizedValue(otherOption.label, languageCode)}
</span>
</span>
@@ -219,7 +225,7 @@ export const MultipleChoiceSingleQuestion = ({
onChange={(e) => {
onChange({ [question.id]: e.currentTarget.value });
}}
className="placeholder:text-placeholder border-border bg-survey-bg text-heading focus:ring-focus rounded-custom mt-3 flex h-10 w-full border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
className="placeholder:fb-text-placeholder fb-border-border fb-bg-survey-bg fb-text-heading focus:fb-ring-focus fb-rounded-custom fb-mt-3 fb-flex fb-h-10 fb-w-full fb-border fb-px-3 fb-py-2 fb-text-sm focus:fb-outline-none focus:fb-ring-2 focus:fb-ring-offset-2 disabled:fb-cursor-not-allowed disabled:fb-opacity-50"
placeholder={
getLocalizedValue(question.otherOptionPlaceholder, languageCode) ?? "Please specify"
}
@@ -234,7 +240,7 @@ export const MultipleChoiceSingleQuestion = ({
</div>
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
{!isFirstQuestion && (
<BackButton
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}

View File

@@ -60,7 +60,7 @@ export const NPSQuestion = ({
};
const getNPSOptionColor = (idx: number) => {
return idx > 8 ? "bg-emerald-100" : idx > 6 ? "bg-orange-100" : "bg-rose-100";
return idx > 8 ? "fb-bg-emerald-100" : idx > 6 ? "fb-bg-orange-100" : "fb-bg-rose-100";
};
return (
@@ -84,10 +84,10 @@ export const NPSQuestion = ({
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
questionId={question.id}
/>
<div className="my-4">
<div className="fb-my-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="flex">
<legend className="fb-sr-only">Options</legend>
<div className="fb-flex">
{Array.from({ length: 11 }, (_, i) => i).map((number, idx) => {
return (
<label
@@ -105,13 +105,16 @@ export const NPSQuestion = ({
}}
className={cn(
value === number
? "border-border-highlight bg-accent-selected-bg z-10 border"
: "border-border",
"text-heading first:rounded-l-custom last:rounded-r-custom focus:border-brand relative h-10 flex-1 cursor-pointer overflow-hidden border-b border-l border-t text-center text-sm leading-10 last:border-r focus:border-2 focus:outline-none",
hoveredNumber === number ? "bg-accent-bg" : ""
? "fb-border-border-highlight fb-bg-accent-selected-bg fb-z-10 fb-border"
: "fb-border-border",
"fb-text-heading first:fb-rounded-l-custom last:fb-rounded-r-custom focus:fb-border-brand fb-relative fb-h-10 fb-flex-1 fb-cursor-pointer fb-overflow-hidden fb-border-b fb-border-l fb-border-t fb-text-center fb-text-sm fb-leading-10 last:fb-border-r focus:fb-border-2 focus:fb-outline-none",
question.isColorCodingEnabled ? "fb-h-[46px]" : "fb-h-10",
hoveredNumber === number ? "fb-bg-accent-bg" : ""
)}>
{question.isColorCodingEnabled && (
<div className={`absolute left-0 top-0 h-[6px] w-full ${getNPSOptionColor(idx)}`} />
<div
className={`fb-absolute fb-left-0 fb-top-0 fb-h-[6px] fb-w-full ${getNPSOptionColor(idx)}`}
/>
)}
<input
type="radio"
@@ -119,7 +122,7 @@ export const NPSQuestion = ({
name="nps"
value={number}
checked={value === number}
className="absolute left-0 h-full w-full cursor-pointer opacity-0"
className="fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
onClick={() => handleClick(number)}
required={question.required}
/>
@@ -128,7 +131,7 @@ export const NPSQuestion = ({
);
})}
</div>
<div className="text-subheading mt-2 flex justify-between px-1.5 text-xs leading-6">
<div className="fb-text-subheading fb-mt-2 fb-flex fb-justify-between fb-px-1.5 fb-text-xs fb-leading-6">
<p dir="auto">{getLocalizedValue(question.lowerLabel, languageCode)}</p>
<p dir="auto">{getLocalizedValue(question.upperLabel, languageCode)}</p>
</div>
@@ -136,7 +139,7 @@ export const NPSQuestion = ({
</div>
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
{!isFirstQuestion && (
<BackButton
tabIndex={isLastQuestion ? 12 : 13}

View File

@@ -78,7 +78,7 @@ export const OpenTextQuestion = ({
setTtc(updatedttc);
onSubmit({ [question.id]: value }, updatedttc);
}}
className="w-full">
className="fb-w-full">
<ScrollableContainer>
<div>
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
@@ -91,7 +91,7 @@ export const OpenTextQuestion = ({
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
questionId={question.id}
/>
<div className="mt-4">
<div className="fb-mt-4">
{question.longAnswer === false ? (
<input
ref={openTextRef}
@@ -106,7 +106,7 @@ export const OpenTextQuestion = ({
type={question.inputType}
onInput={(e) => handleInputChange(e.currentTarget.value)}
autoFocus={autoFocusEnabled}
className="border-border placeholder:text-placeholder text-subheading focus:border-brand bg-input-bg rounded-custom block w-full border p-2 shadow-sm focus:outline-none focus:ring-0 sm:text-sm"
className="fb-border-border placeholder:fb-text-placeholder fb-text-subheading focus:fb-border-brand fb-bg-input-bg fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-outline-none focus:fb-ring-0 sm:fb-text-sm"
pattern={question.inputType === "phone" ? "[0-9+ ]+" : ".*"}
title={question.inputType === "phone" ? "Enter a valid phone number" : undefined}
/>
@@ -128,7 +128,7 @@ export const OpenTextQuestion = ({
handleInputResize(e);
}}
autoFocus={autoFocusEnabled}
className="border-border placeholder:text-placeholder bg-input-bg text-subheading focus:border-brand rounded-custom block w-full border p-2 shadow-sm focus:ring-0 sm:text-sm"
className="fb-border-border placeholder:fb-text-placeholder fb-bg-input-bg fb-text-subheading focus:fb-border-brand fb-rounded-custom fb-block fb-w-full fb-border fb-p-2 fb-shadow-sm focus:fb-ring-0 sm:fb-text-sm"
pattern={question.inputType === "phone" ? "[+][0-9 ]+" : ".*"}
title={question.inputType === "phone" ? "Please enter a valid phone number" : undefined}
/>
@@ -136,7 +136,7 @@ export const OpenTextQuestion = ({
</div>
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
{!isFirstQuestion && (
<BackButton
backButtonLabel={getLocalizedValue(question.backButtonLabel, languageCode)}

View File

@@ -94,7 +94,7 @@ export const PictureSelectionQuestion = ({
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value }, updatedTtcObj);
}}
className="w-full">
className="fb-w-full">
<ScrollableContainer>
<div>
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
@@ -107,10 +107,10 @@ export const PictureSelectionQuestion = ({
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
questionId={question.id}
/>
<div className="mt-4">
<div className="fb-mt-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="bg-survey-bg relative grid grid-cols-2 gap-x-5 gap-y-4">
<legend className="fb-sr-only">Options</legend>
<div className="fb-bg-survey-bg fb-relative fb-grid fb-grid-cols-2 fb-gap-x-5 fb-gap-y-4">
{questionChoices.map((choice, idx) => (
<label
key={choice.id}
@@ -127,15 +127,15 @@ export const PictureSelectionQuestion = ({
onClick={() => handleChange(choice.id)}
className={cn(
Array.isArray(value) && value.includes(choice.id)
? `border-brand text-brand z-10 border-4 shadow-xl`
? `fb-border-brand fb-text-brand fb-z-10 fb-border-4 fb-shadow-xl`
: "",
"focus:border-brand group/image rounded-custom relative inline-block h-28 w-full cursor-pointer overflow-hidden border focus:border-4 focus:outline-none"
"focus:fb-border-brand fb-rounded-custom fb-relative fb-inline-block fb-h-28 fb-w-full fb-cursor-pointer fb-overflow-hidden fb-border focus:fb-border-4 focus:fb-outline-none"
)}>
<img
src={choice.imageUrl}
id={choice.id}
alt={choice.imageUrl.split("/").pop()}
className="h-full w-full object-cover"
className="fb-h-full fb-w-full fb-object-cover"
/>
<a
tabIndex={-1}
@@ -144,7 +144,7 @@ export const PictureSelectionQuestion = ({
title="Open in new tab"
rel="noreferrer"
onClick={(e) => e.stopPropagation()}
className="absolute bottom-2 right-2 flex items-center gap-2 whitespace-nowrap rounded-md bg-gray-800 bg-opacity-40 p-1.5 text-white opacity-0 backdrop-blur-lg transition duration-300 ease-in-out hover:bg-opacity-65 group-hover/image:opacity-100">
className="fb-absolute fb-bottom-2 fb-right-2 fb-flex fb-items-center fb-gap-2 fb-whitespace-nowrap fb-rounded-md fb-bg-gray-800 fb-bg-opacity-40 fb-p-1.5 fb-text-white fb-opacity-0 fb-backdrop-blur-lg fb-transition fb-duration-300 fb-ease-in-out hover:fb-bg-opacity-65 group-hover/image:fb-opacity-100">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
@@ -170,8 +170,8 @@ export const PictureSelectionQuestion = ({
tabIndex={-1}
checked={value.includes(choice.id)}
className={cn(
"border-border rounded-custom pointer-events-none absolute right-2 top-2 z-20 h-5 w-5 border",
value.includes(choice.id) ? "border-brand text-brand" : ""
"fb-border-border fb-rounded-custom fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-border",
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
)}
required={question.required && value.length ? false : question.required}
/>
@@ -183,8 +183,8 @@ export const PictureSelectionQuestion = ({
tabIndex={-1}
checked={value.includes(choice.id)}
className={cn(
"border-border pointer-events-none absolute right-2 top-2 z-20 h-5 w-5 rounded-full border",
value.includes(choice.id) ? "border-brand text-brand" : ""
"fb-border-border fb-pointer-events-none fb-absolute fb-right-2 fb-top-2 fb-z-20 fb-h-5 fb-w-5 fb-rounded-full fb-border",
value.includes(choice.id) ? "fb-border-brand fb-text-brand" : ""
)}
required={question.required && value.length ? false : question.required}
/>
@@ -196,7 +196,7 @@ export const PictureSelectionQuestion = ({
</div>
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
{!isFirstQuestion && (
<BackButton
tabIndex={questionChoices.length + 3}

View File

@@ -77,7 +77,7 @@ export const RatingQuestion = ({
id={id}
name="rating"
value={number}
className="invisible absolute left-0 h-full w-full cursor-pointer opacity-0"
className="fb-invisible fb-absolute fb-left-0 fb-h-full fb-w-full fb-cursor-pointer fb-opacity-0"
onClick={() => handleSelect(number)}
required={question.required}
checked={value === number}
@@ -90,17 +90,17 @@ export const RatingQuestion = ({
const getRatingNumberOptionColor = (range: number, idx: number) => {
if (range > 5) {
if (range - idx < 2) return "bg-emerald-100";
if (range - idx < 4) return "bg-orange-100";
return "bg-rose-100";
if (range - idx < 2) return "fb-bg-emerald-100";
if (range - idx < 4) return "fb-bg-orange-100";
return "fb-bg-rose-100";
} else if (range < 5) {
if (range - idx < 1) return "bg-emerald-100";
if (range - idx < 2) return "bg-orange-100";
return "bg-rose-100";
if (range - idx < 1) return "fb-bg-emerald-100";
if (range - idx < 2) return "fb-bg-orange-100";
return "fb-bg-rose-100";
} else {
if (range - idx < 2) return "bg-emerald-100";
if (range - idx < 3) return "bg-orange-100";
return "bg-rose-100";
if (range - idx < 2) return "fb-bg-emerald-100";
if (range - idx < 3) return "fb-bg-orange-100";
return "fb-bg-rose-100";
}
};
@@ -113,7 +113,7 @@ export const RatingQuestion = ({
setTtc(updatedTtcObj);
onSubmit({ [question.id]: value ?? "" }, updatedTtcObj);
}}
className="w-full">
className="fb-w-full">
<ScrollableContainer>
<div>
{isMediaAvailable && <QuestionMedia imgUrl={question.imageUrl} videoUrl={question.videoUrl} />}
@@ -126,16 +126,16 @@ export const RatingQuestion = ({
subheader={question.subheader ? getLocalizedValue(question.subheader, languageCode) : ""}
questionId={question.id}
/>
<div className="mb-4 mt-6 flex items-center justify-center">
<fieldset className="w-full">
<legend className="sr-only">Choices</legend>
<div className="flex w-full">
<div className="fb-mb-4 fb-mt-6 fb-flex fb-items-center fb-justify-center">
<fieldset className="fb-w-full">
<legend className="fb-sr-only">Choices</legend>
<div className="fb-flex fb-w-full">
{Array.from({ length: question.range }, (_, i) => i + 1).map((number, i, a) => (
<span
key={number}
onMouseOver={() => setHoveredNumber(number)}
onMouseLeave={() => setHoveredNumber(0)}
className="bg-survey-bg flex-1 text-center text-sm">
className="fb-bg-survey-bg fb-flex-1 fb-text-center fb-text-sm">
{question.scale === "number" ? (
<label
tabIndex={i + 1}
@@ -149,17 +149,17 @@ export const RatingQuestion = ({
}}
className={cn(
value === number
? "bg-accent-selected-bg border-border-highlight z-10 border"
: "border-border",
a.length === number ? "rounded-r-custom border-r" : "",
number === 1 ? "rounded-l-custom" : "",
hoveredNumber === number ? "bg-accent-bg" : "",
question.isColorCodingEnabled ? "min-h-[47px]" : "min-h-[41px]",
"text-heading focus:border-brand relative flex w-full cursor-pointer items-center justify-center overflow-hidden border-b border-l border-t focus:border-2 focus:outline-none"
? "fb-bg-accent-selected-bg fb-border-border-highlight fb-z-10 fb-border"
: "fb-border-border",
a.length === number ? "fb-rounded-r-custom fb-border-r" : "",
number === 1 ? "fb-rounded-l-custom" : "",
hoveredNumber === number ? "fb-bg-accent-bg" : "",
question.isColorCodingEnabled ? "fb-min-h-[47px]" : "fb-min-h-[41px]",
"fb-text-heading focus:fb-border-brand fb-relative fb-flex fb-w-full fb-cursor-pointer fb-items-center fb-justify-center fb-overflow-hidden fb-border-b fb-border-l fb-border-t focus:fb-border-2 focus:fb-outline-none"
)}>
{question.isColorCodingEnabled && (
<div
className={`absolute left-0 top-0 h-[6px] w-full ${getRatingNumberOptionColor(question.range, number)}`}
className={`fb-absolute fb-left-0 fb-top-0 fb-h-[6px] fb-w-full ${getRatingNumberOptionColor(question.range, number)}`}
/>
)}
<HiddenRadioInput number={number} id={number.toString()} />
@@ -178,15 +178,15 @@ export const RatingQuestion = ({
}}
className={cn(
number <= hoveredNumber || number <= (value as number)
? "text-amber-400"
: "text-[#8696AC]",
hoveredNumber === number ? "text-amber-400" : "",
"relative flex max-h-16 min-h-9 cursor-pointer justify-center focus:outline-none"
? "fb-text-amber-400"
: "fb-text-[#8696AC]",
hoveredNumber === number ? "fb-text-amber-400" : "",
"fb-relative fb-flex fb-max-h-16 fb-min-h-9 fb-cursor-pointer fb-justify-center focus:fb-outline-none"
)}
onFocus={() => setHoveredNumber(number)}
onBlur={() => setHoveredNumber(0)}>
<HiddenRadioInput number={number} id={number.toString()} />
<div className="h-full w-full max-w-[74px] object-contain">
<div className="fb-h-full fb-w-full fb-max-w-[74px] fb-object-contain">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path
fillRule="evenodd"
@@ -198,10 +198,10 @@ export const RatingQuestion = ({
) : (
<label
className={cn(
"relative flex max-h-16 min-h-9 w-full cursor-pointer justify-center",
"fb-relative fb-flex fb-max-h-16 fb-min-h-9 fb-w-full fb-cursor-pointer fb-justify-center",
value === number || hoveredNumber === number
? "stroke-rating-selected text-rating-selected"
: "stroke-heading text-heading focus:border-accent-bg focus:border-2 focus:outline-none"
? "fb-stroke-rating-selected fb-text-rating-selected"
: "fb-stroke-heading fb-text-heading focus:fb-border-accent-bg focus:fb-border-2 focus:fb-outline-none"
)}
tabIndex={i + 1}
onKeyDown={(e) => {
@@ -215,7 +215,7 @@ export const RatingQuestion = ({
onFocus={() => setHoveredNumber(number)}
onBlur={() => setHoveredNumber(0)}>
<HiddenRadioInput number={number} id={number.toString()} />
<div className={cn("h-full w-full max-w-[74px] object-contain")}>
<div className={cn("fb-h-full fb-w-full fb-max-w-[74px] fb-object-contain")}>
<RatingSmiley
active={value === number || hoveredNumber === number}
idx={i}
@@ -228,11 +228,11 @@ export const RatingQuestion = ({
</span>
))}
</div>
<div className="text-subheading mt-4 flex justify-between px-1.5 text-xs leading-6">
<p className="w-1/2 text-left" dir="auto">
<div className="fb-text-subheading fb-mt-4 fb-flex fb-justify-between fb-px-1.5 fb-text-xs fb-leading-6">
<p className="fb-w-1/2 fb-text-left" dir="auto">
{getLocalizedValue(question.lowerLabel, languageCode)}
</p>
<p className="w-1/2 text-right" dir="auto">
<p className="fb-w-1/2 fb-text-right" dir="auto">
{getLocalizedValue(question.upperLabel, languageCode)}
</p>
</div>
@@ -240,7 +240,7 @@ export const RatingQuestion = ({
</div>
</div>
</ScrollableContainer>
<div className="flex w-full justify-between px-6 py-4">
<div className="fb-flex fb-w-full fb-justify-between fb-px-6 fb-py-4">
{!isFirstQuestion && (
<BackButton
tabIndex={!question.required || value ? question.range + 2 : question.range + 1}
@@ -274,39 +274,39 @@ interface RatingSmileyProps {
const getSmileyColor = (range: number, idx: number) => {
if (range > 5) {
if (range - idx < 3) return "fill-emerald-100";
if (range - idx < 5) return "fill-orange-100";
return "fill-rose-100";
if (range - idx < 3) return "fb-fill-emerald-100";
if (range - idx < 5) return "fb-fill-orange-100";
return "fb-fill-rose-100";
} else if (range < 5) {
if (range - idx < 2) return "fill-emerald-100";
if (range - idx < 3) return "fill-orange-100";
return "fill-rose-100";
if (range - idx < 2) return "fb-fill-emerald-100";
if (range - idx < 3) return "fb-fill-orange-100";
return "fb-fill-rose-100";
} else {
if (range - idx < 3) return "fill-emerald-100";
if (range - idx < 4) return "fill-orange-100";
return "fill-rose-100";
if (range - idx < 3) return "fb-fill-emerald-100";
if (range - idx < 4) return "fb-fill-orange-100";
return "fb-fill-rose-100";
}
};
const getActiveSmileyColor = (range: number, idx: number) => {
if (range > 5) {
if (range - idx < 3) return "fill-emerald-300";
if (range - idx < 5) return "fill-orange-300";
return "fill-rose-300";
if (range - idx < 3) return "fb-fill-emerald-300";
if (range - idx < 5) return "fb-fill-orange-300";
return "fb-fill-rose-300";
} else if (range < 5) {
if (range - idx < 2) return "fill-emerald-300";
if (range - idx < 3) return "fill-orange-300";
return "fill-rose-300";
if (range - idx < 2) return "fb-fill-emerald-300";
if (range - idx < 3) return "fb-fill-orange-300";
return "fb-fill-rose-300";
} else {
if (range - idx < 3) return "fill-emerald-300";
if (range - idx < 4) return "fill-orange-300";
return "fill-rose-300";
if (range - idx < 3) return "fb-fill-emerald-300";
if (range - idx < 4) return "fb-fill-orange-300";
return "fb-fill-rose-300";
}
};
const getSmiley = (iconIdx: number, idx: number, range: number, active: boolean, addColors: boolean) => {
const activeColor = addColors ? getActiveSmileyColor(range, idx) : "fill-rating-fill";
const inactiveColor = addColors ? getSmileyColor(range, idx) : "fill-none";
const activeColor = addColors ? getActiveSmileyColor(range, idx) : "fb-fill-rating-fill";
const inactiveColor = addColors ? getSmileyColor(range, idx) : "fb-fill-none";
const icons = [
<TiredFace className={active ? activeColor : inactiveColor} />,

View File

@@ -44,11 +44,11 @@ export const AutoCloseWrapper = ({ survey, onClose, children, offset }: AutoClos
}, [survey.autoClose]);
return (
<div className="h-full w-full">
<div className="fb-h-full fb-w-full">
{survey.autoClose && showAutoCloseProgressBar && (
<AutoCloseProgressBar autoCloseTimeout={survey.autoClose} />
)}
<div onClick={stopCountdown} onMouseOver={stopCountdown} className="h-full w-full">
<div onClick={stopCountdown} onMouseOver={stopCountdown} className="fb-h-full fb-w-full">
{children}
</div>
</div>

View File

@@ -45,17 +45,17 @@ export const Modal = ({ children, isOpen, placement, clickOutside, darkOverlay,
const getPlacementStyle = (placement: TPlacement) => {
switch (placement) {
case "bottomRight":
return "sm:bottom-3 sm:right-3";
return "sm:fb-bottom-3 sm:fb-right-3";
case "topRight":
return "sm:top-3 sm:right-3 sm:bottom-3";
return "sm:fb-top-3 sm:fb-right-3 sm:fb-bottom-3";
case "topLeft":
return "sm:top-3 sm:left-3 sm:bottom-3";
return "sm:fb-top-3 sm:fb-left-3 sm:fb-bottom-3";
case "bottomLeft":
return "sm:bottom-3 sm:left-3";
return "sm:fb-bottom-3 sm:fb-left-3";
case "center":
return "sm:top-1/2 sm:left-1/2 sm:transform sm:-translate-x-1/2 sm:-translate-y-1/2";
return "sm:fb-top-1/2 sm:fb-left-1/2 sm:fb-transform sm:-fb-translate-x-1/2 sm:-fb-translate-y-1/2";
default:
return "sm:bottom-3 sm:right-3";
return "sm:fb-bottom-3 sm:fb-right-3";
}
};
@@ -65,24 +65,24 @@ export const Modal = ({ children, isOpen, placement, clickOutside, darkOverlay,
<div
aria-live="assertive"
className={cn(
isCenter ? "pointer-events-auto" : "pointer-events-none",
"z-999999 fixed inset-0 flex items-end"
isCenter ? "fb-pointer-events-auto" : "fb-pointer-events-none",
"fb-z-999999 fb-fixed fb-inset-0 fb-flex fb-items-end"
)}>
<div
className={cn(
"relative h-full w-full",
"fb-relative fb-h-full fb-w-full",
isCenter
? darkOverlay
? "bg-gray-700/80"
: "bg-white/50"
: "bg-none transition-all duration-500 ease-in-out"
? "fb-bg-gray-700/80"
: "fb-bg-white/50"
: "fb-bg-none fb-transition-all fb-duration-500 fb-ease-in-out"
)}>
<div
ref={modalRef}
className={cn(
getPlacementStyle(placement),
show ? "opacity-100" : "opacity-0",
"rounded-custom pointer-events-auto absolute bottom-0 h-fit w-full overflow-visible bg-white shadow-lg transition-all duration-500 ease-in-out sm:m-4 sm:max-w-sm"
show ? "fb-opacity-100" : "fb-opacity-0",
"fb-rounded-custom fb-pointer-events-auto fb-absolute fb-bottom-0 fb-h-fit fb-w-full fb-overflow-visible fb-bg-white fb-shadow-lg fb-transition-all fb-duration-500 fb-ease-in-out sm:fb-m-4 sm:fb-max-w-sm"
)}>
<div>{children}</div>
</div>

View File

@@ -49,9 +49,9 @@ export const ScrollableContainer = ({ children }: ScrollableContainerProps) => {
}, [children]);
return (
<div className="relative">
<div className="fb-relative">
{!isAtTop && (
<div className="from-survey-bg absolute left-0 right-2 top-0 z-10 h-4 bg-gradient-to-b to-transparent"></div>
<div className="fb-from-survey-bg fb-absolute fb-left-0 fb-right-2 fb-top-0 fb-z-10 fb-h-4 fb-bg-gradient-to-b fb-to-transparent"></div>
)}
<div
ref={containerRef}
@@ -59,13 +59,16 @@ export const ScrollableContainer = ({ children }: ScrollableContainerProps) => {
scrollbarGutter: "stable both-edges",
maxHeight: isSurveyPreview ? "40dvh" : "60dvh",
}}
className={cn("overflow-auto px-4 pb-1", isOverflowHidden ? "no-scrollbar" : "bg-survey-bg")}
className={cn(
"fb-overflow-auto fb-px-4 fb-pb-1",
isOverflowHidden ? "fb-no-scrollbar" : "fb-bg-survey-bg"
)}
onMouseEnter={() => toggleOverflow(false)}
onMouseLeave={() => toggleOverflow(true)}>
{children}
</div>
{!isAtBottom && (
<div className="from-survey-bg absolute -bottom-2 left-0 right-2 h-8 bg-gradient-to-t to-transparent"></div>
<div className="fb-from-survey-bg fb-absolute -fb-bottom-2 fb-left-0 fb-right-2 fb-h-8 fb-bg-gradient-to-t fb-to-transparent"></div>
)}
</div>
);

View File

@@ -155,7 +155,7 @@ export const StackedCardsContainer = ({
return (
<div
className="relative flex h-full items-end justify-center md:items-center"
className="fb-relative fb-flex fb-h-full fb-items-end fb-justify-center md:fb-items-center"
onMouseEnter={() => {
setHovered(true);
}}
@@ -197,7 +197,7 @@ export const StackedCardsContainer = ({
...straightCardArrangementStyles(offset),
...getBottomStyles(),
}}
className="pointer rounded-custom bg-survey-bg absolute inset-x-0 backdrop-blur-md transition-all ease-in-out">
className="fb-pointer fb-rounded-custom fb-bg-survey-bg fb-absolute fb-inset-x-0 fb-backdrop-blur-md fb-transition-all fb-ease-in-out">
{getCardContent(questionIdxTemp, offset)}
</div>
);

View File

@@ -32,7 +32,7 @@
/* this is for styling the HtmlBody component */
.fb-htmlbody {
@apply block text-sm font-normal leading-6;
@apply fb-block fb-text-sm fb-font-normal fb-leading-6;
/* need to use !important because in packages/ui/components/editor/stylesEditorFrontend.css the color is defined for some classes */
color: var(--fb-subheading-color) !important;
}
@@ -106,7 +106,7 @@ p.fb-editor-paragraph {
}
}
.no-scrollbar {
.fb-no-scrollbar {
-ms-overflow-style: none !important; /* Internet Explorer 10+ */
scrollbar-width: thin !important; /* Firefox */
scrollbar-color: transparent transparent !important; /* Firefox */

View File

@@ -1,6 +1,7 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
important: "#fbjs",
prefix: "fb-",
darkMode: "class",
corePlugins: {
preflight: false,

View File

@@ -136,6 +136,7 @@ export type TLegacySurveyFileUploadQuestion = z.infer<typeof ZLegacySurveyFileUp
export const ZLegacySurveyCalQuestion = ZLegacySurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.Cal),
calUserName: z.string(),
calHost: z.string().optional(),
logic: z.array(ZSurveyCalLogic).optional(),
});

View File

@@ -385,6 +385,7 @@ export type TSurveyFileUploadQuestion = z.infer<typeof ZSurveyFileUploadQuestion
export const ZSurveyCalQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionTypeEnum.Cal),
calUserName: z.string(),
calHost: z.string().optional(),
logic: z.array(ZSurveyCalLogic).optional(),
});
@@ -524,6 +525,13 @@ export const ZSurvey = z.object({
showLanguageSwitch: z.boolean().nullable(),
});
export const ZSurveyUpdateInput = ZSurvey.omit({ createdAt: true, updatedAt: true }).and(
z.object({
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
})
);
export const ZSurveyInput = z.object({
name: z.string(),
type: ZSurveyType.optional(),

View File

@@ -34,7 +34,7 @@ export const DeleteDialog = ({
<Modal open={open} setOpen={setOpen} title={`Delete ${deleteWhat}`}>
<p>{text || "Are you sure? This action cannot be undone."}</p>
<div>{children}</div>
<div className="space-x-2 text-right">
<div className="mt-4 space-x-2 text-right">
<Button
loading={isSaving}
variant="secondary"

View File

@@ -50,7 +50,7 @@ export const DropdownSelector = ({
{!disabled && (
<DropdownMenuPortal>
<DropdownMenuContent
className="z-50 max-h-64 min-w-[220px] max-w-[90%] overflow-auto rounded-md bg-white text-sm text-slate-800 shadow-md"
className="z-50 max-h-64 min-w-[220px] max-w-96 overflow-auto rounded-md bg-white text-sm text-slate-800 shadow-md"
align="start">
{items
.sort((a, b) => a.name?.localeCompare(b.name))

View File

@@ -1,53 +1,82 @@
"use client";
import { TriangleAlertIcon, XIcon } from "lucide-react";
import Link from "next/link";
import {
getMonthlyActiveOrganizationPeopleCount,
getMonthlyOrganizationResponseCount,
} from "@formbricks/lib/organization/service";
import { useState } from "react";
import { TOrganization } from "@formbricks/types/organizations";
interface LimitsReachedBannerProps {
organization: TOrganization;
environmentId: string;
peopleCount: number;
responseCount: number;
}
export const LimitsReachedBanner = async ({ organization }: LimitsReachedBannerProps) => {
const [peopleCount, responseCount] = await Promise.all([
getMonthlyActiveOrganizationPeopleCount(organization.id),
getMonthlyOrganizationResponseCount(organization.id),
]);
export const LimitsReachedBanner = ({
organization,
peopleCount,
responseCount,
environmentId,
}: LimitsReachedBannerProps) => {
const orgBillingPeopleLimit = organization.billing?.limits?.monthly?.miu;
const orgBillingResponseLimit = organization.billing?.limits?.monthly?.responses;
const isPeopleLimitReached = orgBillingPeopleLimit !== null && peopleCount >= orgBillingPeopleLimit;
const isResponseLimitReached = orgBillingResponseLimit !== null && responseCount >= orgBillingResponseLimit;
if (isPeopleLimitReached && isResponseLimitReached) {
const [show, setShow] = useState(true);
if (show && (isPeopleLimitReached || isResponseLimitReached)) {
return (
<>
<div className="z-40 flex h-5 items-center justify-center bg-orange-800 text-center text-xs text-white">
You have reached your monthly MIU limit of {orgBillingPeopleLimit} and response limit of{" "}
{orgBillingResponseLimit}. <Link href="https://formbricks.com/pricing#faq">Learn more</Link>
<div
aria-live="assertive"
className="pointer-events-none fixed inset-0 z-[100] flex min-w-80 items-end px-4 py-6 sm:items-start sm:p-6">
<div className="flex w-full flex-col items-center space-y-4 sm:items-end">
<div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition">
<div className="p-4">
<div className="relative flex flex-col">
<div className="flex">
<div className="flex-shrink-0">
<TriangleAlertIcon className="text-error h-6 w-6" aria-hidden="true" />
</div>
<div className="ml-3 w-0 flex-1">
<p className="text-base font-medium text-gray-900">Limits Reached</p>
<p className="mt-1 text-sm text-gray-500">
{isPeopleLimitReached && isResponseLimitReached ? (
<>
You have reached your monthly MIU limit of <span>{orgBillingPeopleLimit}</span> and
response limit of {orgBillingResponseLimit}.{" "}
</>
) : null}
{isPeopleLimitReached && !isResponseLimitReached ? (
<>You have reached your monthly MIU limit of {orgBillingPeopleLimit}. </>
) : null}
{!isPeopleLimitReached && isResponseLimitReached ? (
<>You have reached your monthly response limit of {orgBillingResponseLimit}. </>
) : null}
</p>
<Link href={`/environments/${environmentId}/settings/billing`}>
<span className="text-sm text-slate-900">Learn more</span>
</Link>
</div>
</div>
<div className="absolute right-0 top-0 ml-4 flex flex-shrink-0">
<button
type="button"
className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => setShow(false)}>
<span className="sr-only">Close</span>
<XIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
</div>
</>
</div>
);
}
return (
<>
<div className="z-40 flex h-5 items-center justify-center bg-orange-800 text-center text-xs text-white">
{isPeopleLimitReached && (
<div>
You have reached your monthly MIU limit of {orgBillingPeopleLimit}.{" "}
<Link href="https://formbricks.com/pricing#faq">Learn more</Link>
</div>
)}
{isResponseLimitReached && (
<div>
You have reached your monthly response limit of {orgBillingResponseLimit}.{" "}
<Link href="https://formbricks.com/pricing#faq">Learn more</Link>
</div>
)}
</div>
</>
);
return null;
};

View File

@@ -0,0 +1,77 @@
"use client";
import { TriangleAlertIcon, XIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
interface PendingDowngradeBannerProps {
lastChecked: Date;
active: boolean;
isPendingDowngrade: boolean;
environmentId: string;
}
export const PendingDowngradeBanner = ({
lastChecked,
active,
isPendingDowngrade,
environmentId,
}: PendingDowngradeBannerProps) => {
const threeDaysInMillis = 3 * 24 * 60 * 60 * 1000;
const isLastCheckedWithin72Hours = lastChecked
? new Date().getTime() - lastChecked.getTime() < threeDaysInMillis
: false;
const scheduledDowngradeDate = new Date(lastChecked.getTime() + threeDaysInMillis);
const formattedDate = `${scheduledDowngradeDate.getMonth() + 1}/${scheduledDowngradeDate.getDate()}/${scheduledDowngradeDate.getFullYear()}`;
const [show, setShow] = useState(true);
if (show && active && isPendingDowngrade) {
return (
<div
aria-live="assertive"
className="pointer-events-none fixed inset-0 z-[100] flex min-w-80 items-end px-4 py-6 sm:items-start sm:p-6">
<div className="flex w-full flex-col items-center space-y-4 sm:items-end">
<div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition">
<div className="p-4">
<div className="relative flex flex-col">
<div className="flex">
<div className="flex-shrink-0">
<TriangleAlertIcon className="text-error h-6 w-6" aria-hidden="true" />
</div>
<div className="ml-3 w-0 flex-1">
<p className="text-base font-medium text-gray-900">Pending Downgrade</p>
<p className="mt-1 text-sm text-gray-500">
We were unable to verify your license because the license server is unreachable.{" "}
{isLastCheckedWithin72Hours
? `You will be downgraded to the Community Edition on ${formattedDate}.`
: "You are downgraded to the Community Edition."}
</p>
<Link href={`/environments/${environmentId}/settings/enterprise`}>
<span className="text-sm text-slate-900">Learn more</span>
</Link>
</div>
</div>
<div className="absolute right-0 top-0 ml-4 flex flex-shrink-0">
<button
type="button"
className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
onClick={() => setShow(false)}>
<span className="sr-only">Close</span>
<XIcon className="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
return null;
};

View File

@@ -15,7 +15,6 @@ interface PricingCardProps {
monthly: string;
yearly: string;
};
offer?: boolean;
mainFeatures: string[];
href: string;
};
@@ -135,7 +134,7 @@ export const PricingCard = ({
<div className="mt-2 flex items-center gap-x-4">
<p
className={cn(
plan.offer ? "text-orange-600" : plan.featured ? "text-slate-900" : "text-slate-800",
plan.featured ? "text-slate-900" : "text-slate-800",
"text-4xl font-bold tracking-tight"
)}>
{planPeriod === "monthly" ? plan.price.monthly : plan.price.yearly}

26117
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff