Compare commits

..

83 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
Sargam
81a4da6199 fix: initial fix for mail logger (#2798)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-06-26 10:07:37 +00:00
Anshuman Pandey
b8efc442e3 fix: e2e tests (#2806)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-06-26 10:05:01 +00:00
Piyush Gupta
e00bdf2f79 feat: Add colors to rating (#2795)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-06-26 08:08:23 +00:00
Dhruwang Jariwala
cea5716c48 fix: onboarding survey link (#2815) 2024-06-26 06:36:55 +00:00
yonathan suarez
fc150f8860 chore: add the CUSTOM_CACHE_DISABLED variable to the .env.example and documentation (#2746)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-06-25 15:13:14 +00:00
Piyush Gupta
7fdf8a63c8 fix: restricted svg upload in profile image and brand logo (#2813) 2024-06-25 14:56:34 +00:00
Laurens Nienhaus
374f17df63 feat: add column with person.id in CSV to allow backlinking to survey/person URL (#2807)
Co-authored-by: laurens <laurens@users.noreply.github.com>
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2024-06-25 14:51:13 +00:00
Dhruwang Jariwala
d27da8927e chore: Chinese to iso languages (#2794)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2024-06-25 14:49:34 +00:00
Johannes
785afd4bda fix: typo (#2812) 2024-06-25 14:18:22 +00:00
Matti Nannt
e856006e04 Merge branch 'main' into feature/custom-calcom-host 2024-06-25 15:34:10 +02:00
Dhruwang Jariwala
e28c226308 fix: question form card width issue (#2811) 2024-06-25 12:08:20 +00:00
plant
4170e20e21 docs: fix gitpod url (#2810)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-06-25 12:02:40 +00:00
Matthias Nannt
af548aa624 docs: fix notion links not working on contributing page 2024-06-25 14:01:11 +02:00
Matthias Nannt
13d68bbac0 chore: remove run-in-postman in docs as this is only possible for team members 2024-06-25 13:51:08 +02:00
Matti Nannt
fabea3c813 fix: update cache handler based on official example (#2805)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2024-06-25 08:09:11 +02:00
Piyush Gupta
db80e7f7cb fix: removes poweredByHeader (#2801) 2024-06-24 11:19:18 +00:00
Dhruwang Jariwala
1ebddbd1de fix: missing branding settings (#2800) 2024-06-24 07:08:15 +00:00
Alexander Schaber
01210dba3f feat(calcom): add custom cal.com field
Closes: #2654
2024-06-23 16:50:14 +02:00
Piyush Gupta
6c6061a123 fix: remove attributes from loading skeleton (#2796) 2024-06-22 05:22:54 +00:00
Matthias Nannt
0bf38aed9f docs: Emphasize the need to open port 80+443 for the one click setup 2024-06-21 14:38:10 +02:00
Anshuman Pandey
8aedbde36a fix: 2.2 data migration for self-hosters (#2792) 2024-06-20 14:43:26 +02:00
Piyush Gupta
17279cb3fe fix: null channel survey type bug (#2788) 2024-06-20 12:55:06 +02:00
Piyush Gupta
1b4823421f fix: google translate issue (#2787) 2024-06-20 10:32:36 +00:00
Piyush Gupta
deef604325 feat: adds functionality limit based on channel (#2772)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2024-06-20 09:16:59 +02:00
Johannes
22e44b47e6 fix: orange css for offer price (#2786) 2024-06-20 06:39:29 +00:00
Johannes
08052b13cf chore: add launch and pricing ui tweaks (#2782)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-06-20 05:54:23 +00:00
Matti Nannt
5e7fe96ef6 chore: prepare 2.2 launch (#2783) 2024-06-19 17:16:57 +02:00
Piyush Gupta
83ff98cd3a fix: Ability to increase file size limit (#2734)
Co-authored-by: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com>
2024-06-19 14:49:16 +00:00
Pranoy Roy
eb54230445 feat: added in-page section navigation for docs (#2720)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-06-19 14:23:51 +00:00
Dhruwang Jariwala
59d57c50eb fix: editor text color (#2781) 2024-06-19 12:31:46 +00:00
Dhruwang Jariwala
f358254e3c feat: Product onboarding with XM approach (#2770)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-06-19 12:29:05 +00:00
Dhruwang Jariwala
3ab8092a82 fix: Loading indicator for question card images (#2778)
Co-authored-by: Piyush Gupta <piyushguptaa2z123@gmail.com>
2024-06-19 12:04:27 +00:00
Anshuman Pandey
3b08b718ff feat: unlimited billing pages (#2777)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-06-19 08:44:45 +00:00
Dhruwang Jariwala
a473719eee feat: XM template filters (#2745)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-06-19 08:19:45 +00:00
Dhruwang Jariwala
cd40e655fb fix: progress bar issue (#2771)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-06-19 08:04:11 +00:00
Dhruwang Jariwala
5d468b4420 fix: Adjust billing permissions (#2775)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-06-19 07:23:28 +00:00
Piyush Gupta
96806c613f fix: fixes notification subscription based on user membership (#2779) 2024-06-19 07:22:05 +00:00
Piyush Gupta
6f2a4b2b03 feat: Full RTL support for Multi-Language (#2727)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
2024-06-19 07:10:28 +00:00
Piyush Gupta
d33efa0274 fix: e2e tests (#2780) 2024-06-19 06:34:16 +00:00
Matthias Nannt
9b35ebc114 fix: re-activate weekly summary cron job on Github Actions 2024-06-18 10:31:30 +02:00
Matthias Nannt
dba62157bf fix: temporary simplify getMonthlyActiveOrganizationPeopleCount to solve performance issues 2024-06-17 17:22:08 +02:00
Matthias Nannt
556184f442 fix: remove example survey creation to avoid having multiple example surveys in one environment 2024-06-17 16:48:35 +02:00
Matthias Nannt
c33532f952 chore: improve performance of getMonthlyActiveOrganizationPeopleCount 2024-06-17 15:26:49 +02:00
Piyush Gupta
8f61ceb4ea feat: adds separate status indicator for both app and website (#2747)
Co-authored-by: Johannes <johannes@formbricks.com>
2024-06-17 11:07:52 +00:00
Matthias Nannt
e19dcdc0c4 chore: improve performance of getMonthlyActiveOrganizationPeopleCount call 2024-06-17 12:07:33 +02:00
Piyush Gupta
65f977786d feat: add file size limit error message for FileUploadQuestionForm (#2769)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-06-17 06:37:57 +00:00
Matthias Nannt
95c13fd9a9 chore: deprecate js/sync endpoint 2024-06-15 10:01:06 +02:00
Matthias Nannt
f8adf28df8 chore: refactor getMonthlyActiveOrganizationPeopleCount to optimize performance 2024-06-14 23:08:12 +02:00
Matthias Nannt
9d468379f2 chore: reintroduce getMonthlyActiveOrganizationPeopleCount 2024-06-14 22:27:26 +02:00
Matthias Nannt
8071e82390 fix: reintroduce getMonthlyOrganizationResponseCount 2024-06-14 22:05:57 +02:00
Matthias Nannt
3ce775ec05 fix: performance issues on Formbricks Cloud due to updated billing checks (hotfix) 2024-06-14 21:18:39 +02:00
Anshuman Pandey
1223a30127 fix: wip (#2774) 2024-06-14 17:32:28 +02:00
Anshuman Pandey
f3906cab55 fix: getPersonCount (#2773) 2024-06-14 16:45:08 +02:00
Shubham Palriwala
d33304e3ad feat: pricing & payments v2 for formbricks cloud (#2648)
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-06-14 13:17:33 +00:00
Piyush Gupta
ebed950392 fix: product creation bug (#2768) 2024-06-14 05:12:48 +00:00
Piyush Gupta
4e6ab1c2bb feat: adds product config (#2760)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-06-13 12:51:02 +00:00
Dhruwang Jariwala
543d85eb28 feat: Hidden fields and metadata for integrations (#2752)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-06-13 11:14:43 +00:00
Matti Nannt
afe01a61ae fix: remove unused telemetry to fix build errors (#2761) 2024-06-13 08:42:14 +02:00
Johannes
b4c86835ed fix: survey padding and add report survey (#2763) 2024-06-12 15:58:19 +00:00
Dhruwang Jariwala
ab80bc1bf2 feat: language switch (#2692)
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-06-12 14:10:22 +00:00
Matti Nannt
5e5a9fac00 chore: rename responses to completed in stats card (#2759) 2024-06-12 15:20:59 +02:00
Dhruwang Jariwala
09b6805886 fix: recall from hidden fields (#2758) 2024-06-12 11:26:17 +00:00
Matti Nannt
33822350fd fix: add missing dependency in ee package (#2757) 2024-06-11 21:41:45 +02:00
852 changed files with 12355 additions and 11876 deletions

View File

@@ -162,9 +162,6 @@ ENTERPRISE_LICENSE_KEY=
# DEFAULT_ORGANIZATION_ID=
# DEFAULT_ORGANIZATION_ROLE=admin
# set to 1 to skip onboarding for new users
# ONBOARDING_DISABLED=1
# Send new users to customer.io
# CUSTOMER_IO_API_KEY=
# CUSTOMER_IO_SITE_ID=
@@ -179,7 +176,10 @@ ENTERPRISE_LICENSE_KEY=
UNSPLASH_ACCESS_KEY=
# The below is used for Next Caching (uses In-Memory from Next Cache if not provided)
# REDIS_URL:
# REDIS_URL=redis://localhost:6379
# The below is used for Rate Limiting (uses In-Memory LRU Cache if not provided) (You can use a service like Webdis for this)
# REDIS_HTTP_URL:
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
# CUSTOM_CACHE_DISABLED=1

View File

@@ -1,24 +0,0 @@
name: Cron - Report usage to Stripe
on:
workflow_dispatch:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
# schedule:
# This will run the job at 20:00 UTC every day of every month.
# - cron: "0 20 * * *"
jobs:
cron-reportUsageToStripe:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_SECRET }}
run: |
curl ${{ env.APP_URL }}/api/cron/report-usage \
-X POST \
-H 'x-api-key: ${{ env.CRON_SECRET }}' \
-H 'Cache-Control: no-cache' \
--fail

View File

@@ -4,9 +4,9 @@ on:
workflow_dispatch:
# "Scheduled workflows run on the latest commit on the default or base branch."
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
# schedule:
# Runs “At 08:00 on Monday.” (see https://crontab.guru)
# - cron: "0 8 * * 1"
schedule:
# Runs “At 08:00 on Monday.” (see https://crontab.guru)
- cron: "0 8 * * 1"
jobs:
cron-weeklySummary:
env:

View File

@@ -7,6 +7,20 @@ jobs:
name: Run E2E Tests
runs-on: ubuntu-latest
timeout-minutes: 60
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd="pg_isready -U testuser"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
@@ -16,44 +30,46 @@ jobs:
with:
e2e_testing_mode: "1"
- name: Check if pnpm is installed
id: pnpm-check
run: |
if pnpm --version; then
echo "pnpm is installed."
echo "PNPM_INSTALLED=true" >> $GITHUB_ENV
else
echo "pnpm is not installed."
echo "PNPM_INSTALLED=false" >> $GITHUB_ENV
fi
- name: Install pnpm
if: env.PNPM_INSTALLED == 'false'
uses: pnpm/action-setup@v2
- name: Install dependencies
if: env.PNPM_INSTALLED == 'false'
run: pnpm install
- name: Start PostgreSQL
- name: Apply Prisma Migrations
run: |
cd packages/database && pnpm db:up &
for attempt in {1..20}; do
if nc -zv localhost 5432; then
echo "Ready"
break
fi
echo "Waiting..."
sleep 5
done
pnpm db:migrate:dev
- name: Serve packages for lazy loading
run: |
cd packages/surveys && pnpm serve &
pnpm prisma migrate deploy
- name: Run App
run: |
NODE_ENV=test pnpm start --filter=@formbricks/web &
for attempt in {1..20}; do
sleep 10 # Optional: gives some buffer for the app to start
for attempt in {1..10}; do
if [ $(curl -o /dev/null -s -w "%{http_code}" http://localhost:3000/health) -eq 200 ]; then
echo "Ready"
echo "Application is ready."
break
fi
echo "Waiting..."
if [ $attempt -eq 10 ]; then
echo "Application failed to start in time."
exit 1
fi
echo "Still waiting for the application to be ready..."
sleep 10
done
- name: Test Serve endpoints
run: |
curl -s http://localhost:3003
- name: Cache Playwright
uses: actions/cache@v3
id: playwright-cache

View File

@@ -5,9 +5,9 @@ concurrency:
on:
workflow_dispatch:
push:
branches:
- main
#push:
# branches:
# - main
jobs:
Deploy:
@@ -57,7 +57,6 @@ jobs:
AIRTABLE_CLIENT_ID: ${{ secrets.AIRTABLE_CLIENT_ID }}
ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }}
DEFAULT_ORGANIZATION_ID: ${{ vars.DEFAULT_ORGANIZATION_ID }}
ONBOARDING_DISABLED: ${{ vars.ONBOARDING_DISABLED }}
CUSTOMER_IO_API_KEY: ${{ secrets.CUSTOMER_IO_API_KEY }}
CUSTOMER_IO_SITE_ID: ${{ secrets.CUSTOMER_IO_SITE_ID }}
NEXT_PUBLIC_POSTHOG_API_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_API_KEY }}

View File

@@ -54,7 +54,6 @@ jobs:
AIRTABLE_CLIENT_ID: ${{ secrets.AIRTABLE_CLIENT_ID }}
ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }}
DEFAULT_ORGANIZATION_ID: ${{ vars.DEFAULT_ORGANIZATION_ID }}
ONBOARDING_DISABLED: ${{ vars.ONBOARDING_DISABLED }}
CUSTOMER_IO_API_KEY: ${{ secrets.CUSTOMER_IO_API_KEY }}
CUSTOMER_IO_SITE_ID: ${{ secrets.CUSTOMER_IO_SITE_ID }}
NEXT_PUBLIC_POSTHOG_API_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_API_KEY }}

View File

@@ -1,9 +1,10 @@
name: Release Changesets
on:
push:
branches:
- main
workflow_dispatch:
#push:
# branches:
# - main
concurrency: ${{ github.workflow }}-${{ github.ref }}

View File

@@ -9,7 +9,6 @@ import {
ShieldCheckIcon,
UsersIcon,
} from "lucide-react";
import { classNames } from "../lib/utils";
const navigation = [

View File

@@ -13,8 +13,8 @@
"dependencies": {
"@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*",
"lucide-react": "^0.379.0",
"next": "14.2.3",
"lucide-react": "^0.397.0",
"next": "14.2.4",
"react": "18.3.1",
"react-dom": "18.3.1"
},

View File

@@ -1,6 +1,5 @@
import type { AppProps } from "next/app";
import Head from "next/head";
import "../styles/globals.css";
const App = ({ Component, pageProps }: AppProps) => {

View File

@@ -1,9 +1,7 @@
import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import formbricks from "@formbricks/js/app";
import { SurveySwitch } from "../../components/SurveySwitch";
import fbsetup from "../../public/fb-setup.png";
@@ -142,7 +140,7 @@ const AppPage = ({}) => {
<div className="p-6">
<div>
<button className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
<button className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
No-Code Action
</button>
</div>
@@ -171,7 +169,7 @@ const AppPage = ({}) => {
onClick={() => {
formbricks.setAttribute("Plan", "Free");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Plan to &apos;Free&apos;
</button>
</div>
@@ -194,7 +192,7 @@ const AppPage = ({}) => {
onClick={() => {
formbricks.setAttribute("Plan", "Paid");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Plan to &apos;Paid&apos;
</button>
</div>
@@ -217,7 +215,7 @@ const AppPage = ({}) => {
onClick={() => {
formbricks.setEmail("test@web.com");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600">
Set Email
</button>
</div>

View File

@@ -1,9 +1,7 @@
import Image from "next/image";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import formbricks from "@formbricks/js/website";
import { SurveySwitch } from "../../components/SurveySwitch";
import fbsetup from "../../public/fb-setup.png";
@@ -36,7 +34,7 @@ const AppPage = ({}) => {
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
const defaultAttributes = {
language: "de",
language: "en",
};
formbricks.init({

View File

@@ -22,9 +22,7 @@ export const metadata = {
App surveys have 6-10x better conversion rates than emailed out surveys. This tutorial explains how to run an app survey in your web app in 10 to 15 minutes. Lets go!
<Note>
App Surveys are ideal for websites that **have a user authentication** system. If you are looking to run
surveys on your public facing website, head over to the [Website Surveys Quickstart
Guide](/website-surveys/quickstart).
App Surveys are ideal for websites that **have a user authentication** system. If you are looking to run surveys on your public facing website, head over to the [Website Surveys Quickstart Guide](/website-surveys/quickstart).
</Note>
1. **Create a free Formbricks Cloud account**: While you can [self-host](/self-hosting/deployment) Formbricks, but the quickest and easiest way to get started is with the free Cloud plan. Just [sign up here](https://app.formbricks.com/auth/signup) and you'll be guided to our onboarding like below:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -16,7 +16,7 @@ export const metadata = {
### Overview
The Formbricks JS SDK is a 2-in-1 SDK for seamlessly integrating both App Surveys and Website Surveys into your projects. In this section, we'll explore how to leverage the SDK specifically for **app** surveys. Its available on npm [here](https://www.npmjs.com/package/@formbricks/js/).
The Formbricks JS SDK is a 2-in-1 SDK for seamlessly integrating both App Surveys and Website Surveys into your projects. In this section, we'll explore how to leverage the SDK specifically for **app** surveys. Its available on npm [here](https://www.npmjs.com/package/@formbricks/js/).
### Install
@@ -26,9 +26,11 @@ The Formbricks JS SDK is a 2-in-1 SDK for seamlessly integrating both App Survey
```js {{ title: 'npm' }}
npm install @formbricks/js
```
```js {{ title: 'yarn' }}
yarn add @formbricks/js
```
```js {{ title: 'pnpm' }}
pnpm add @formbricks/js
```
@@ -51,9 +53,10 @@ import formbricks from "@formbricks/js/app";
formbricks.init({
environmentId: "<your-environment-id>", // required
apiHost: "<your-api-host>", // required
userId: "<user-id>" // required
userId: "<user-id>", // required
});
```
</CodeGroup>
</Col>
@@ -67,15 +70,16 @@ Formbricks JS is a client SDK meant to be run client-side in their browser so ma
```js
if (window !== undefined) {
formbricks.init({
environmentId: "<your-environment-id>",
apiHost: "<your-api-host>",
userId: "<user-id>"
});
formbricks.init({
environmentId: "<your-environment-id>",
apiHost: "<your-api-host>",
userId: "<user-id>",
});
} else {
console.error("Window object not accessible to init Formbricks");
console.error("Window object not accessible to init Formbricks");
}
```
</CodeGroup>
</Col>
@@ -91,6 +95,7 @@ You can set custom attributes for the identified user. This can be helpful for s
```js
formbricks.setAttribute("Plan", "Paid");
```
</CodeGroup>
</Col>
@@ -104,10 +109,10 @@ Track user actions to trigger surveys based on user interactions, such as button
```js
formbricks.track("Clicked on Claim");
```
</CodeGroup>
</Col>
### Logout
To log out and deinitialize Formbricks, use the formbricks.logout() function. This action clears the current initialization configuration and erases stored frontend information, such as the surveys a user has viewed or completed. It's an important step when a user logs out of your application or when you want to reset Formbricks.
@@ -118,6 +123,7 @@ To log out and deinitialize Formbricks, use the formbricks.logout() function. Th
```js
formbricks.logout();
```
</CodeGroup>
</Col>
@@ -133,6 +139,7 @@ Reset the current instance and fetch the latest surveys and state again:
```js
formbricks.reset();
```
</CodeGroup>
</Col>
@@ -140,7 +147,11 @@ formbricks.reset();
Listen for page changes and dynamically show surveys configured via no-code actions in the Formbricks app:
<Note> This is only needed when your framework has a custom routing system and you want to trigger surveys on route changes. For example: NextJs</Note>
<Note>
{" "}
This is only needed when your framework has a custom routing system and you want to trigger surveys on route
changes. For example: NextJs
</Note>
<Col>
<CodeGroup>
@@ -148,6 +159,7 @@ Listen for page changes and dynamically show surveys configured via no-code acti
```js
formbricks.registerRouteChange();
```
</CodeGroup>
</Col>
@@ -167,7 +179,7 @@ In case you dont see your survey right away, here's what you can do. Go throu
### Formbricks Cloud and your app are not connected properly.
Go back to [app.formbricks.com](http://app.formbricks.com) or your self-hosted instance's URL and go to the Setup Checklist in the Settings. If the status is still indicated as “Not connected” your app hasn't yet pinged the Formbricks Cloud:
Go back to [app.formbricks.com](http://app.formbricks.com) or your self-hosted instance's URL and go to the App connection in the Configuration. If the status is still indicated as “Not connected” your app hasn't yet pinged the Formbricks Cloud:
<MdxImage
src={I1}

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

@@ -1,5 +1,4 @@
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@formbricks/ui/Accordion";
import { FaqJsonLdComponent } from "./FAQPageJsonLd";
const FAQ_DATA = [

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,469 +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://www.notion.so/Formbricks-End-to-End-Tests-06dc830d71604deaa8da24714540f7ab?pvs=21).
- **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://www.notion.so/Guidelines-for-Implementing-a-New-Question-Type-9ac0d1c362714addb24b9abeb326d1c1?pvs=21).
- **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

@@ -15,15 +15,7 @@ export const metadata = {
Formbricks offers two types of APIs: the **Public Client API** and the **Management API**. Each API serves a different purpose, has different authentication requirements, and provides access to different data and settings.
View our [API Documentation](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh) in more than 30 frameworks and languages. Or directly try out our APIs in Postman by clicking the button below:
<div className="max-w-full sm:max-w-3xl">
<a
target="_blank"
href="https://formbricks.postman.co/collection/11026000-927c954f-85a9-4f8f-b0ec-14191b903737?source=rip_html">
<img alt="Run in Postman" src="https://run.pstmn.io/button.svg" />
</a>
</div>
View our [API Documentation](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh) in more than 30 frameworks and languages.
## Public Client API
@@ -67,8 +59,8 @@ The API requests are authorized with a personal API key. This API key gives you
/>
<Note>
### Store API key safely!
Anyone who has your API key has full control over your account. For security reasons, you cannot view the API key again.
### Store API key safely! Anyone who has your API key has full control over your account. For security
reasons, you cannot view the API key again.
</Note>
### Test your API Key
@@ -123,7 +115,8 @@ Hit the below request to verify that you are authenticated with your API Key and
"id": "cll2m30r60003mx0hnemjfckr",
"name": "My Product"
},
"widgetSetupCompleted": false
"appSetupCompleted": false,
"websiteSetupCompleted": false,
}
```
```json {{ title: '401 Not Authenticated' }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -1,4 +1,5 @@
import { MdxImage } from "@/components/MdxImage";
import I1 from "./images/1-set-up-website-micro-survey-popup.webp";
export const metadata = {
title: "Formbricks Website Survey SDK",
@@ -142,4 +143,25 @@ This activates detailed debug messages in the browser console, providing deeper
---
## Troubleshooting
In case you dont see your survey right away, here's what you can do. Go through these to find the error fast:
### Formbricks Cloud and your website are not connected properly.
Go back to [app.formbricks.com](http://app.formbricks.com) or your self-hosted instance's URL and go to the Website connection in the Configuration. If the status is still indicated as “Not connected” your app hasn't yet pinged the Formbricks Cloud:
<MdxImage
src={I1}
alt="setup checklist ui of survey popup for website surveys"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
**How to fix it:**
1. Check if your website loads the Formbricks widget correctly.
2. Make sure you have `debug` mode enabled in your integration and you should see the Formbricks debug logs in your browser console while being in your app (right click in the browser, `Inspect`, switch to the console tab). If you dont see them, double check your integration.
---
If you have any questions or need help, feel free to reach out to us on our **[Discord](https://formbricks.com/discord)**

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

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

View File

@@ -49,7 +49,7 @@ How to deliver a specific language depends on the survey type (app or link surve
className="max-w-full rounded-lg sm:max-w-3xl"
/>
2. Click on the **Edit Languages** button, to add a new language to your survey
2. Click on the **Edit languages** button, to add a new language to your survey
<MdxImage
src={SurveyLanguageSettings}
@@ -58,7 +58,7 @@ How to deliver a specific language depends on the survey type (app or link surve
className="max-w-full rounded-lg sm:max-w-3xl"
/>
3. Select the preferred language from the dropdown and assign an identifier Alias. Click the **Add Language** button to add the language to your product.
3. Select the preferred language from the dropdown and assign an identifier Alias. Click the **Add language** button to add the language to your product.
<MdxImage
src={AddLanguages}

View File

@@ -3,7 +3,7 @@ import { MdxImage } from "@/components/MdxImage";
import PeopleView from "./people-view.webp";
export const metadata = {
title: "Effective Identify Users in Formbricks Link Surveys",
title: "Effectively identify users in Formbricks link surveys",
description:
"Discover how to seamlessly connect responses from Formbricks link surveys to existing users in your database. Learn the intricacies of the userId URL parameter to enhance user tracking, profiling, and segmentation, ensuring more personalized interactions and data-driven decisions.",
};

View File

@@ -52,13 +52,13 @@ These variables are present inside your machines docker-compose file. Restart
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | #64748b |
| DEFAULT_ORGANIZATION_ID | Automatically assign new users to a specific organization when joining | optional | |
| DEFAULT_ORGANIZATION_ROLE | Role of the user in the default organization. | optional | admin |
| ONBOARDING_DISABLED | Disables onboarding for new users if set to 1 | optional | |
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have .well-known configured at this) | optional (required if OIDC auth is enabled) | |
| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | RS256 |
| OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | |
| CUSTOM_CACHE_DISABLED | Disables custom cache handler if set to 1 (required for deployment on Vercel) | optional | |
| `<add more>` | | | |
| | | | |
@@ -176,8 +176,10 @@ An example configuration for a FusionAuth OpenID Connect in Formbricks would loo
<Col>
<CodeGroup title="Formbricks Env for FusionAuth OIDC">
```yml {{ title: ".env" }}
OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7 OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s
OIDC_ISSUER=http://localhost:9011 OIDC_DISPLAY_NAME=FusionAuth OIDC_SIGNING_ALGORITHM=HS256
OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7
OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s
OIDC_ISSUER=http://localhost:9011
OIDC_DISPLAY_NAME=FusionAuth OIDC_SIGNING_ALGORITHM=HS256
```
</CodeGroup>
</Col>

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,201 @@ 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.
### Steps to Migrate
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.2_$(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.2" \
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.
### Changes in Environment Variables
- `ONBOARDING_DISABLED` is now deprecated since we replaced the user onboarding with a product onboarding that only runs when creating a new product.
## v2.1
Formbricks v2.1 introduces more options for creating No-Code Actions and lays the foundation for easier self-hosting of Formbricks starting with an Onboarding for fresh instances.

View File

@@ -26,6 +26,7 @@ Before you proceed, make sure you have the following:
- A **Linux Ubuntu** Virtual Machine deployed with SSH access.
- An **A record** set up to connect a custom domain to your instance. Formbricks will **automatically create an SSL certificate** for your domain using Let's Encrypt.
- **Port 80 and 443** open in your VM's Security Group to allow Traefik to create the SSL certificate.
## Start
@@ -91,7 +92,7 @@ File '/etc/apt/keyrings/docker.gpg' exists. Overwrite? (y/N)
</CodeGroup>
</Col>
3. **Domain Name**: Enter the domain name that Traefik will use to create the SSL certificate and forward requests to Formbricks.
3. **Domain Name**: Enter the domain name that Traefik will use to create the SSL certificate and forward requests to Formbricks. Please make sure that port 80 and 443 are open in your VM's Security Group to allow Traefik to create the SSL certificate.
<Col>
<CodeGroup title="Domain Name for SSL certificate Prompt">

View File

@@ -22,9 +22,7 @@ export const metadata = {
Website Surveys make it easy for your public website visitors to give you feedback. They are a great way to get feedback from your users, without interrupting their workflow. This quickstart guide will show you how to create your first website survey in under 5 minutes.
<Note>
Website Surveys are ideal for **public facing websites**. If you are looking to run surveys in your app
where you have user identification & want advanced user targeting, head over to the [App Surveys Quickstart
Guide](/app-surveys/quickstart).
Website Surveys are ideal for **public facing websites**. If you are looking to run surveys in your app where you have user identification & want advanced user targeting, head over to the [App Surveys Quickstart Guide](/app-surveys/quickstart).
</Note>
1. **Create a free Formbricks Cloud account**: While you can [self-host](/self-hosting/deployment) Formbricks, but the quickest and easiest way to get started is with the free Cloud plan. Just [sign up here](https://app.formbricks.com/auth/signup) and you'll be guided to our onboarding like below:

View File

@@ -3,7 +3,6 @@
import { navigation } from "@/lib/navigation";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Button } from "./Button";
import { DiscordIcon } from "./icons/DiscordIcon";
import { GithubIcon } from "./icons/GithubIcon";
@@ -116,7 +115,7 @@ const SmallPrint = () => {
export const Footer = () => {
return (
<footer className="mx-auto w-full max-w-2xl space-y-10 pb-16 lg:max-w-5xl">
<footer className="my-10 flex-auto pb-16">
<PageNavigation />
<SmallPrint />
</footer>

View File

@@ -6,7 +6,6 @@ import clsx from "clsx";
import { motion, useScroll, useTransform } from "framer-motion";
import Link from "next/link";
import { forwardRef } from "react";
import { Button } from "./Button";
import { MobileNavigation, useIsInsideMobileNavigation, useMobileNavigationStore } from "./MobileNavigation";
import { ThemeToggle } from "./ThemeToggle";

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>
@@ -44,7 +44,7 @@ const Anchor = ({ id, inView, children }: { id: string; inView: boolean; childre
);
};
export const Heading = <Level extends 2 | 3>({
export const Heading = <Level extends 2 | 3 | 4>({
children,
tag,
label,
@@ -59,11 +59,11 @@ export const Heading = <Level extends 2 | 3>({
anchor?: boolean;
}) => {
level = level ?? (2 as Level);
let Component = `h${level}` as "h2" | "h3";
let ref = useRef<HTMLHeadingElement>(null);
let registerHeading = useSectionStore((s) => s.registerHeading);
const Component: "h2" | "h3" | "h4" = `h${level}`;
const ref = useRef<HTMLHeadingElement>(null);
const registerHeading = useSectionStore((s) => s.registerHeading);
let inView = useInView(ref, {
const inView = useInView(ref, {
margin: `${remToPx(-3.5)}px 0px 0px 0px`,
amount: "all",
});
@@ -71,8 +71,12 @@ export const Heading = <Level extends 2 | 3>({
useEffect(() => {
if (level === 2) {
registerHeading({ id: props.id, ref, offsetRem: tag || label ? 8 : 6 });
} else if (level === 3) {
registerHeading({ id: props.id, ref, offsetRem: tag || label ? 7 : 5 });
} else if (level === 4) {
registerHeading({ id: props.id, ref, offsetRem: tag || label ? 6 : 4 });
}
});
}, [label, level, props.id, registerHeading, tag]);
return (
<>

View File

@@ -2,10 +2,10 @@
import { Logo } from "@/components/Logo";
import { Navigation } from "@/components/Navigation";
import { SideNavigation } from "@/components/SideNavigation";
import { motion } from "framer-motion";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Footer } from "./Footer";
import { Header } from "./Header";
import { type Section, SectionProvider } from "./SectionProvider";
@@ -17,7 +17,7 @@ export const Layout = ({
children: React.ReactNode;
allSections: Record<string, Array<Section>>;
}) => {
let pathname = usePathname();
const pathname = usePathname();
return (
<SectionProvider sections={allSections[pathname || ""] ?? []}>
@@ -35,9 +35,12 @@ export const Layout = ({
<Navigation className="hidden lg:mt-10 lg:block" isMobile={false} />
</div>
</motion.header>
<div className="relative flex h-full flex-col px-4 pt-14 sm:px-6 lg:px-8">
<main className="flex-auto">{children}</main>
<Footer />
<div className="flex h-screen flex-col">
<div className="flex flex-col px-4 pt-14 sm:px-6 lg:w-[calc(100%-20rem)] lg:px-8">
<main className="overflow-y-auto overflow-x-hidden">{children}</main>
<Footer />
</div>
<SideNavigation pathname={pathname} />
</div>
</div>
</SectionProvider>

View File

@@ -1,6 +1,15 @@
import Image, { ImageProps } from "next/image";
import React from "react";
export const MdxImage = (props: ImageProps) => {
return <Image {...props} alt={props.alt} />;
return (
<Image
{...props}
alt={props.alt}
sizes="100vw"
style={{
width: "100%",
height: "auto",
}}
/>
);
};

View File

@@ -8,7 +8,6 @@ import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useRef, useState } from "react";
import { Button } from "./Button";
import { useIsInsideMobileNavigation } from "./MobileNavigation";
import { useSectionStore } from "./SectionProvider";

View File

@@ -0,0 +1,82 @@
import { useTableContentObserver } from "@/hooks/useTableContentObserver";
import Link from "next/link";
import { useEffect, useState } from "react";
type Heading = {
id: string;
text: string | null;
level: number;
};
export const SideNavigation = ({ pathname }) => {
const [headings, setHeadings] = useState<Heading[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
useTableContentObserver(setSelectedId, pathname);
useEffect(() => {
const getHeadings = () => {
// Select all heading elements (h2, h3, h4) with an 'id' attribute
const headingElements = document.querySelectorAll("h2[id], h3[id], h4[id]");
// Convert the NodeList of heading elements into an array and map them to an array of 'Heading' objects
const headings: Heading[] = Array.from(headingElements).map((heading) => ({
id: heading.id,
text: heading.textContent,
level: parseInt(heading.tagName.slice(1)),
}));
// Check if there are any h2 headings in the list
const hasH2 = headings.some((heading) => heading.level === 2);
// Update the 'headings' state with the retrieved headings, but only if there are h2 headings
setHeadings(hasH2 ? headings : []);
};
getHeadings();
}, [pathname]);
const renderHeading = (items: Heading[], currentLevel: number) => (
<ul className="ml-1 mt-4">
{items.map((heading, index) => {
if (heading.level === currentLevel) {
let nextIndex = index + 1;
while (nextIndex < items.length && items[nextIndex].level > currentLevel) {
nextIndex++;
}
return (
<li
key={heading.text}
className={`mb-4 ml-4 text-slate-900 dark:text-white ml-${heading.level === 2 ? 0 : heading.level === 3 ? 4 : 6}`}>
<Link
href={`#${heading.id}`}
onClick={() => setSelectedId(heading.id)}
className={`${
heading.id === selectedId
? "text-brand font-medium"
: "font-normal text-slate-600 hover:text-slate-950 dark:text-white dark:hover:text-slate-50"
}`}>
{heading.text}
</Link>
{nextIndex > index + 1 && renderHeading(items.slice(index + 1, nextIndex), currentLevel + 1)}
</li>
);
}
return null;
})}
</ul>
);
if (headings.length) {
return (
<aside className="fixed right-0 top-0 hidden h-[calc(100%-2.5rem)] w-80 overflow-hidden overflow-y-auto pr-8 pt-16 text-sm [scrollbar-width:none] lg:mt-10 lg:block">
<div className="border-l border-slate-200 dark:border-slate-700">
<h3 className="ml-5 mt-1 text-xs font-semibold uppercase text-slate-400">on this page</h3>
{renderHeading(headings, 2)}
</div>
</aside>
);
}
return null;
};

View File

@@ -19,10 +19,19 @@ export const wrapper = ({ children }: { children: React.ReactNode }) => {
);
};
export const h2 = (props: Omit<React.ComponentPropsWithoutRef<typeof Heading>, "level">) => {
return <Heading level={2} {...props} />;
const createHeadingComponent = (level: 2 | 3 | 4) => {
const Component = (props: Omit<React.ComponentPropsWithoutRef<typeof Heading>, "level">) => {
return <Heading level={level} {...props} />;
};
Component.displayName = `H${level}`;
return Component;
};
export const h2 = createHeadingComponent(2);
export const h3 = createHeadingComponent(3);
export const h4 = createHeadingComponent(4);
const InfoIcon = (props: React.ComponentPropsWithoutRef<"svg">) => {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" {...props}>

View File

@@ -0,0 +1,71 @@
import { useEffect, useRef } from "react";
interface HeadingElement extends IntersectionObserverEntry {
target: HTMLHeadingElement;
}
/**
* A custom hook that sets up an IntersectionObserver to track the visibility of headings on the page.
*
* @param {Function} setActiveId - A function to set the active heading ID.
* @param {string} pathname - The current pathname, used as a dependency for the useEffect hook.
* @returns {void}
*
* This hook performs the following tasks:
* 1. Creates a map of heading elements, where the key is the heading's ID and the value is the heading element.
* 2. Finds the visible headings (i.e., headings that are currently intersecting with the viewport).
* 3. If there is only one visible heading, sets it as the active heading using the `setActiveId` function.
* 4. If there are multiple visible headings, sets the active heading to the one that is highest on the page (i.e., the one with the lowest index in the `headingElements` array).
* 5. Cleans up the IntersectionObserver and the `headingElementsRef` when the component is unmounted.
*/
export const useTableContentObserver = (setActiveId: (id: string) => void, pathname: string) => {
const headingElementsRef = useRef<Record<string, HeadingElement>>({});
useEffect(() => {
const callback = (headings: HeadingElement[]) => {
// Create a map of heading elements, where the key is the heading's ID and the value is the heading element
headingElementsRef.current = headings.reduce(
(map, headingElement) => {
return { ...map, [headingElement.target.id]: headingElement };
},
{} as Record<string, HeadingElement>
);
// Find the visible headings (i.e., headings that are currently intersecting with the viewport)
const visibleHeadings: HeadingElement[] = [];
Object.keys(headingElementsRef.current).forEach((key) => {
const headingElement = headingElementsRef.current[key];
if (headingElement.isIntersecting) visibleHeadings.push(headingElement);
});
// Define a function to get the index of a heading element in the headingElements array
const getIndexFromId = (id: string) => headingElements.findIndex((heading) => heading.id === id);
// If there is only one visible heading, set it as the active heading
if (visibleHeadings.length === 1) {
setActiveId(visibleHeadings[0].target.id);
}
// If there are multiple visible headings, set the active heading to the one that is highest on the page
else if (visibleHeadings.length > 1) {
const sortedVisibleHeadings = visibleHeadings.sort((a, b) => {
const aIndex = getIndexFromId(a.target.id);
const bIndex = getIndexFromId(b.target.id);
return aIndex - bIndex;
});
setActiveId(sortedVisibleHeadings[0].target.id);
}
};
const observer = new IntersectionObserver(callback, {
rootMargin: "-40px 0px -40% 0px",
});
const headingElements = Array.from(document.querySelectorAll("h2[id], h3[id], h4[id]"));
headingElements.forEach((element) => observer.observe(element));
return () => {
observer.disconnect();
headingElementsRef.current = {};
};
}, [setActiveId, pathname]);
};

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

@@ -46,9 +46,9 @@ const rehypeShiki = () => {
const rehypeSlugify = () => {
return (tree) => {
let slugify = slugifyWithCounter();
const slugify = slugifyWithCounter();
visit(tree, "element", (node) => {
if (node.tagName === "h2" && !node.properties.id) {
if (["h2", "h3", "h4"].includes(node.tagName) && !node.properties.id) {
node.properties.id = slugify(toString(node));
}
});
@@ -83,15 +83,15 @@ const rehypeAddMDXExports = (getExports) => {
};
const getSections = (node) => {
let sections = [];
const sections = [];
for (let child of node.children ?? []) {
if (child.type === "element" && child.tagName === "h2") {
for (const child of node.children ?? []) {
if (child.type === "element" && ["h2", "h3", "h4"].includes(child.tagName)) {
sections.push(`{
title: ${JSON.stringify(toString(child))},
id: ${JSON.stringify(child.properties.id)},
...${child.properties.annotation}
}`);
title: ${JSON.stringify(toString(child))},
id: ${JSON.stringify(child.properties.id)},
...${child.properties.annotation}
}`);
} else if (child.children) {
sections.push(...getSections(child));
}

View File

@@ -12,34 +12,34 @@
},
"browserslist": "defaults, not ie <= 11",
"dependencies": {
"@algolia/autocomplete-core": "^1.17.0",
"@calcom/embed-react": "^1.4.0",
"@algolia/autocomplete-core": "^1.17.2",
"@calcom/embed-react": "^1.5.0",
"@docsearch/css": "3",
"@docsearch/react": "^3.6.0",
"@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*",
"@headlessui/react": "^2.0.3",
"@headlessui/tailwindcss": "^0.2.0",
"@headlessui/react": "^2.1.1",
"@headlessui/tailwindcss": "^0.2.1",
"@mapbox/rehype-prism": "^0.9.0",
"@mdx-js/loader": "^3.0.1",
"@mdx-js/react": "^3.0.1",
"@next/mdx": "14.2.3",
"@next/mdx": "14.2.4",
"@paralleldrive/cuid2": "^2.2.2",
"@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/typography": "^0.5.13",
"acorn": "^8.11.3",
"acorn": "^8.12.0",
"autoprefixer": "^10.4.19",
"clsx": "^2.1.1",
"fast-glob": "^3.3.2",
"flexsearch": "^0.7.43",
"framer-motion": "11.1.9",
"framer-motion": "11.2.12",
"lottie-web": "^5.12.2",
"lucide": "^0.378.0",
"lucide-react": "^0.378.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.3",
"next": "14.2.4",
"next-plausible": "^3.12.0",
"next-seo": "^6.5.0",
"next-sitemap": "^4.2.3",
@@ -59,15 +59,15 @@
"sharp": "^0.33.4",
"shiki": "^0.14.7",
"simple-functional-loader": "^1.2.1",
"tailwindcss": "^3.4.3",
"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:*",
"@types/dompurify": "^3.0.5",
"@types/react-highlight-words": "^0.16.7",
"@types/react-highlight-words": "^0.20.0",
"@formbricks/eslint-config": "workspace:*"
}
}

View File

@@ -3,7 +3,6 @@ import forms from "@tailwindcss/forms";
import typographyPlugin from "@tailwindcss/typography";
import { type Config } from "tailwindcss";
import defaultTheme from "tailwindcss/defaultTheme";
import typographyStyles from "./typography";
export default {

View File

@@ -1,6 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -1,118 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://formbricks.com</loc><lastmod>2024-04-08T07:33:12.606Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog</loc><lastmod>2024-04-08T07:33:12.613Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/author/johannes</loc><lastmod>2024-04-08T07:33:12.613Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/author/shubham</loc><lastmod>2024-04-08T07:33:12.613Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/author/sudhanshu</loc><lastmod>2024-04-08T07:33:12.613Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/best-feedback-app-and-how-to-use-them</loc><lastmod>2024-04-08T07:33:12.613Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/best-hotjar-alternatives-2024-incl-open-source</loc><lastmod>2024-04-08T07:33:12.613Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/best-open-source-survey-software-2023</loc><lastmod>2024-04-08T07:33:12.613Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/best-website-feedback-tools-2024</loc><lastmod>2024-04-08T07:33:12.613Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/experience-management-open-source</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/first-end-to-endcommunity-feature</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/github-accelerator-experience</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/granular-targeting-in-app-surveys</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/how-smart-writers-use-formbricks-open-source-tool-to-measure-the-quality-of-their-newsletter-content</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/inaugural-batch-github-accelerator</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/join-the-formtribe</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/open-source-forms-will-save-the-world</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/open-source-qualtrics-beats-typeform</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/remove-branding-for-free</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/snoopforms-becomes-formbricks</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/terraform-ecs-aws-self-hosting</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/the-perfect-waitlist-survey</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/understanding-formbricks-self-hosting</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/blog/why-open-source-no-code-is-the-future-of-enterprise-gov-software</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/careers</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/community</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/community/ContributorGrid</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/community/HallOfFame</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/community/HeaderTribe</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/community/LayoutTribe</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/community/LevelCard</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/community/LevelGrid</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/community/Roadmap</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/concierge</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs-feedback</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/feature-chaser</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/feedback-box</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/gdpr</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/gdpr-guide</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/imprint</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/improve-trial-conversion</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/in-app-survey</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/interview-prompt</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/learn-from-churn</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/measure-product-market-fit</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/onboarding-segmentation</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/open-source-form-builder</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/oss-friends</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/pricing</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/privacy-policy</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/terms</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/vs-formspree</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/vs-google-forms</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/vs-ohmyform</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/website-survey</loc><lastmod>2024-04-08T07:33:12.614Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/client/responses</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/management/action-classes</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/management/attribute-classes</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/management/me</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/management/people</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/client/overview</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/management/responses</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/management/webhooks</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/client/displays</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/best-practices/cancel-subscription</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/client/people</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/best-practices/feedback-box</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/best-practices/feature-chaser</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/best-practices/docs-feedback</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/best-practices/improve-trial-cr</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/management/api-key-setup</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/best-practices/improve-email-content</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/best-practices/pmf-survey</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/best-practices/interview-prompt</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/contributing/demo</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/contributing/how-we-code</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/contributing/introduction</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/contributing/creating-a-service</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/contributing/setup</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/additional-features/multi-language-surveys</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/contributing/troubleshooting</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/getting-started/quickstart-in-app-survey</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/faq</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/in-app-surveys/actions</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/client/actions</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/getting-started/framework-guides</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/in-app-surveys/advanced-targeting</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/in-app-surveys/user-identification</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/getting-started/troubleshooting</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/in-app-surveys/attributes</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/api/management/surveys</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/integrations/make</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/integrations/airtable</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/integrations/notion</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/integrations/zapier</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/link-surveys/data-prefilling</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/integrations/wordpress</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/introduction/what-is-formbricks</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/introduction/how-it-works</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/introduction/why-is-it-better</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/link-surveys/single-use-links</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/link-surveys/hidden-fields</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/link-surveys/quickstart</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/link-surveys/source-tracking</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/self-hosting/docker</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/self-hosting/deployment</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/link-surveys/start-at-question</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/link-surveys/user-identification</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/self-hosting/enterprise</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/self-hosting/external-auth-providers</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/self-hosting/migration-guide</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/self-hosting/production</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/self-hosting/license</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/integrations/n8n</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://formbricks.com/docs/integrations/google-sheets</loc><lastmod>2024-04-08T07:33:12.615Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
</urlset>

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap><loc>https://formbricks.com/sitemap-0.xml</loc></sitemap>
</sitemapindex>

View File

@@ -1,5 +1,4 @@
import type { Preview } from "@storybook/react";
import "../../web/app/globals.css";
const preview: Preview = {

View File

@@ -19,22 +19,22 @@
"devDependencies": {
"@chromatic-com/storybook": "^1.5.0",
"@formbricks/config-typescript": "workspace:*",
"@storybook/addon-essentials": "^8.1.5",
"@storybook/addon-interactions": "^8.1.5",
"@storybook/addon-links": "^8.1.5",
"@storybook/addon-onboarding": "^8.1.5",
"@storybook/blocks": "^8.1.5",
"@storybook/react": "^8.1.5",
"@storybook/react-vite": "^8.1.5",
"@storybook/test": "^8.1.5",
"@typescript-eslint/eslint-plugin": "^7.12.0",
"@typescript-eslint/parser": "^7.12.0",
"@vitejs/plugin-react": "^4.3.0",
"esbuild": "^0.21.4",
"@storybook/addon-essentials": "^8.1.10",
"@storybook/addon-interactions": "^8.1.10",
"@storybook/addon-links": "^8.1.10",
"@storybook/addon-onboarding": "^8.1.10",
"@storybook/blocks": "^8.1.10",
"@storybook/react": "^8.1.10",
"@storybook/react-vite": "^8.1.10",
"@storybook/test": "^8.1.10",
"@typescript-eslint/eslint-plugin": "^7.13.1",
"@typescript-eslint/parser": "^7.13.1",
"@vitejs/plugin-react": "^4.3.1",
"esbuild": "^0.21.5",
"eslint-plugin-storybook": "^0.8.0",
"prop-types": "^15.8.1",
"storybook": "^8.1.5",
"storybook": "^8.1.10",
"tsup": "^8.1.0",
"vite": "^5.2.12"
"vite": "^5.3.1"
}
}

View File

@@ -1,5 +1,4 @@
import { useState } from "react";
import "./App.css";
export const App = () => {

View File

@@ -1,6 +1,5 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";

View File

@@ -0,0 +1,90 @@
"use client";
import Dance from "@/images/onboarding-dance.gif";
import Lost from "@/images/onboarding-lost.gif";
import { ArrowRight } from "lucide-react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { cn } from "@formbricks/lib/cn";
import { TEnvironment } from "@formbricks/types/environment";
import { TProductConfigChannel } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
interface ConnectWithFormbricksProps {
environment: TEnvironment;
webAppUrl: string;
widgetSetupCompleted: boolean;
channel: TProductConfigChannel;
}
export const ConnectWithFormbricks = ({
environment,
webAppUrl,
widgetSetupCompleted,
channel,
}: ConnectWithFormbricksProps) => {
const router = useRouter();
const handleFinishOnboarding = async () => {
if (!widgetSetupCompleted) {
router.push(`/environments/${environment.id}/connect/invite`);
return;
}
router.push(`/environments/${environment.id}/surveys`);
};
useEffect(() => {
const handleVisibilityChange = async () => {
if (document.visibilityState === "visible") {
router.refresh();
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="mt-6 flex w-5/6 flex-col items-center space-y-10 lg:w-2/3 2xl:w-1/2">
<div className="flex w-full space-x-10">
<div className="flex w-1/2 flex-col space-y-4">
<OnboardingSetupInstructions
environmentId={environment.id}
webAppUrl={webAppUrl}
channel={channel}
widgetSetupCompleted={widgetSetupCompleted}
/>
</div>
<div
className={cn(
"flex h-[30rem] w-1/2 flex-col items-center justify-center rounded-lg border bg-slate-200 text-center shadow",
widgetSetupCompleted ? "border-green-500 bg-green-100" : ""
)}>
{widgetSetupCompleted ? (
<div>
<Image src={Dance} alt="lost" height={250} />
<p className="mt-6 text-xl font-bold">Connection successful </p>
</div>
) : (
<div className="space-y-4">
<Image src={Lost} alt="lost" height={250} />
<p className="pt-4 text-slate-400">Waiting for your signal...</p>
</div>
)}
</div>
</div>
<Button
id="finishOnboarding"
variant={widgetSetupCompleted ? "darkCTA" : "minimal"}
onClick={handleFinishOnboarding}
EndIcon={ArrowRight}>
{widgetSetupCompleted ? "Finish Onboarding" : "Skip"}
</Button>
</div>
);
};

View File

@@ -0,0 +1,121 @@
"use client";
import { inviteOrganizationMemberAction } from "@/app/(app)/(onboarding)/organizations/actions";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { FormProvider, useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { z } from "zod";
import { TOrganization } from "@formbricks/types/organizations";
import { Button } from "@formbricks/ui/Button";
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
import { Input } from "@formbricks/ui/Input";
interface InviteOrganizationMemberProps {
organization: TOrganization;
environmentId: string;
}
const ZInviteOrganizationMemberDetails = z.object({
email: z.string().email(),
inviteMessage: z.string().trim().min(1),
});
type TInviteOrganizationMemberDetails = z.infer<typeof ZInviteOrganizationMemberDetails>;
export const InviteOrganizationMember = ({ organization, environmentId }: InviteOrganizationMemberProps) => {
const router = useRouter();
const form = useForm<TInviteOrganizationMemberDetails>({
defaultValues: {
email: "",
inviteMessage: "I'm looking into Formbricks to run targeted surveys. Can you help me set it up? 🙏",
},
resolver: zodResolver(ZInviteOrganizationMemberDetails),
});
const { isSubmitting } = form.formState;
const handleInvite = async (data: TInviteOrganizationMemberDetails) => {
try {
await inviteOrganizationMemberAction(organization.id, data.email, "developer", data.inviteMessage);
toast.success("Invite sent successful");
await finishOnboarding();
} catch (error) {
toast.error("An unexpected error occurred");
}
};
const finishOnboarding = async () => {
router.push(`/environments/${environmentId}/surveys`);
};
return (
<div className="mb-8 w-full max-w-xl space-y-8">
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(handleInvite)} className="w-full space-y-4">
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<FormLabel>Email</FormLabel>
<FormControl>
<div>
<Input
value={field.value}
onChange={(email) => field.onChange(email)}
placeholder="engineering@acme.com"
className="bg-white"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="inviteMessage"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<FormLabel>Invite Message</FormLabel>
<FormControl>
<div>
<textarea
rows={5}
className="focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-transparent bg-white px-3 py-2 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:text-slate-300"
value={field.value}
onChange={(inviteMessage) => field.onChange(inviteMessage)}
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<div className="flex w-full justify-end space-x-2">
<Button
id="onboarding-inapp-invite-have-a-look-first"
className="font-normal text-slate-400"
variant="minimal"
onClick={(e) => {
e.preventDefault();
finishOnboarding();
}}>
Skip
</Button>
<Button
id="onboarding-inapp-invite-send-invite"
variant="darkCTA"
type={"submit"}
loading={isSubmitting}>
Invite
</Button>
</div>
</div>
</form>
</FormProvider>
</div>
);
};

View File

@@ -0,0 +1,161 @@
"use client";
import "prismjs/themes/prism.css";
import { useState } from "react";
import toast from "react-hot-toast";
import { TProductConfigChannel } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { CodeBlock } from "@formbricks/ui/CodeBlock";
import { TabBar } from "@formbricks/ui/TabBar";
import { Html5Icon, NpmIcon } from "@formbricks/ui/icons";
const tabs = [
{ id: "html", label: "HTML", icon: <Html5Icon /> },
{ id: "npm", label: "NPM", icon: <NpmIcon /> },
];
interface OnboardingSetupInstructionsProps {
environmentId: string;
webAppUrl: string;
channel: TProductConfigChannel;
widgetSetupCompleted: boolean;
}
export const OnboardingSetupInstructions = ({
environmentId,
webAppUrl,
channel,
widgetSetupCompleted,
}: OnboardingSetupInstructionsProps) => {
const [activeTab, setActiveTab] = useState(tabs[0].id);
const htmlSnippetForAppSurveys = `<!-- START Formbricks Surveys -->
<script type="text/javascript">
!function(){
var apiHost = "${webAppUrl}";
var environmentId = "${environmentId}";
var userId = "testUser";
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/api/packages/app";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost, userId: userId})},500)}();
</script>
<!-- END Formbricks Surveys -->
`;
const htmlSnippetForWebsiteSurveys = `<!-- START Formbricks Surveys -->
<script type="text/javascript">
!function(){
var apiHost = "${webAppUrl}";
var environmentId = "${environmentId}";
var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src=apiHost+"/api/packages/website";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: environmentId, apiHost: apiHost})},500)}();
</script>
<!-- END Formbricks Surveys -->
`;
const npmSnippetForAppSurveys = `
import formbricks from "@formbricks/js/app";
if (typeof window !== "undefined") {
formbricks.init({
environmentId: "${environmentId}",
apiHost: "${webAppUrl}",
userId: "testUser",
});
}
function App() {
// your own app
}
export default App;
`;
const npmSnippetForWebsiteSurveys = `
// other imports
import formbricks from "@formbricks/js/website";
if (typeof window !== "undefined") {
formbricks.init({
environmentId: "${environmentId}",
apiHost: "${webAppUrl}",
});
}
function App() {
// your own app
}
export default App;
`;
return (
<div>
<div className="flex h-14 w-full items-center justify-center rounded-md border border-slate-200 bg-white">
<TabBar
tabs={tabs}
activeId={activeTab}
setActiveId={setActiveTab}
tabStyle="button"
className="bg-slate-100"
/>
</div>
<div>
{activeTab === "npm" ? (
<div className="prose prose-slate w-full">
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="sh">
npm install @formbricks/js
</CodeBlock>
<p>or</p>
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="sh">
yarn add @formbricks/js
</CodeBlock>
<p className="text-sm text-slate-700">
Import Formbricks and initialize the widget in your Component (e.g. App.tsx):
</p>
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
{channel === "app" ? npmSnippetForAppSurveys : npmSnippetForWebsiteSurveys}
</CodeBlock>
<Button
id="onboarding-inapp-connect-read-npm-docs"
className="mt-3"
variant="secondary"
href={`https://formbricks.com/docs/${channel}-surveys/framework-guides`}
target="_blank">
Read docs
</Button>
</div>
) : activeTab === "html" ? (
<div className="prose prose-slate">
<p className="-mb-1 mt-6 text-sm text-slate-700">
Insert this code into the &lt;head&gt; tag of your website:
</p>
<div>
<CodeBlock customEditorClass="!bg-white border border-slate-200" language="js">
{channel === "app" ? htmlSnippetForAppSurveys : htmlSnippetForWebsiteSurveys}
</CodeBlock>
</div>
<div className="mt-4 flex justify-between space-x-2">
<Button
id="onboarding-inapp-connect-copy-code"
variant={widgetSetupCompleted ? "secondary" : "darkCTA"}
onClick={() => {
navigator.clipboard.writeText(
channel === "app" ? htmlSnippetForAppSurveys : htmlSnippetForWebsiteSurveys
);
toast.success("Copied to clipboard");
}}>
Copy code
</Button>
<Button
id="onboarding-inapp-connect-step-by-step-manual"
variant="secondary"
href={`https://formbricks.com/docs/${channel}-surveys/framework-guides#html`}
target="_blank">
Step by step manual
</Button>
</div>
</div>
) : null}
</div>
</div>
);
};

View File

@@ -0,0 +1,53 @@
import { InviteOrganizationMember } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/InviteOrganizationMember";
import { XIcon } from "lucide-react";
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 { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
import { Button } from "@formbricks/ui/Button";
import { Header } from "@formbricks/ui/Header";
interface InvitePageProps {
params: {
environmentId: string;
};
}
const Page = async ({ params }: InvitePageProps) => {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
}
const organization = await getOrganizationByEnvironmentId(params.environmentId);
if (!organization) {
throw new Error("Organization not Found");
}
const membership = await getMembershipByUserIdOrganizationId(session.user.id, organization.id);
if (!membership || (membership.role !== "owner" && membership.role !== "admin")) {
return notFound();
}
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center">
<Header
title="Invite your organization to help out"
subtitle="Ask your tech-savvy co-worker to finish the setup:"
/>
<div className="space-y-4 text-center">
<p className="text-4xl font-medium text-slate-800"></p>
<p className="text-sm text-slate-500"></p>
</div>
<InviteOrganizationMember organization={organization} environmentId={params.environmentId} />
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
href={`/environments/${params.environmentId}/`}>
<XIcon className="h-7 w-7" strokeWidth={1.5} />
</Button>
</div>
);
};
export default Page;

View File

@@ -0,0 +1,21 @@
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { AuthorizationError } from "@formbricks/types/errors";
const OnboardingLayout = async ({ children, params }) => {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
}
const isAuthorized = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
if (!isAuthorized) {
throw AuthorizationError;
}
return <div className="flex-1 bg-slate-50">{children}</div>;
};
export default OnboardingLayout;

View File

@@ -0,0 +1,65 @@
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
import { XIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { WEBAPP_URL } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { Button } from "@formbricks/ui/Button";
import { Header } from "@formbricks/ui/Header";
interface ConnectPageProps {
params: {
environmentId: string;
};
}
const Page = async ({ params }: ConnectPageProps) => {
const environment = await getEnvironment(params.environmentId);
if (!environment) {
throw new Error("Environment not found");
}
const product = await getProductByEnvironmentId(environment.id);
if (!product) {
throw new Error("Product not found");
}
const channel = product.config.channel;
const industry = product.config.industry;
if (!channel || !industry) {
return notFound();
}
const customHeadline = getCustomHeadline(channel, industry);
return (
<div className="flex min-h-full flex-col items-center justify-center py-10">
<Header
title={`Let's connect your ${customHeadline} with Formbricks`}
subtitle="If you don't do it now, chances are low that you will ever do it!"
/>
<div className="space-y-4 text-center">
<p className="text-4xl font-medium text-slate-800"></p>
<p className="text-sm text-slate-500"></p>
</div>
<ConnectWithFormbricks
environment={environment}
webAppUrl={WEBAPP_URL}
widgetSetupCompleted={
channel === "app" ? environment.appSetupCompleted : environment.websiteSetupCompleted
}
channel={channel}
/>
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
href={`/environments/${environment.id}/`}>
<XIcon className="h-7 w-7" strokeWidth={1.5} />
</Button>
</div>
);
};
export default Page;

View File

@@ -0,0 +1,11 @@
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
export const getCustomHeadline = (channel: TProductConfigChannel, industry: TProductConfigIndustry) => {
const combinations = {
"website+eCommerce": "web shop",
"website+saas": "landing page",
"app+eCommerce": "shopping app",
"app+saas": "SaaS app",
};
return combinations[`${channel}+${industry}`] || "app";
};

View File

@@ -0,0 +1,44 @@
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";
const ProductOnboardingLayout = async ({ children, params }) => {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
}
const isAuthorized = await canUserAccessOrganization(session.user.id, params.organizationId);
if (!isAuthorized) {
throw AuthorizationError;
}
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>
);
};
export default ProductOnboardingLayout;

View File

@@ -0,0 +1,60 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { CircleUserRoundIcon, EarthIcon, SendHorizonalIcon, XIcon } from "lucide-react";
import { getProducts } from "@formbricks/lib/product/service";
import { Button } from "@formbricks/ui/Button";
import { Header } from "@formbricks/ui/Header";
interface ChannelPageProps {
params: {
organizationId: string;
};
}
const Page = async ({ params }: ChannelPageProps) => {
const channelOptions = [
{
title: "Public website",
description: "Display surveys on public websites, well timed and targeted.",
icon: EarthIcon,
iconText: "Built for scale",
href: `/organizations/${params.organizationId}/products/new/industry?channel=website`,
},
{
title: "App with sign up",
description: "Run highly targeted surveys with any user cohort.",
icon: CircleUserRoundIcon,
iconText: "Enrich user profiles",
href: `/organizations/${params.organizationId}/products/new/industry?channel=app`,
},
{
channel: "link",
title: "Anywhere online",
description: "Create link and email surveys, reach your people anywhere.",
icon: SendHorizonalIcon,
iconText: "100% custom branding",
href: `/organizations/${params.organizationId}/products/new/industry?channel=link`,
},
];
const products = await getProducts(params.organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header
title="Where do you want to survey people?"
subtitle="Get started with proven best practices 🚀"
/>
<OnboardingOptionsContainer options={channelOptions} />
{products.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
href={"/"}>
<XIcon className="h-7 w-7" strokeWidth={1.5} />
</Button>
)}
</div>
);
};
export default Page;

View File

@@ -0,0 +1,69 @@
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
import { HeartIcon, MonitorIcon, ShoppingCart, XIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { getProducts } from "@formbricks/lib/product/service";
import { TProductConfigChannel } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { Header } from "@formbricks/ui/Header";
interface IndustryPageProps {
params: {
organizationId: string;
};
searchParams: {
channel?: TProductConfigChannel;
};
}
const Page = async ({ params, searchParams }: IndustryPageProps) => {
const channel = searchParams.channel;
if (!channel) {
return notFound();
}
const products = await getProducts(params.organizationId);
const industryOptions = [
{
title: "E-Commerce",
description: "Implement proven best practices to understand why people buy.",
icon: ShoppingCart,
iconText: "B2B and B2C",
href: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=eCommerce`,
},
{
title: "SaaS",
description: "Gather contextualized feedback to improve product-market fit.",
icon: MonitorIcon,
iconText: "Proven methods",
href: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=saas`,
},
{
title: "Other",
description: "Universal Formricks experience with features for every industry.",
icon: HeartIcon,
iconText: "Customer insights",
href: IS_FORMBRICKS_CLOUD
? `/organizations/${params.organizationId}/products/new/survey?channel=${channel}&industry=other`
: `/organizations/${params.organizationId}/products/new/settings?channel=${channel}&industry=other`,
},
];
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
<Header title="Which industry do you work for?" subtitle="Get started with proven best practices 🚀" />
<OnboardingOptionsContainer options={industryOptions} />
{products.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
href={"/"}>
<XIcon className="h-7 w-7" strokeWidth={1.5} />
</Button>
)}
</div>
);
};
export default Page;

View File

@@ -0,0 +1,165 @@
"use client";
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
import { createProductAction } from "@/app/(app)/environments/[environmentId]/actions";
import { zodResolver } from "@hookform/resolvers/zod";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { toast } from "react-hot-toast";
import { PREVIEW_SURVEY } from "@formbricks/lib/styling/constants";
import {
TProductConfigChannel,
TProductConfigIndustry,
TProductUpdateInput,
ZProductUpdateInput,
} from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { ColorPicker } from "@formbricks/ui/ColorPicker";
import {
FormControl,
FormDescription,
FormError,
FormField,
FormItem,
FormLabel,
FormProvider,
} from "@formbricks/ui/Form";
import { Input } from "@formbricks/ui/Input";
import { SurveyInline } from "@formbricks/ui/Survey";
interface ProductSettingsProps {
organizationId: string;
channel: TProductConfigChannel;
industry: TProductConfigIndustry;
defaultBrandColor: string;
}
export const ProductSettings = ({
organizationId,
channel,
industry,
defaultBrandColor,
}: ProductSettingsProps) => {
const router = useRouter();
const addProduct = async (data: TProductUpdateInput) => {
try {
const product = await createProductAction(organizationId, {
...data,
config: { channel, industry },
});
// get production environment
const productionEnvironment = product.environments.find(
(environment) => environment.type === "production"
);
if (channel !== "link") {
router.push(`/environments/${productionEnvironment?.id}/connect`);
} else {
router.push(`/environments/${productionEnvironment?.id}/surveys`);
}
} catch (error) {
toast.error("Product creation failed");
console.error(error);
}
};
const form = useForm<TProductUpdateInput>({
defaultValues: {
name: "",
styling: { allowStyleOverwrite: true, brandColor: { light: defaultBrandColor } },
},
resolver: zodResolver(ZProductUpdateInput),
});
const logoUrl = form.watch("logo.url");
const brandColor = form.watch("styling.brandColor.light") ?? defaultBrandColor;
const { isSubmitting } = form.formState;
return (
<div className="mt-6 flex w-5/6 space-x-10 lg:w-2/3 2xl:w-1/2">
<div className="flex w-1/2 flex-col space-y-4">
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(addProduct)} className="w-full space-y-4">
<FormField
control={form.control}
name="styling.brandColor.light"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<div>
<FormLabel>Brand color</FormLabel>
<FormDescription>Change the brand color of the survey.</FormDescription>
</div>
<FormControl>
<div>
<ColorPicker
color={field.value || defaultBrandColor}
onChange={(color) => field.onChange(color)}
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormItem className="w-full space-y-4">
<div>
<FormLabel>Product Name</FormLabel>
<FormDescription>
What is your {getCustomHeadline(channel, industry)} called ?
</FormDescription>
</div>
<FormControl>
<div>
<Input
value={field.value}
onChange={(name) => field.onChange(name)}
placeholder="Formbricks Merch Store"
className="bg-white"
autoFocus={true}
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
</FormControl>
</FormItem>
)}
/>
<div className="flex w-full justify-end">
<Button variant="darkCTA" loading={isSubmitting} type="submit">
Next
</Button>
</div>
</form>
</FormProvider>
</div>
<div className="relative flex h-[30rem] w-1/2 flex-col items-center justify-center space-y-2 rounded-lg border bg-slate-200 shadow">
{logoUrl && (
<Image
src={logoUrl}
alt="Logo"
width={256}
height={56}
className="absolute left-2 top-2 -mb-6 h-20 w-auto max-w-64 rounded-lg border object-contain p-1"
/>
)}
<p className="text-sm text-slate-400">Preview</p>
<div className="h-3/4 w-3/4">
<SurveyInline
survey={PREVIEW_SURVEY}
styling={{ brandColor: { light: brandColor } }}
isBrandingEnabled={false}
languageCode="default"
onFileUpload={async (file) => file.name}
autoFocus={false}
/>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,60 @@
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
import { ProductSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings";
import { XIcon } from "lucide-react";
import { notFound } from "next/navigation";
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
import { getProducts } from "@formbricks/lib/product/service";
import { startsWithVowel } from "@formbricks/lib/utils/strings";
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
import { Button } from "@formbricks/ui/Button";
import { Header } from "@formbricks/ui/Header";
interface ProductSettingsPageProps {
params: {
organizationId: string;
};
searchParams: {
channel?: TProductConfigChannel;
industry?: TProductConfigIndustry;
};
}
const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
const channel = searchParams.channel;
const industry = searchParams.industry;
if (!channel || !industry) return notFound();
const customHeadline = getCustomHeadline(channel, industry);
const products = await getProducts(params.organizationId);
return (
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
{channel === "link" ? (
<Header
title="Match your brand, get 2x more responses."
subtitle="When people recognize your brand, they are much more likely to start and complete responses."
/>
) : (
<Header
title={`You run ${startsWithVowel(customHeadline) ? "an " + customHeadline : "a " + customHeadline}, how exciting!`}
subtitle="Get 2x more responses matching surveys with your brand and UI"
/>
)}
<ProductSettings
organizationId={params.organizationId}
channel={channel}
industry={industry}
defaultBrandColor={DEFAULT_BRAND_COLOR}
/>
{products.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="minimal"
href={"/"}>
<XIcon className="h-7 w-7" strokeWidth={1.5} />
</Button>
)}
</div>
);
};
export default Page;

View File

@@ -0,0 +1,48 @@
"use client";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";
import { TProductConfigChannel } from "@formbricks/types/product";
interface OnboardingSurveyProps {
organizationId: string;
channel: TProductConfigChannel;
userId: string;
}
export const OnboardingSurvey = ({ organizationId, channel, userId }: OnboardingSurveyProps) => {
const [isIFrameVisible, setIsIFrameVisible] = useState(false);
const [fadeout, setFadeout] = useState(false);
const router = useRouter();
const handleMessageEvent = (event: MessageEvent) => {
if (event.data === "formbricksSurveyCompleted") {
setFadeout(true); // Start fade-out
setTimeout(() => {
router.push(
`/organizations/${organizationId}/products/new/settings?channel=${channel}&industry=other`
);
}, 800); // Delay the navigation until fade-out completes
}
};
useEffect(() => {
if (isIFrameVisible) {
window.addEventListener("message", handleMessageEvent, false);
return () => {
window.removeEventListener("message", handleMessageEvent, false);
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isIFrameVisible]);
return (
<div
className={`overflow relative flex h-[100vh] flex-col items-center justify-center ${fadeout ? "opacity-0 transition-opacity duration-1000" : "opacity-100"}`}>
<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

@@ -0,0 +1,32 @@
import { OnboardingSurvey } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/survey/components/OnboardingSurvey";
import { getServerSession } from "next-auth";
import { notFound, redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
interface OnboardingSurveyPageProps {
params: {
organizationId: string;
};
searchParams: {
channel?: TProductConfigChannel;
industry?: TProductConfigIndustry;
};
}
const Page = async ({ params, searchParams }: OnboardingSurveyPageProps) => {
const session = await getServerSession(authOptions);
if (!session) {
return redirect(`/auth/login`);
}
const channel = searchParams.channel;
const industry = searchParams.industry;
if (!channel || !industry) return notFound();
return (
<OnboardingSurvey organizationId={params.organizationId} channel={channel} userId={session.user.id} />
);
};
export default Page;

View File

@@ -0,0 +1,61 @@
"use server";
import { getServerSession } from "next-auth";
import { sendInviteMemberEmail } from "@formbricks/email";
import { hasOrganizationAuthority } from "@formbricks/lib/auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { INVITE_DISABLED } from "@formbricks/lib/constants";
import { inviteUser } from "@formbricks/lib/invite/service";
import { verifyUserRoleAccess } from "@formbricks/lib/organization/auth";
import { AuthenticationError } from "@formbricks/types/errors";
import { TMembershipRole } from "@formbricks/types/memberships";
export const inviteOrganizationMemberAction = async (
organizationId: string,
email: string,
role: TMembershipRole,
inviteMessage: string
) => {
const session = await getServerSession(authOptions);
if (!session) {
throw new AuthenticationError("Not authenticated");
}
const isUserAuthorized = await hasOrganizationAuthority(session.user.id, organizationId);
if (INVITE_DISABLED) {
throw new AuthenticationError("Invite disabled");
}
if (!isUserAuthorized) {
throw new AuthenticationError("Not authorized");
}
const { hasCreateOrUpdateMembersAccess } = await verifyUserRoleAccess(organizationId, session.user.id);
if (!hasCreateOrUpdateMembersAccess) {
throw new AuthenticationError("Not authorized");
}
const invite = await inviteUser({
organizationId,
invitee: {
email,
name: "",
role,
},
});
if (invite) {
await sendInviteMemberEmail(
invite.id,
email,
session.user.name ?? "",
"",
true, // is onboarding invite
inviteMessage
);
}
return invite;
};

View File

@@ -0,0 +1,41 @@
import { LucideProps } from "lucide-react";
import Link from "next/link";
import { ForwardRefExoticComponent, RefAttributes } from "react";
import { OptionCard } from "@formbricks/ui/OptionCard";
interface OnboardingOptionsContainerProps {
options: {
title: string;
description: string;
icon: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>;
iconText: string;
href: string;
}[];
}
export const OnboardingOptionsContainer = ({ options }: OnboardingOptionsContainerProps) => {
return (
<div className="grid w-5/6 grid-cols-3 gap-8 text-center lg:w-2/3">
{options.map((option, index) => {
const Icon = option.icon;
return (
<Link href={option.href}>
<OptionCard
size="md"
key={index}
title={option.title}
description={option.description}
loading={false}>
<div className="flex flex-col items-center">
<Icon className="h-16 w-16 text-slate-600" strokeWidth={0.5} />
<p className="mt-4 w-fit rounded-xl bg-slate-200 px-4 text-sm text-slate-700">
{option.iconText}
</p>
</div>
</OptionCard>
</Link>
);
})}
</div>
);
};

View File

@@ -3,7 +3,6 @@ import { PosthogIdentify } from "@/app/(app)/environments/[environmentId]/compon
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { getEnvironment } from "@formbricks/lib/environment/service";
@@ -41,9 +40,7 @@ const EnvLayout = async ({ children, params }) => {
environmentId={params.environmentId}
organizationId={organization.id}
organizationName={organization.name}
inAppSurveyBillingStatus={organization.billing.features.inAppSurvey.status}
linkSurveyBillingStatus={organization.billing.features.linkSurvey.status}
userTargetingBillingStatus={organization.billing.features.userTargeting.status}
organizationBilling={organization.billing}
/>
<FormbricksClient session={session} />
<ToasterClient />

View File

@@ -5,7 +5,6 @@ import { createId } from "@paralleldrive/cuid2";
import * as Collapsible from "@radix-ui/react-collapsible";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TProduct } from "@formbricks/types/product";
@@ -23,7 +22,7 @@ export const AddQuestionButton = ({ addQuestion, product }: AddQuestionButtonPro
onOpenChange={setOpen}
className={cn(
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
"group w-full space-y-2 rounded-lg border border-slate-300 bg-white transition-all duration-300 ease-in-out hover:scale-100 hover:cursor-pointer hover:bg-slate-50"
"group w-full space-y-2 rounded-lg border border-slate-300 bg-white transition-all duration-300 ease-in-out hover:scale-100 hover:cursor-pointer hover:bg-slate-50"
)}>
<Collapsible.CollapsibleTrigger asChild className="group h-full w-full">
<div className="inline-flex">
@@ -36,7 +35,7 @@ export const AddQuestionButton = ({ addQuestion, product }: AddQuestionButtonPro
</div>
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="justify-left flex flex-col ">
<Collapsible.CollapsibleContent className="justify-left flex flex-col">
{/* <hr className="py-1 text-slate-600" /> */}
{questionTypes.map((questionType) => (
<button

View File

@@ -1,8 +1,6 @@
"use client";
import { PlusIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
import { PlusIcon } from "lucide-react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys";
@@ -32,7 +30,6 @@ export const AddressQuestionForm = ({
setSelectedLanguageCode,
attributeClasses,
}: AddressQuestionFormProps): JSX.Element => {
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
return (
@@ -51,7 +48,7 @@ export const AddressQuestionForm = ({
/>
<div>
{showSubheader && (
{question.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
@@ -67,17 +64,9 @@ export const AddressQuestionForm = ({
attributeClasses={attributeClasses}
/>
</div>
<TrashIcon
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => {
setShowSubheader(false);
updateQuestion(questionIdx, { subheader: undefined });
}}
/>
</div>
)}
{!showSubheader && (
{question.subheader === undefined && (
<Button
size="sm"
variant="minimal"
@@ -87,7 +76,6 @@ export const AddressQuestionForm = ({
updateQuestion(questionIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
setShowSubheader(true);
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description

View File

@@ -1,6 +1,5 @@
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
import { LogicEditor } from "./LogicEditor";
import { UpdateQuestionId } from "./UpdateQuestionId";

View File

@@ -3,14 +3,12 @@
import * as Collapsible from "@radix-ui/react-collapsible";
import { CheckIcon } from "lucide-react";
import { UseFormReturn } from "react-hook-form";
import { cn } from "@formbricks/lib/cn";
import { TProductStyling } from "@formbricks/types/product";
import { TSurveyStyling } from "@formbricks/types/surveys";
import { Badge } from "@formbricks/ui/Badge";
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
import { Slider } from "@formbricks/ui/Slider";
import { SurveyBgSelectorTab } from "./SurveyBgSelectorTab";
interface BackgroundStylingCardProps {
@@ -43,7 +41,7 @@ export const BackgroundStylingCard = ({
}}
className={cn(
open ? "" : "hover:bg-slate-50",
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
"w-full space-y-2 rounded-lg border border-slate-300 bg-white"
)}>
<Collapsible.CollapsibleTrigger
asChild
@@ -110,7 +108,7 @@ export const BackgroundStylingCard = ({
<div className="flex flex-col justify-center">
<div className="flex flex-col gap-4">
<div className="flex flex-col justify-center ">
<div className="flex flex-col justify-center">
<FormField
control={form.control}
name="background.brightness"

View File

@@ -1,10 +1,10 @@
import { PlusIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
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";
@@ -31,8 +31,16 @@ export const CalQuestionForm = ({
isInvalid,
attributeClasses,
}: CalQuestionFormProps): JSX.Element => {
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
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>
@@ -49,7 +57,7 @@ export const CalQuestionForm = ({
attributeClasses={attributeClasses}
/>
<div>
{showSubheader && (
{question.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
@@ -65,17 +73,9 @@ export const CalQuestionForm = ({
attributeClasses={attributeClasses}
/>
</div>
<TrashIcon
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => {
setShowSubheader(false);
updateQuestion(questionIdx, { subheader: undefined });
}}
/>
</div>
)}
{!showSubheader && (
{question.subheader === undefined && (
<Button
size="sm"
className="mt-3"
@@ -85,22 +85,47 @@ export const CalQuestionForm = ({
updateQuestion(questionIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
setShowSubheader(true);
}}>
{" "}
<PlusIcon className="mr-1 h-4 w-4" />
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,5 +1,4 @@
import { useState } from "react";
import { ColorPicker } from "@formbricks/ui/ColorPicker";
interface ColorSurveyBgProps {

View File

@@ -152,6 +152,7 @@ export const CreateNewActionTab = ({
reset();
resetAllStates();
toast.success("Action created successfully");
} catch (e: any) {
toast.error(e.message);
}

View File

@@ -1,6 +1,4 @@
import { PlusIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
import { PlusIcon } from "lucide-react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys";
@@ -46,7 +44,6 @@ export const DateQuestionForm = ({
setSelectedLanguageCode,
attributeClasses,
}: IDateQuestionFormProps): JSX.Element => {
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages);
return (
@@ -64,7 +61,7 @@ export const DateQuestionForm = ({
attributeClasses={attributeClasses}
/>
<div>
{showSubheader && (
{question.subheader !== undefined && (
<div className="mt-2 inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
@@ -80,18 +77,10 @@ export const DateQuestionForm = ({
attributeClasses={attributeClasses}
/>
</div>
<TrashIcon
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => {
setShowSubheader(false);
updateQuestion(questionIdx, { subheader: undefined });
}}
/>
</div>
)}
{!showSubheader && (
{question.subheader === undefined && (
<Button
size="sm"
className="mt-3"
@@ -101,9 +90,7 @@ export const DateQuestionForm = ({
updateQuestion(questionIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
setShowSubheader(true);
}}>
{" "}
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
</Button>

View File

@@ -2,7 +2,6 @@
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
@@ -62,7 +61,7 @@ export const EditThankYouCard = ({
return (
<div
className={cn(
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
"group z-20 flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
)}>
<div

View File

@@ -61,7 +61,7 @@ export const EditWelcomeCard = ({
return (
<div
className={cn(
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
"group flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
)}>
<div

View File

@@ -1,9 +1,9 @@
"use client";
import { PlusIcon, TrashIcon, XCircleIcon } from "lucide-react";
import { PlusIcon, XCircleIcon } from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
import { toast } from "react-hot-toast";
import { extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { createI18nString } from "@formbricks/lib/i18n/utils";
import { useGetBillingInfo } from "@formbricks/lib/organization/hooks/useGetBillingInfo";
@@ -27,6 +27,7 @@ interface FileUploadFormProps {
setSelectedLanguageCode: (languageCode: string) => void;
isInvalid: boolean;
attributeClasses: TAttributeClass[];
isFormbricksCloud: boolean;
}
export const FileUploadQuestionForm = ({
@@ -39,9 +40,10 @@ export const FileUploadQuestionForm = ({
selectedLanguageCode,
setSelectedLanguageCode,
attributeClasses,
isFormbricksCloud,
}: FileUploadFormProps): JSX.Element => {
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
const [extension, setExtension] = useState("");
const [isMaxSizeError, setMaxSizeError] = useState(false);
const {
billingInfo,
error: billingInfoError,
@@ -105,7 +107,7 @@ export const FileUploadQuestionForm = ({
return 10;
}
if (billingInfo.features.linkSurvey.status === "active") {
if (billingInfo.plan !== "free") {
// 1GB in MB
return 1024;
}
@@ -113,6 +115,12 @@ export const FileUploadQuestionForm = ({
return 10;
}, [billingInfo, billingInfoError, billingInfoLoading]);
const handleMaxSizeInMBToggle = (checked: boolean) => {
const defaultMaxSizeInMB = isFormbricksCloud ? maxSizeInMBLimit : 1024;
updateQuestion(questionIdx, { maxSizeInMB: checked ? defaultMaxSizeInMB : undefined });
};
return (
<form>
<QuestionFormInput
@@ -128,7 +136,7 @@ export const FileUploadQuestionForm = ({
attributeClasses={attributeClasses}
/>
<div>
{showSubheader && (
{question.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
@@ -144,17 +152,9 @@ export const FileUploadQuestionForm = ({
attributeClasses={attributeClasses}
/>
</div>
<TrashIcon
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => {
setShowSubheader(false);
updateQuestion(questionIdx, { subheader: undefined });
}}
/>
</div>
)}
{!showSubheader && (
{question.subheader === undefined && (
<Button
size="sm"
className="mt-3"
@@ -164,9 +164,7 @@ export const FileUploadQuestionForm = ({
updateQuestion(questionIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
setShowSubheader(true);
}}>
{" "}
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
</Button>
@@ -184,7 +182,7 @@ export const FileUploadQuestionForm = ({
<AdvancedOptionToggle
isChecked={!!question.maxSizeInMB}
onToggle={(checked) => updateQuestion(questionIdx, { maxSizeInMB: checked ? 10 : undefined })}
onToggle={handleMaxSizeInMBToggle}
htmlId="maxFileSize"
title="Max file size"
description="Limit the maximum file size."
@@ -201,8 +199,9 @@ export const FileUploadQuestionForm = ({
onChange={(e) => {
const parsedValue = parseInt(e.target.value, 10);
if (parsedValue > maxSizeInMBLimit) {
if (isFormbricksCloud && parsedValue > maxSizeInMBLimit) {
toast.error(`Max file size limit is ${maxSizeInMBLimit} MB`);
setMaxSizeError(true);
updateQuestion(questionIdx, { maxSizeInMB: maxSizeInMBLimit });
return;
}
@@ -213,6 +212,17 @@ export const FileUploadQuestionForm = ({
/>
MB
</p>
{isMaxSizeError && (
<p className="text-xs text-red-500">
Max file size limit is {maxSizeInMBLimit} MB. If you need more, please{" "}
<Link
className="underline"
target="_blank"
href={`/environments/${localSurvey.environmentId}/settings/billing`}>
upgrade your plan.
</Link>
</p>
)}
</label>
</AdvancedOptionToggle>

View File

@@ -3,7 +3,6 @@
import * as Collapsible from "@radix-ui/react-collapsible";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { cn } from "@formbricks/lib/cn";
import { TSurvey, TSurveyHiddenFields } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
@@ -11,7 +10,6 @@ import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { Switch } from "@formbricks/ui/Switch";
import { Tag } from "@formbricks/ui/Tag";
import { validateId } from "../lib/validation";
interface HiddenFieldsCardProps {
@@ -51,7 +49,7 @@ export const HiddenFieldsCard = ({
return (
<div
className={cn(
open ? "scale-100 shadow-lg " : "scale-97 shadow-md",
open ? "scale-100 shadow-lg" : "scale-97 shadow-md",
"group z-10 flex flex-row rounded-lg bg-white transition-transform duration-300 ease-in-out"
)}>
<div

View File

@@ -1,12 +1,12 @@
"use client";
import * as Collapsible from "@radix-ui/react-collapsible";
import { AlertCircleIcon, CheckIcon, EarthIcon, LinkIcon, MonitorIcon, SmartphoneIcon } from "lucide-react";
import { AlertCircleIcon, BlocksIcon, CheckIcon, EarthIcon, LinkIcon, MonitorIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TEnvironment } from "@formbricks/types/environment";
import { TProduct } from "@formbricks/types/product";
import { TSegment } from "@formbricks/types/segment";
import { TSurvey, TSurveyType } from "@formbricks/types/surveys";
import { Badge } from "@formbricks/ui/Badge";
@@ -17,17 +17,18 @@ interface HowToSendCardProps {
localSurvey: TSurvey;
setLocalSurvey: (survey: TSurvey | ((TSurvey: TSurvey) => TSurvey)) => void;
environment: TEnvironment;
product: TProduct;
}
export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowToSendCardProps) => {
export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment, product }: HowToSendCardProps) => {
const [open, setOpen] = useState(false);
const [widgetSetupCompleted, setWidgetSetupCompleted] = useState(false);
const [appSetupCompleted, setAppSetupCompleted] = useState(false);
const [websiteSetupCompleted, setWebsiteSetupCompleted] = useState(false);
useEffect(() => {
if (environment && environment.widgetSetupCompleted) {
setWidgetSetupCompleted(true);
} else {
setWidgetSetupCompleted(false);
if (environment) {
setAppSetupCompleted(environment.appSetupCompleted);
setWebsiteSetupCompleted(environment.websiteSetupCompleted);
}
}, [environment]);
@@ -77,7 +78,8 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
icon: EarthIcon,
description: "Run targeted surveys on public websites.",
comingSoon: false,
alert: !widgetSetupCompleted,
alert: !websiteSetupCompleted,
hide: product.config.channel && product.config.channel !== "website",
},
{
id: "app",
@@ -85,7 +87,8 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
icon: MonitorIcon,
description: "Embed a survey in your web app to collect responses with user identification.",
comingSoon: false,
alert: !widgetSetupCompleted,
alert: !appSetupCompleted,
hide: product.config.channel && product.config.channel !== "app",
},
{
id: "link",
@@ -94,24 +97,35 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
description: "Share a link to a survey page or embed it in a web page or email.",
comingSoon: false,
alert: false,
hide: false,
},
{
id: "mobile",
name: "Mobile App Survey",
icon: SmartphoneIcon,
description: "Survey users inside a mobile app (iOS & Android).",
id: "headless",
name: "Headless Survey",
icon: BlocksIcon,
description: "Use Formbricks API only and create your own frontend experience.",
comingSoon: true,
alert: false,
hide: false,
},
];
const promotedFeaturesString =
product.config.channel === "website"
? "app"
: product.config.channel === "app"
? "website"
: product.config.channel === "link"
? "app or website"
: "";
return (
<Collapsible.Root
open={open}
onOpenChange={setOpen}
className={cn(
open ? "" : "hover:bg-slate-50",
"w-full space-y-2 rounded-lg border border-slate-300 bg-white "
"w-full space-y-2 rounded-lg border border-slate-300 bg-white"
)}>
<Collapsible.CollapsibleTrigger
asChild
@@ -126,7 +140,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
</div>
<div>
<p className="font-semibold text-slate-800">Survey Type</p>
<p className="mt-1 text-sm text-slate-500">Choose between website, in-app or link survey.</p>
<p className="mt-1 text-sm text-slate-500">Choose where to run the survey.</p>
</div>
</div>
</Collapsible.CollapsibleTrigger>
@@ -138,66 +152,79 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
value={localSurvey.type}
onValueChange={setSurveyType}
className="flex flex-col space-y-3">
{options.map((option) => (
<Label
key={option.id}
htmlFor={option.id}
className={cn(
"flex w-full items-center rounded-lg border bg-slate-50 p-4",
option.comingSoon
? "border-slate-200 bg-slate-50/50"
: option.id === localSurvey.type
? "border-brand-dark cursor-pointer bg-slate-50"
: "cursor-pointer bg-slate-50"
)}
id={`howToSendCardOption-${option.id}`}>
<RadioGroupItem
value={option.id}
id={option.id}
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
disabled={option.comingSoon}
/>
<div className=" inline-flex items-center">
<option.icon className="mr-4 h-8 w-8 text-slate-500" />
<div>
<div className="inline-flex items-center">
<p
className={cn(
"font-semibold",
option.comingSoon ? "text-slate-500" : "text-slate-800"
)}>
{option.name}
</p>
{option.comingSoon && (
<Badge text="coming soon" size="normal" type="success" className="ml-2" />
{options
.filter((option) => !Boolean(option.hide))
.map((option) => (
<Label
key={option.id}
htmlFor={option.id}
className={cn(
"flex w-full items-center rounded-lg border bg-slate-50 p-4",
option.comingSoon
? "border-slate-200 bg-slate-50/50"
: option.id === localSurvey.type
? "border-brand-dark cursor-pointer bg-slate-50"
: "cursor-pointer bg-slate-50"
)}
id={`howToSendCardOption-${option.id}`}>
<RadioGroupItem
value={option.id}
id={option.id}
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
disabled={option.comingSoon}
/>
<div className="inline-flex items-center">
<option.icon className="mr-4 h-8 w-8 text-slate-500" />
<div>
<div className="inline-flex items-center">
<p
className={cn(
"font-semibold",
option.comingSoon ? "text-slate-500" : "text-slate-800"
)}>
{option.name}
</p>
{option.comingSoon && (
<Badge text="coming soon" size="normal" type="success" className="ml-2" />
)}
</div>
<p className="mt-2 text-xs font-normal text-slate-600">{option.description}</p>
{option.alert && (
<div className="mt-2 flex items-center space-x-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-2">
<AlertCircleIcon className="h-5 w-5 text-amber-500" />
<div className="text-amber-800">
<p className="text-xs font-semibold">
Your {option.id} is not yet connected to Formbricks.
</p>
<p className="text-xs font-normal">
<Link
href={`/environments/${environment.id}/product/${option.id}-connection`}
className="underline hover:text-amber-900"
target="_blank">
Connect Formbricks
</Link>{" "}
and launch surveys in your {option.id}.
</p>
</div>
</div>
)}
</div>
<p className="mt-2 text-xs font-normal text-slate-600">{option.description}</p>
{option.alert && (
<div className="mt-2 flex items-center space-x-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-2">
<AlertCircleIcon className="h-5 w-5 text-amber-500" />
<div className=" text-amber-800">
<p className="text-xs font-semibold">
Your app is not yet connected to Formbricks.
</p>
<p className="text-xs font-normal">
<Link
href={`/environments/${environment.id}/product/setup`}
className="underline hover:text-amber-900"
target="_blank">
Connect Formbricks
</Link>{" "}
and launch surveys in your app or website.
</p>
</div>
</div>
)}
</div>
</div>
</Label>
))}
</Label>
))}
</RadioGroup>
</div>
{promotedFeaturesString && (
<div className="mt-2 flex items-center space-x-3 rounded-b-lg border border-slate-200 bg-slate-50/50 px-4 py-2">
<AlertCircleIcon className="h-5 w-5 text-slate-500" />
<div className="text-slate-500">
<p className="text-xs">
You can also use Formbricks to run {promotedFeaturesString} surveys. Create a new product for
your {promotedFeaturesString} to use this feature.
</p>
</div>
</div>
)}
</Collapsible.CollapsibleContent>
</Collapsible.Root>
);

View File

@@ -285,11 +285,13 @@ export const LogicEditor = ({
<div className="mt-2 space-y-3">
{question?.logic?.map((logic, logicIdx) => (
<div key={logicIdx} className="flex items-center space-x-2 space-y-1 text-xs xl:text-sm">
<CornerDownRightIcon className="h-4 w-4" />
<div>
<CornerDownRightIcon className="h-4 w-4" />
</div>
<p className="text-slate-800">If this answer</p>
<Select value={logic.condition} onValueChange={(e) => updateLogic(logicIdx, { condition: e })}>
<SelectTrigger className=" min-w-fit flex-1">
<SelectTrigger className="min-w-fit flex-1">
<SelectValue placeholder="Select condition" className="text-xs lg:text-sm" />
</SelectTrigger>
<SelectContent>
@@ -367,7 +369,7 @@ export const LogicEditor = ({
<Select
value={logic.destination}
onValueChange={(e) => updateLogic(logicIdx, { destination: e })}>
<SelectTrigger className="w-fit overflow-hidden ">
<SelectTrigger className="w-fit overflow-hidden">
<SelectValue placeholder="Select question" />
</SelectTrigger>
<SelectContent>
@@ -391,11 +393,12 @@ export const LogicEditor = ({
<SelectItem value="end">End of survey</SelectItem>
</SelectContent>
</Select>
<TrashIcon
className="h-4 w-4 cursor-pointer text-slate-400"
onClick={() => deleteLogic(logicIdx)}
/>
<div>
<TrashIcon
className="h-4 w-4 cursor-pointer text-slate-400"
onClick={() => deleteLogic(logicIdx)}
/>
</div>
</div>
))}
<div className="flex flex-wrap items-center space-x-2 py-1 text-sm">

View File

@@ -1,16 +1,13 @@
"use client";
import { PlusIcon, TrashIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "react-hot-toast";
import { createI18nString, extractLanguageCodes, getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { TI18nString, TSurvey, TSurveyMatrixQuestion } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Label } from "@formbricks/ui/Label";
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
import { isLabelValidForAllLanguages } from "../lib/validation";
interface MatrixQuestionFormProps {
@@ -35,7 +32,6 @@ export const MatrixQuestionForm = ({
setSelectedLanguageCode,
attributeClasses,
}: MatrixQuestionFormProps): JSX.Element => {
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
const languageCodes = extractLanguageCodes(localSurvey.languages);
// Function to add a new Label input field
const handleAddLabel = (type: "row" | "column") => {
@@ -117,7 +113,7 @@ export const MatrixQuestionForm = ({
attributeClasses={attributeClasses}
/>
<div>
{showSubheader && (
{question.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
@@ -133,23 +129,19 @@ export const MatrixQuestionForm = ({
attributeClasses={attributeClasses}
/>
</div>
<TrashIcon
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => {
setShowSubheader(false);
updateQuestion(questionIdx, { subheader: undefined });
}}
/>
</div>
)}
{!showSubheader && (
{question.subheader === undefined && (
<Button
size="sm"
variant="minimal"
className="mt-3"
type="button"
onClick={() => setShowSubheader(true)}>
onClick={() => {
updateQuestion(questionIdx, {
subheader: createI18nString("", languageCodes),
});
}}>
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
</Button>

View File

@@ -3,7 +3,7 @@
import { DndContext } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { createId } from "@paralleldrive/cuid2";
import { PlusIcon, TrashIcon } from "lucide-react";
import { PlusIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
@@ -45,7 +45,6 @@ export const MultipleChoiceQuestionForm = ({
}: OpenQuestionFormProps): JSX.Element => {
const lastChoiceRef = useRef<HTMLInputElement>(null);
const [isNew, setIsNew] = useState(true);
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
const [isInvalidValue, setisInvalidValue] = useState<string | null>(null);
const questionRef = useRef<HTMLInputElement>(null);
@@ -196,7 +195,7 @@ export const MultipleChoiceQuestionForm = ({
/>
<div>
{showSubheader && (
{question.subheader !== undefined && (
<div className="inline-flex w-full items-center">
<div className="w-full">
<QuestionFormInput
@@ -212,17 +211,9 @@ export const MultipleChoiceQuestionForm = ({
attributeClasses={attributeClasses}
/>
</div>
<TrashIcon
className="ml-2 mt-10 h-4 w-4 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => {
setShowSubheader(false);
updateQuestion(questionIdx, { subheader: undefined });
}}
/>
</div>
)}
{!showSubheader && (
{question.subheader === undefined && (
<Button
size="sm"
variant="minimal"
@@ -232,9 +223,7 @@ export const MultipleChoiceQuestionForm = ({
updateQuestion(questionIdx, {
subheader: createI18nString("", surveyLanguageCodes),
});
setShowSubheader(true);
}}>
{" "}
<PlusIcon className="mr-1 h-4 w-4" />
Add Description
</Button>

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