Compare commits

..

28 Commits

Author SHA1 Message Date
pandeymangg
2254c24cad fix: github action 2024-05-07 16:41:46 +05:30
pandeymangg
0f28cea7c8 fix: github action 2024-05-07 16:38:47 +05:30
pandeymangg
868ee71e34 fix: github action 2024-05-07 16:33:29 +05:30
pandeymangg
1dbbaca8fd fix: github action 2024-05-07 16:32:06 +05:30
pandeymangg
8b895b4b43 Merge branch 'main' into piyush/enterprise-license-check 2024-05-07 15:44:14 +05:30
pandeymangg
f81eae3691 fix: github action 2024-05-07 14:59:32 +05:30
pandeymangg
317013eee7 fix 2024-05-07 14:25:26 +05:30
pandeymangg
d19470f71a fix: e2e enbv 2024-05-07 13:56:21 +05:30
pandeymangg
3bf5693af1 fix: adds e2e check fallback for test environment 2024-05-07 13:30:56 +05:30
Matti Nannt
70cf58b55c Merge branch 'main' into piyush/enterprise-license-check 2024-05-06 18:00:45 +02:00
Anshuman Pandey
68fc25587c Merge branch 'main' into piyush/enterprise-license-check 2024-05-06 17:39:17 +05:30
pandeymangg
4c54c0d934 Merge branch 'main' into piyush/enterprise-license-check 2024-05-06 17:20:49 +05:30
pandeymangg
93b49969f9 fix: removes logs, refactor 2024-05-06 17:18:43 +05:30
pandeymangg
2839e49ccb testing 2024-05-06 16:52:57 +05:30
pandeymangg
e1dae1bd98 Merge branch 'main' into piyush/enterprise-license-check 2024-05-06 16:33:46 +05:30
pandeymangg
606305e54b Merge branch 'main' into piyush/enterprise-license-check 2024-05-03 15:55:45 +05:30
pandeymangg
0d1ded1139 Merge branch 'main' into piyush/enterprise-license-check 2024-05-03 14:06:20 +05:30
pandeymangg
6b1b2895f8 fix: feedback 2024-05-03 14:05:59 +05:30
pandeymangg
a3cb37b128 refactor 2024-05-02 15:06:07 +05:30
pandeymangg
b27314cec6 fix: caching for license key check 2024-05-02 15:03:58 +05:30
pandeymangg
1891f286e7 wip 2024-05-01 15:36:03 +05:30
pandeymangg
48409ced60 fix: short circuit logix 2024-05-01 11:41:36 +05:30
pandeymangg
9a7e5bfa8d Merge branch 'main' into piyush/enterprise-license-check 2024-05-01 11:36:23 +05:30
Piyush Gupta
adcba0139a Merge branch 'main' of https://github.com/formbricks/formbricks into piyush/enterprise-license-check 2024-04-16 09:53:19 +05:30
Piyush Gupta
0d3f7103af refactoring of getIsEnterpriseEdition service 2024-04-15 15:01:21 +05:30
Piyush Gupta
a51d68d23f Merge branch 'main' of https://github.com/formbricks/formbricks into piyush/enterprise-license-check 2024-04-15 09:09:21 +05:30
Piyush Gupta
0d33c27295 used unstable_cache instead of custom cache 2024-04-12 16:29:00 +05:30
Piyush Gupta
4fa4528771 adds enterprise license check 2024-04-12 15:02:07 +05:30
469 changed files with 7655 additions and 8114 deletions

View File

@@ -16,6 +16,9 @@ runs:
- uses: ./.github/actions/dangerous-git-checkout
- run: echo "E2E Testing Mode is ${{ inputs.e2e_testing_mode }}"
shell: bash
- name: Cache Build
uses: actions/cache@v3
id: cache-build

View File

@@ -2,6 +2,9 @@ name: E2E Tests
on:
workflow_call:
workflow_dispatch:
push:
branches:
- action-test
jobs:
build:
name: Run E2E Tests

View File

@@ -33,7 +33,9 @@ jobs:
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3.5.0
uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1
with:
cosign-release: "v2.1.1"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3

View File

@@ -41,7 +41,9 @@ jobs:
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3.5.0
uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1
with:
cosign-release: "v2.1.1"
# Login against a Docker registry except on PR
# https://github.com/docker/login-action

View File

@@ -13,16 +13,12 @@ export const SurveySwitch = ({ value, formbricks }: SurveySwitchProps) => {
formbricks.logout();
window.location.href = `/${v}`;
}}>
<SelectTrigger className="w-[180px] px-4">
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
<SelectItem value="website" className="h-10 px-4 hover:bg-slate-100">
Website Surveys
</SelectItem>
<SelectItem value="app" className="hover:bg-slate-10 h-10 px-4">
App Surveys
</SelectItem>
<SelectItem value="website">Website Surveys</SelectItem>
<SelectItem value="app">App Surveys</SelectItem>
</SelectContent>
</Select>
);

View File

@@ -13,10 +13,10 @@
"dependencies": {
"@formbricks/js": "workspace:*",
"@formbricks/ui": "workspace:*",
"lucide-react": "^0.378.0",
"lucide-react": "^0.373.0",
"next": "14.2.3",
"react": "18.3.1",
"react-dom": "18.3.1"
"react": "18.2.0",
"react-dom": "18.2.0"
},
"devDependencies": {
"eslint-config-formbricks": "workspace:*",

View File

@@ -115,7 +115,7 @@ export default function AppPage({}) {
</div>
<div className="md:grid md:grid-cols-3">
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-800">
<div className="col-span-3 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-800">
<h3 className="text-lg font-semibold dark:text-white">
Reset person / pull data from Formbricks app
</h3>
@@ -136,6 +136,26 @@ export default function AppPage({}) {
</p>
</div>
<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"
onClick={() => {
formbricks.track("Code Action");
}}>
Code Action
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends a{" "}
<a href="https://formbricks.com/docs/actions/code" className="underline" target="_blank">
Code Action
</a>{" "}
to the Formbricks API called &apos;Code Action&apos;. You will find it in the Actions Tab.
</p>
</div>
</div>
<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">

View File

@@ -115,7 +115,7 @@ export default function AppPage({}) {
</div>
<div className="md:grid md:grid-cols-3">
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-800">
<div className="col-span-3 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-800">
<h3 className="text-lg font-semibold dark:text-white">
Reset person / pull data from Formbricks app
</h3>
@@ -135,6 +135,60 @@ export default function AppPage({}) {
try again.
</p>
</div>
<div className="pt-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"
onClick={() => {
formbricks.track("New Session");
}}>
Track New Session
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends an Action to the Formbricks API called &apos;New Session&apos;. You will
find it in the Actions Tab.
</p>
</div>
</div>
<div className="pt-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"
onClick={() => {
formbricks.track("Exit Intent");
}}>
Track Exit Intent
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends an Action to the Formbricks API called &apos;Exit Intent&apos;. You can also
move your mouse to the top of the browser to trigger the exit intent.
</p>
</div>
</div>
<div className="pt-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"
onClick={() => {
formbricks.track("50% Scroll");
}}>
Track 50% Scroll
</button>
</div>
<div>
<p className="text-xs text-slate-700 dark:text-slate-300">
This button sends an Action to the Formbricks API called &apos;50% Scroll&apos;. You can also
scroll down to trigger the 50% scroll.
</p>
</div>
</div>
</div>
</div>
</div>

View File

@@ -35,4 +35,3 @@ yarn-error.log*
next-env.d.ts
public/sitemap*.xml
public/robots.txt

View File

@@ -1,5 +1,4 @@
import { MdxImage } from "@/components/MdxImage";
import { ResponsiveVideo } from "@/components/ResponsiveVideo";
import GermansGpt from "./germans-gpt.webp";
import Hni from "./hni.webp";
@@ -19,9 +18,13 @@ export const metadata = {
Advanced Targeting allows you to show surveys to the right group of people. You can target surveys based on user attributes, user events, and more instead of spraying and praying. This helps you get more relevant feedback and make data-driven decisions. All of this without writing a single line of code.
<ResponsiveVideo title="Formbricks Multi-language Surveys"
src="https://www.youtube-nocookie.com/embed/0BQp6N4cXzU?si=KeBM7G7Ch1xtrsOm&amp;controls=0" />
<iframe
width="700"
height="450"
src="https://www.youtube.com/embed/0BQp6N4cXzU?si=gyeEZRXZ6Kei1zzm"
title="YouTube video player: Formbricks"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"></iframe>
## How to setup Advanced Targeting

View File

@@ -2,6 +2,7 @@ import { MdxImage } from "@/components/MdxImage";
import { Libraries } from "./components/Libraries";
import SetupChecklist from "./images/env-id.png";
import ReactApp from "./images/react-in-app-survey-app-popup-form.webp";
import WidgetConnected from "./images/widget-connected.webp";
import WidgetNotConnected from "./images/widget-not-connected.webp";
@@ -26,7 +27,14 @@ for something else, please [join our Discord!](https://formbricks.com/discord) a
Before getting started, make sure you have:
1. A web application (behind your user authentication system) in your desired framework is set up and running.
2. A Formbricks account with access to your environment ID and API host. You can find these in the **Setup Checklist** in the Settings.
2. A Formbricks account with access to your environment ID and API host. You can find these in the **Setup Checklist** in the Settings:
<MdxImage
src={SetupChecklist}
alt="Step 2 - Setup Checklist"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
---

View File

@@ -41,8 +41,8 @@ To run the Churn Survey in your app you want to proceed as follows:
4. Prevent that churn!
<Note>
## Formbricks Widget running?
We assume that you have already installed the Formbricks Widget in your web app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web
app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
(takes 15mins max.)](/app-surveys/quickstart)
</Note>

View File

@@ -38,8 +38,8 @@ To run the Feature Chaser survey in your app you want to proceed as follows:
2. Setup a user action to display survey at the right point in time
<Note>
## Formbricks Widget running?
We assume that you have already installed the Formbricks Widget in your web wapp. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web
app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
(takes 15mins max.)](/app-surveys/quickstart)
</Note>

View File

@@ -38,8 +38,8 @@ To display the Trial Conversion Survey in your app you want to proceed as follow
3. Print that 💸
<Note>
## Formbricks Widget running?
We assume that you have already installed the Formbricks Widget in your web app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web
app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
(takes 15mins max.)](/app-surveys/quickstart)
</Note>

View File

@@ -43,8 +43,8 @@ To display an Interview Prompt in your app you want to proceed as follows:
3. Thats it! 🎉
<Note>
## Formbricks Widget running?
We assume that you have already installed the Formbricks Widget in your web app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web
app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
(15mins).](/app-surveys/quickstart)
</Note>
@@ -86,9 +86,8 @@ Save, and move over to the “Audience” tab.
### 3. Pre-segment your audience (coming soon)
<Note>
## Filter by attribute coming soon.
We're working on pre-segmenting users by attributes. We will update this
manual in the next few days.
## Filter by attribute coming soon We're working on pre-segmenting users by attributes. We will update this
manual in the next few days.
</Note>
Once you clicked over to the “Audience” tab you can change the settings. In the **Who To Send** card, select “Filter audience by attribute”. This allows you to only show the prompt to a specific segment of your user base.
@@ -162,10 +161,9 @@ Scroll down to “Recontact Options”. Here you have to choose the correct sett
<MdxImage src={Publish} alt="Publish survey" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
<Note>
## Formbricks Widget running?
You need to have the Formbricks Widget installed to display the Feedback Box
in your app. Please follow [this tutorial (Step 4 onwards)](/app-surveys/quickstart)
to install the widget.
## Formbricks Widget running? You need to have the Formbricks Widget installed to display the Feedback Box
in your app. Please follow [this tutorial (Step 4 onwards)](/app-surveys/quickstart)
to install the widget.
</Note>
###

View File

@@ -37,8 +37,8 @@ To display the Product-Market Fit survey in your app you want to proceed as foll
3. Setup the user action to display survey at good point in time
<Note>
## Formbricks Widget running?
We assume that you have already installed the Formbricks Widget in your web app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web
app. Its required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
(15mins).](/app-surveys/quickstart)
</Note>

View File

@@ -1,4 +1,5 @@
import { MdxImage } from "@/components/MdxImage";
import FAQ from "./components/FAQ";
import GitpodAuth from "./images/gitpod/auth.webp";
import GitpodNewWorkspace from "./images/gitpod/new-workspace.webp";
@@ -469,3 +470,10 @@ This happens when you're using the Demo App and delete the Person within the For
<MdxImage src={Logout} alt="Logout Person" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
---
# Frequently Asked Questions
Here you'll find help with frequently recurring problems. If you can't find an answer to your question, please join our [Discord server](https://formbricks.com/discord).
<FAQ />

View File

@@ -59,6 +59,6 @@ Thats it! Your webhooks will not start receiving data as soon as it arrives!
className="max-w-full rounded-lg sm:max-w-3xl"
/>
- API: Use our documented methods on Creation, List, & Deletion endpoints of the Webhook API mentioned in the [Postman Documenter](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#62e6ec65-021b-42a4-ac93-d1434b393c6c)
- API: Use our documented methods on Creation, List, & Deletion endpoints of the Webhook API mentioned here: https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#62e6ec65-021b-42a4-ac93-d1434b393c6c
---

View File

@@ -1,6 +1,5 @@
import { MdxImage } from "@/components/MdxImage";
import { ResponsiveVideo } from "@/components/ResponsiveVideo";
import AddLanguageInSurvey from "./add-language-in-survey.webp";
import AddLanguages from "./add-languages.webp";
import EditMultiLang from "./edit-multi-lang.webp";
@@ -32,9 +31,13 @@ How to deliver a specific language depends on the survey type (app or link surve
details on the Formbricks Cloud.
</Note>
<ResponsiveVideo title="Formbricks Multi-language Surveys"
src="https://www.youtube-nocookie.com/embed/Vol5zjYIoos?si=bdP2y3uk8-xR0uSD&amp;controls=0" />
<iframe
width="700"
height="450"
src="https://www.youtube.com/embed/Vol5zjYIoos?si=FOeDSqcy_OgtaUyM"
title="YouTube video player: Formbricks"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"></iframe>
---

View File

@@ -7,25 +7,27 @@ import Trigger from "./trigger.webp";
import { MdxImage } from "@/components/MdxImage";
export const metadata = {
title: "Formbricks Components Overview",
title: "Inside Look: Formbricks In-Product Micro-Surveys",
description:
"Formbricks is broadly composed of four components: An open source form builder, targeting & triggers, integrations and analytics & insights.",
"Unlock the full potential of Formbricks: From intuitive form-building and event-based triggers to effortless integrations and deep analytics. Master the art of in-product surveys for your SaaS or digital platform.",
};
#### Introduction
# How Formbricks works
Formbricks is broadly composed of four elements, which enable gathering, analyzing and reporting of experience data:
Formbricks is a powerful platform designed to help you create and manage in-product micro-surveys for SaaS and digital products. Here is an overview:
1. **Survey builder**: Create and customize your surveys with a user-friendly, no-code interface.
2. **Targeting & Triggers**: Define user segments based on attributes and set event-based triggers to display your surveys to the right users at the right time.
3. **Integrations**: Seamlessly integrate Formbricks with your current stack using the provided SDKs, native integrations or open APIs.
### Four components
1. **Form Builder**: Create and customize your surveys with a user-friendly, no-code interface.
2. **Targeting & Triggers**: Define specific user segments and set event-based triggers to display your surveys to the right users at the right time.
3. **Integration**: Seamlessly integrate Formbricks into your web or mobile application using the provided SDKs or the HTML snippet.
4. **Analytics & Insights**: Analyze user responses and gain actionable insights to make informed product decisions.
## Survey builder
## Form Builder
The survey builder is where you create and customize your surveys. With its intuitive drag-and-drop interface, you can easily add different question types, set response options, and apply your branding to the survey forms. The survey builder allows you to preview your survey in real-time, ensuring it looks and feels perfect for your users.
The Form Builder is where you create and customize your micro-surveys. With its intuitive drag-and-drop interface, you can easily add different question types, set response options, and apply your branding to the survey forms. The Form Builder allows you to preview your survey in real-time, ensuring it looks and feels perfect for your users.
<MdxImage
src={FormBuilder}
@@ -54,7 +56,7 @@ Formbricks offers fine-grained user targeting and event-based triggers to help y
## Integration
Integrating Formbricks into your web is a breeze. With SDKs for popular web frameworks like React, and an HTML snippet for non-framework based websites, you can quickly add Formbricks to your project. The provided code snippets make it easy to initialize the Formbricks widget and configure it to communicate with your backend.
Integrating Formbricks into your web or mobile application is a breeze. With SDKs for popular web frameworks like React, and an HTML snippet for non-framework based websites, you can quickly add Formbricks to your project. The provided code snippets make it easy to initialize the Formbricks widget and configure it to communicate with your backend.
<MdxImage
src={integrations}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -4,9 +4,9 @@ import { HeroPattern } from "@/components/HeroPattern";
import { GettingStarted } from "./components/GettingStarted";
export const metadata = {
title: "Formbricks: Open Source Experience Management",
title: "Formbricks: Privacy-first Experience Management",
description:
"Formbricks is a versatile open source survey platform with an Experience Management Suite built on top of it. Survey customers, users or employee at any points with a perfectly timed and targeted survey.",
"Enhance your product with Formbricks the leading open-source solution for in-product micro-surveys. Dive deep into user research, amplify product-market fit, and uncover the 'why' behind your analytics.",
};
export const sections = [];
@@ -15,40 +15,26 @@ export const sections = [];
# Formbricks Open Source Experience Management
Welcome to Formbricks! Formbricks is a versatile open source survey platform with an Experience Management Suite built on top of it. Survey customers, users or employee at any points with a perfectly timed and targeted survey. {{ className: 'lead' }}
Welcome to Formbricks, your go-to solution for in-product micro-surveys that will supercharge your product experience! 🚀 {{ className: 'lead' }}
<div className="mb-16 mt-6 flex gap-3" id="why-formbricks">
<Button href="/app-surveys/quickstart" arrow="right" children="Quickstart" />
</div>
## Formbricks - The Open Source Survey Platform
## Why Formbricks? 🤔
The foundation of Formbricks is an open source (AGPLv3) survey platform. Our objective is to built a survey tool which can be used to survey any stakeholder of an organisation (user, customer, employee, etc.) at any point on any platform.
Natively embed qualitative user research into your B2B SaaS. Leverage Best Practices for user discovery to increase Product-Market Fit. {{ className: 'lead' }}
Today, you can already replace many of the existing surveying solutions with Formbricks:
- **Standalone surveys (share via link):** Replace Google Forms, Typeform or any other link survey tool [with Formbricks Form Builder](https://formbricks.com/open-source-form-builder). Use lots of question types and comprehensive customisations.
- **Scalable website surveys:** Even if you have millions of website visitors, Formbricks lets you run well-timed and anonymously targeted [surveys on any public website.](https://formbricks.com/website-survey)
- **Highly targeted app surveys:** Identify known users with Formbricks and enrich their profiles with attributes and specific actions. Build cohorts for [highly targeted in app surveys.](https://formbricks.com/in-app-survey)
The surveying platform is **largely free, also for commercial use.** As we further develop the product offering, all surveying capacity will move into the forever free Community Edition.
## Formbricks - The XM Solution
To fund the development of the most powerful and versatile surveying platform there is, we're building a commercial offering on top of the surveying platform: The Formbricks Experience Management (XM) Suite.
- **What is XM?** "Experience Management" describes the effort to gather, analyze and report data from any stakeholder (customers, users, employees, etc.) or an organisation to measure and then manage how their experience with the organisation is developing.
- **Why are we excited about XM?** Empowering companies, governments and nonprofit organisations to measure how customers, citizens, employees or visitors experience their products and services is a meaningful undertaking. Life is too short for poorly managed service offerings. Formbricks XM provides the data to make human-centric decisions at scale.
- **How does XM work on Formbricks?** Essentially, through reduction and contextualisation. In an app-like format, we strip away everything you don't need to measure a specific experience. We also provide meaningful context in matching templates, reports and best practices.
So far, we have spent most of our time and energy building out the open source survey platform which powers the above. Stick around to see how Formbricks XM Apps will empower everyone to think and work human-centric.
- 🎯 **Tailor-made for SaaS & digital products**: Craft stunning, highly configurable surveys that enable better product decisions, deep user segmentation, and personalization.
- 🌐 **Platform agnostic**: Seamlessly integrate Formbricks surveys into web, mobile, or desktop applications.
- 📊 **Complete the analytics puzzle**: Answer the "why" behind your product analytics with insightful data analysis and visualization tools.
- 🧪 **Smart triggering**: Show the right survey at the right time with event-based triggers for accurate research and well-defined priorities.
- 🎉 **Open-source and self-hosted**: Enjoy full control over your data and infrastructure with our AGPL-licensed solution, and stay tuned for our upcoming cloud version!
<div>
<Button
href="https://app.formbricks.com/"
variant="primary"
variant="text"
arrow="right"
target="_blank"
children="Try Formbricks Cloud"

View File

@@ -0,0 +1,24 @@
export const metadata = {
title: "Formbricks vs. Generic Survey Tools: A Comparative Insight",
description:
"Discover how Formbricks excels as a specialized in-product micro-survey platform for SaaS. Get unmatched targeting, seamless integrations, and make informed decisions with our open-source advantage.",
};
#### Introduction
# Why is Formbricks better?
Formbricks outshines other survey tools by specializing in in-product micro-surveys for SaaS and digital products. With Formbricks, you're better equipped to understand user behavior, improve your product, and make data-driven decisions. Let's see how Formbricks compares to generic survey tools.
| Feature | Generic Survey Tool | Formbricks In-Product Surveys |
| --------------------------- | ------------------- | ----------------------------- |
| Designed for SaaS | ❌ | ✅ |
| In-product micro-surveys | ⚠️ (limited) | ✅ |
| Customizable forms | ✅ | ✅ |
| Fine-grained user targeting | ❌ | ✅ |
| Seamless integrations | ⚠️ (limited) | ✅ |
| Open-source | ❌ | ✅ |
| Event-based triggers | ❌ | ✅ |
| User segmentation | ❌ | ✅ |
With Formbricks, you're not just getting another survey tool, but an in-depth, data-driven solution tailor-made for digital products 🎉

View File

@@ -1,30 +0,0 @@
export const metadata = {
title: "Why Formbricks is Open Source",
description:
"Open source software beats proprietary software in every aspect - except for value capture. We're investing in growing the value creation of our open source platform because it directly translates into business with large organisations.",
};
#### Introduction
# Why is Formbricks open source?
A lot has been written on why open source software beats proprietary software in all aspects - except for value capture for the company investing into its development. While this definitely poses a challenge for a profit-oriented organisation, it's also an interesting opportunity: Due to the open nature of our platform, it's usage is significantly higher. Capturing a small part of the value our platform generates translates into a decently-sized business.
| Advantage | Open Source Software | Proprietary Software |
|----------------------|----------------------------------------------------|----------------------------------------------------------|
| **Data Privacy** | Self-host for maximum control over data | Dependent on thrid party data processor. |
| **Cost** | Often free or significantly lower cost. | Typically requires a purchase or subscription. |
| **Customizability** | Code can be modified to meet specific needs. | Limited customization, restricted to developer's features.|
| **Security**| Frequent community reviews identify vulnerabilities quickly. | Security updates depend on vendor's schedule and interest.|
| **Flexibility**| Supports a wide range of applications and integrations. | Designed for specific environments and integrations. |
| **Community Support**| Large, active communities offer free support and resources. | Paid customer support with limited community help. |
| **Innovation** | Fosters rapid innovation through community contributions. | Innovations depend on vendor's vision and development team.|
| **Licensing** | Permissive licenses allow broad usage and modification. | Strict licensing with limited redistribution rights. |
| **Independence** | Not dependent on a single vendor or developer. | Vendor lock-in can limit future choices. |
| **Transparency** | Full visibility into the code base and development. | Closed-source, code is hidden from users. |
| **Interoperability**| Supports open standards, ensuring interoperability. | Often requires additional software or plugins for compatibility. |

View File

@@ -4,7 +4,6 @@ import { type Section } from "@/components/SectionProvider";
import "@/styles/tailwind.css";
import glob from "fast-glob";
import { type Metadata } from "next";
import { Jost } from "next/font/google";
export const metadata: Metadata = {
title: {
@@ -13,8 +12,6 @@ export const metadata: Metadata = {
},
};
const jost = Jost({ subsets: ["latin"] });
export default async function RootLayout({ children }: { children: React.ReactNode }) {
let pages = await glob("**/*.mdx", { cwd: "src/app" });
let allSectionsEntries = (await Promise.all(
@@ -27,7 +24,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
return (
<html lang="en" className="h-full" suppressHydrationWarning>
<body className={`flex min-h-full bg-white antialiased dark:bg-zinc-900 ${jost.className}`}>
<body className="flex min-h-full bg-white antialiased dark:bg-zinc-900">
<Providers>
<div className="w-full">
<Layout allSections={allSections}>{children}</Layout>

View File

@@ -1,5 +1,4 @@
import { MdxImage } from "@/components/MdxImage";
import { ResponsiveVideo } from "@/components/ResponsiveVideo";
import ShareLink from "./share-link.webp";
import ViewResponse from "./view-response.webp";
@@ -19,8 +18,13 @@ Check out this video to learn more about source tracking in link surveys:
{/* Replace link below with our new link on Source Tracking */}
<ResponsiveVideo title="Formbricks Source Tracking"
src="https://www.youtube-nocookie.com/embed/CytWhuyEMVI?si=nxRrdMmlsQ5P6wwp&amp;controls=0" />
<iframe
width="700"
height="450"
src="https://www.youtube.com/embed/CytWhuyEMVI?si=t-SFB2A1l1RZDdAC"
title="YouTube video player: Formbricks"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"></iframe>
## Purpose

View File

@@ -2,6 +2,7 @@ import { MdxImage } from "@/components/MdxImage";
import { Libraries } from "./components/Libraries";
import SetupChecklist from "./images/env-id.png";
import ReactApp from "./images/react-in-app-survey-app-popup-form.webp";
import WidgetConnected from "./images/widget-connected.webp";
import WidgetNotConnected from "./images/widget-not-connected.webp";
@@ -30,7 +31,14 @@ Detailed Website Survey SDK documentation can be found [here](/developer-docs/we
Before getting started, make sure you have:
1. A **public-facing** web application in your desired framework is set up and running.
2. A Formbricks account with access to your environment ID and API host. You can find these in the **Setup Checklist** in the Settings.
2. A Formbricks account with access to your environment ID and API host. You can find these in the **Setup Checklist** in the Settings:
<MdxImage
src={SetupChecklist}
alt="Step 2 - Setup Checklist"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
---

View File

@@ -16,7 +16,7 @@ function ArrowIcon(props: React.ComponentPropsWithoutRef<"svg">) {
const variantStyles = {
primary:
"rounded-full bg-slate-900 py-1 px-3 text-white hover:text-white hover:bg-slate-700 dark:bg-teal-400/10 dark:text-teal-400 dark:ring-1 dark:ring-inset dark:ring-teal-400/20 dark:hover:bg-teal-400/10 dark:hover:text-teal-300 dark:hover:ring-teal-300",
"rounded-full bg-slate-900 py-1 px-3 text-white hover:bg-slate-700 dark:bg-teal-400/10 dark:text-teal-400 dark:ring-1 dark:ring-inset dark:ring-teal-400/20 dark:hover:bg-teal-400/10 dark:hover:text-teal-300 dark:hover:ring-teal-300",
secondary:
"rounded-full bg-slate-100 py-1 px-3 text-slate-900 hover:bg-slate-200 dark:bg-slate-800/40 dark:text-slate-400 dark:ring-1 dark:ring-inset dark:ring-slate-800 dark:hover:bg-slate-800 dark:hover:text-slate-300",
filled:
@@ -39,7 +39,7 @@ export function Button({ variant = "primary", className, children, arrow, ...pro
"inline-flex gap-0.5 justify-center items-center overflow-hidden font-medium transition text-center",
variantStyles[variant],
className,
"px-5 py-2.5 text-sm"
"px-5 py-2.5 text-xs"
);
let arrowIcon = (

View File

@@ -3,11 +3,9 @@
import { navigation } from "@/lib/navigation";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { FaDiscord, FaGithub, FaXTwitter } from "react-icons/fa6";
import { Button } from "./Button";
import { DiscordIcon } from "./icons/DiscordIcon";
import { GithubIcon } from "./icons/GithubIcon";
import { TwitterIcon } from "./icons/TwitterIcon";
function PageLink({
label,
@@ -100,13 +98,13 @@ function SmallPrint() {
Formbricks GmbH &copy; {currentYear}. All rights reserved.
</p>
<div className="flex gap-4">
<SocialLink href="https://twitter.com/formbricks" icon={TwitterIcon}>
<SocialLink href="https://twitter.com/formbricks" icon={FaXTwitter}>
Follow us on Twitter
</SocialLink>
<SocialLink href="https://github.com/formbricks/formbricks" icon={GithubIcon}>
<SocialLink href="https://github.com/formbricks/formbricks" icon={FaGithub}>
Follow us on GitHub
</SocialLink>
<SocialLink href="https://formbricks.com/discord" icon={DiscordIcon}>
<SocialLink href="https://formbricks.com/discord" icon={FaDiscord}>
Join our Discord server
</SocialLink>
</div>

View File

@@ -40,6 +40,18 @@ function useInitialValue<T>(value: T, condition = true) {
return condition ? initialValue : value;
}
function TopLevelNavItem({ href, children }: { href: string; children: React.ReactNode }) {
return (
<li className="md:hidden">
<Link
href={href}
className="block py-1 text-sm text-slate-600 transition hover:text-slate-900 dark:text-slate-400 dark:hover:text-white">
{children}
</Link>
</li>
);
}
function NavLink({
href,
children,
@@ -60,7 +72,7 @@ function NavLink({
isAnchorLink ? "pl-7" : "pl-4",
active
? "rounded-r-md bg-slate-100 text-slate-900 dark:bg-slate-800 dark:text-white"
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
: "text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-white"
)}>
<span className="flex w-full truncate">{children}</span>
</Link>
@@ -123,71 +135,84 @@ function ActivePageMarker({ group, pathname }: { group: NavGroup; pathname: stri
/>
);
}
function NavigationGroup({
group,
className,
activeGroup,
setActiveGroup,
openGroups,
setOpenGroups,
}: {
group: NavGroup;
className?: string;
activeGroup: NavGroup | null;
setActiveGroup: (group: NavGroup | null) => void;
openGroups: string[];
setOpenGroups: (groups: string[]) => void;
activeGroup: NavGroup;
setActiveGroup: (group: NavGroup) => void;
}) {
const isInsideMobileNavigation = useIsInsideMobileNavigation();
const pathname = usePathname();
const isActiveGroup = activeGroup?.title === group.title;
// If this is the mobile navigation then we always render the initial
// state, so that the state does not change during the close animation.
// The state will still update when we re-open (re-render) the navigation.
let isInsideMobileNavigation = useIsInsideMobileNavigation();
let [pathname] = useInitialValue([usePathname()], isInsideMobileNavigation);
const [activeParentTitle, setActiveParentTitle] = useState<string | undefined>(
group.links.find((link) =>
link.children
? link.children.some((child) => pathname.startsWith(child.href))
: pathname.startsWith(link.href || "")
)?.title
);
const toggleParentTitle = (title: string) => {
if (openGroups.includes(title)) {
setOpenGroups(openGroups.filter((t) => t !== title));
} else {
setOpenGroups([...openGroups, title]);
}
setActiveGroup(group);
};
const isParentOpen = (title: string) => openGroups.includes(title);
const isActiveGroup = group.links.some((link) => {
pathname.startsWith(link.href || "") ||
(activeGroup &&
link.title === activeGroup.title &&
link.children &&
link.children.some((child) => pathname.startsWith(child.href)));
});
return (
<li className={clsx("relative mt-6", className)}>
<motion.h2 layout="position" className="font-semibold text-slate-900 dark:text-white">
<motion.h2 layout="position" className="text-xs font-semibold text-slate-900 dark:text-white">
{group.title}
</motion.h2>
<div className="relative mt-3 pl-2">
<AnimatePresence initial={!isInsideMobileNavigation}>
{isActiveGroup && <VisibleSectionHighlight group={group} pathname={pathname} />}
{activeGroup?.title === group.title && (
<VisibleSectionHighlight group={group} pathname={pathname} />
)}
</AnimatePresence>
<motion.div layout className="absolute inset-y-0 left-2 w-px bg-slate-900/10 dark:bg-white/5" />
<AnimatePresence initial={false}>
{isActiveGroup && <ActivePageMarker group={group} pathname={pathname || "/docs"} />}
{activeGroup?.title === group.title && (
<ActivePageMarker group={group} pathname={pathname || "/docs"} />
)}
</AnimatePresence>
<ul role="list" className="border-l border-transparent">
<ul
role="list"
className="border-l border-transparent"
onClick={() => {
setActiveGroup(group);
}}>
{group.links.map((link) => (
<motion.li key={link.title} layout="position" className="relative">
{link.href ? (
<NavLink href={link.href} active={!!pathname?.startsWith(link.href)}>
<NavLink href={link.href} active={pathname.startsWith(link.href)}>
{link.title}
</NavLink>
) : (
<div onClick={() => toggleParentTitle(link.title)}>
<div
onClick={() => {
setActiveParentTitle(link.title as string);
}}>
<NavLink
href={link.children?.[0]?.href || ""}
active={
!!(
isParentOpen(link.title) &&
(isActiveGroup &&
activeGroup?.title === group.title &&
link.children &&
link.children.some((child) => pathname.startsWith(child.href))
)
link.children.some((child) => pathname.startsWith(child.href))) ||
false
}>
<span className="flex w-full justify-between">
{link.title}
{isParentOpen(link.title) ? (
{link.title === activeParentTitle && activeGroup?.title === group.title ? (
<ChevronUpIcon className="my-1 h-4" />
) : (
<ChevronDownIcon className="my-1 h-4" />
@@ -197,15 +222,21 @@ function NavigationGroup({
</div>
)}
<AnimatePresence mode="popLayout" initial={false}>
{link.children && isParentOpen(link.title) && (
{link.children && link.title === activeParentTitle && activeGroup?.title === group.title && (
<motion.ul
role="list"
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.1 } }}
exit={{ opacity: 0, transition: { duration: 0.15 } }}>
animate={{
opacity: 1,
transition: { delay: 0.1 },
}}
exit={{
opacity: 0,
transition: { duration: 0.15 },
}}>
{link.children.map((child) => (
<li key={child.href}>
<NavLink href={child.href} isAnchorLink active={!!pathname?.startsWith(child.href)}>
<NavLink href={child.href} isAnchorLink active={pathname.startsWith(child.href)}>
{child.title}
</NavLink>
</li>
@@ -222,12 +253,14 @@ function NavigationGroup({
}
export function Navigation(props: React.ComponentPropsWithoutRef<"nav">) {
const [activeGroup, setActiveGroup] = useState<NavGroup | null>(navigation[0]);
const [openGroups, setOpenGroups] = useState<string[]>([]);
const [activeGroup, setActiveGroup] = useState<NavGroup>(navigation[0]);
return (
<nav {...props}>
<ul role="list">
<TopLevelNavItem href="/docs/introduction/what-is-formbricks">Documentation</TopLevelNavItem>
<TopLevelNavItem href="https://github.com/formbricks/formbricks">Star us on GitHub</TopLevelNavItem>
<TopLevelNavItem href="https://formbricks.com/discord">Join our Discord</TopLevelNavItem>
{navigation.map((group, groupIndex) => (
<NavigationGroup
key={group.title}
@@ -235,8 +268,6 @@ export function Navigation(props: React.ComponentPropsWithoutRef<"nav">) {
className={groupIndex === 0 ? "md:mt-0" : ""}
activeGroup={activeGroup}
setActiveGroup={setActiveGroup}
openGroups={openGroups}
setOpenGroups={setOpenGroups}
/>
))}
<li className="sticky bottom-0 z-10 mt-6 min-[416px]:hidden">

View File

@@ -1,15 +0,0 @@
// ResponsiveVideo.js
export function ResponsiveVideo({ src, title }) {
return (
<div className="relative w-full overflow-hidden pt-[56.25%]">
<iframe
src={src}
title={title}
frameBorder="0"
className="absolute left-0 top-0 h-full w-full"
referrerPolicy="strict-origin-when-cross-origin"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen></iframe>
</div>
);
}

View File

@@ -53,13 +53,13 @@ export function Search() {
const style = document.createElement("style");
style.innerHTML = `
:root {
--docsearch-primary-color: ${isLightMode ? "#00C4B8" : "#00C4B8"};
--docsearch-modal-background: ${isLightMode ? "#f8fafc" : "#0f172a"};
--docsearch-primary-color: ${isLightMode ? "#029E94" : "#1F7066"};
--docsearch-modal-background: ${isLightMode ? "#FFFFFF" : "#121212"};
--docsearch-text-color: ${isLightMode ? "#121212" : "#FFFFFF"};
--docsearch-hit-background: ${isLightMode ? "#FFFFFF" : "#111111"};
--docsearch-footer-background: ${isLightMode ? "#EEEEEE" : "#121212"};
--docsearch-searchbox-focus-background: ${isLightMode ? "#f1f5f9" : "#1e293b"};
--docsearch-modal-shadow: "";
--docsearch-searchbox-focus-background: ${isLightMode ? "#D8F6F4" : "#121212"};
--docsearch-modal-shadow: ${isLightMode ? "inset 1px 1px 0 0 hsla(0,0%,100%,0.5), 0 3px 8px 0 #D8F6F4" : "inset 1px 1px 0 0 hsla(0,0%,100%,0.5), 0 3px 8px 0 #808080"};
--DocSearch-Input: ${isLightMode ? "#000000" : "#FFFFFF"};
}
.DocSearch-Hit-title {
@@ -86,9 +86,6 @@ export function Search() {
#docsearch-input {
background-color: transparent;
}
.DocSearch-Footer {
display: none !important;
}
`;
document.head.appendChild(style);

View File

@@ -1,15 +0,0 @@
export const DiscordIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 640 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"></path>
</svg>
);
};

View File

@@ -1,15 +0,0 @@
export const GithubIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 496 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path>
</svg>
);
};

View File

@@ -1,15 +0,0 @@
export const TwitterIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => {
return (
<svg
stroke="currentColor"
fill="currentColor"
stroke-width="0"
viewBox="0 0 512 512"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
{...props}>
<path d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"></path>
</svg>
);
};

View File

@@ -11,7 +11,7 @@ export { CodeGroup, Code as code, Pre as pre } from "@/components/Code";
export function wrapper({ children }: { children: React.ReactNode }) {
return (
<article className="flex h-full flex-col pb-10 pt-16">
<Prose className="flex-auto font-normal">{children}</Prose>
<Prose className="flex-auto">{children}</Prose>
<footer className="mx-auto mt-16 w-full max-w-2xl lg:max-w-5xl">
<Feedback />
</footer>

View File

@@ -5,7 +5,7 @@ export const navigation: Array<NavGroup> = [
title: "Introduction",
links: [
{ title: "What is Formbricks?", href: "/introduction/what-is-formbricks" },
{ title: "Why open source?", href: "/introduction/why-open-source" },
{ title: "Why is it better?", href: "/introduction/why-is-it-better" },
{ title: "How does it work?", href: "/introduction/how-it-works" },
{
title: "Best Practices",

View File

@@ -20,6 +20,11 @@ const nextConfig = {
transpilePackages: ["@formbricks/ui", "@formbricks/lib"],
images: {
remotePatterns: [
{
protocol: "https",
hostname: "seo-strapi-aws-s3.s3.eu-central-1.amazonaws.com",
port: "",
},
{
protocol: "https",
hostname: "avatars.githubusercontent.com",
@@ -34,11 +39,6 @@ const nextConfig = {
destination: "/introduction/what-is-formbricks",
permanent: true,
},
{
source: "/introduction/why-is-it-better",
destination: "/introduction/why-open-source",
permanent: true,
},
// Redirects for Docs 2.0
// Self Hosting
{
@@ -77,7 +77,6 @@ const nextConfig = {
destination: "/developer-docs/overview",
permanent: true,
},
// Link Survey
{
source: "/link-surveys/embed-in-email",

View File

@@ -1,5 +1,5 @@
{
"name": "@formbricks/docs",
"name": "@formbricks/formbricks-com",
"version": "1.0.0",
"private": true,
"scripts": {
@@ -19,27 +19,27 @@
"@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*",
"@headlessui/react": "^2.0.3",
"@headlessui/react": "^1.7.19",
"@headlessui/tailwindcss": "^0.2.0",
"@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.1",
"@paralleldrive/cuid2": "^2.2.2",
"@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/typography": "^0.5.13",
"@tailwindcss/typography": "^0.5.12",
"acorn": "^8.11.3",
"autoprefixer": "^10.4.19",
"clsx": "^2.1.1",
"clsx": "^2.1.0",
"fast-glob": "^3.3.2",
"flexsearch": "^0.7.43",
"framer-motion": "11.1.9",
"framer-motion": "11.1.1",
"lottie-web": "^5.12.2",
"lucide": "^0.378.0",
"lucide-react": "^0.378.0",
"lucide": "^0.368.0",
"lucide-react": "^0.368.0",
"mdast-util-to-string": "^4.0.0",
"mdx-annotations": "^0.1.4",
"next": "14.2.3",
"next": "14.1.3",
"next-plausible": "^3.12.0",
"next-seo": "^6.5.0",
"next-sitemap": "^4.2.3",
@@ -47,9 +47,10 @@
"node-fetch": "^3.3.2",
"prism-react-renderer": "^2.3.1",
"prismjs": "^1.29.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-highlight-words": "^0.20.0",
"react-icons": "^5.1.0",
"react-markdown": "^9.0.1",
"react-responsive-embed": "^2.1.0",
"remark": "^15.0.1",

View File

@@ -1,9 +0,0 @@
# *
User-agent: *
Allow: /
# Host
Host: https://formbricks.com
# Sitemaps
Sitemap: https://formbricks.com/sitemap.xml

View File

@@ -68,7 +68,7 @@ export default {
},
},
fontFamily: {
sans: ["Jost", ...defaultTheme.fontFamily.sans],
sans: ["Poppins", ...defaultTheme.fontFamily.sans],
display: ["Lexend", ...defaultTheme.fontFamily.sans],
kablammo: ["Kablammo", "sans"],
},

View File

@@ -1,7 +1,7 @@
{
"extends": "@formbricks/tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../packages/types/*.d.ts"],
"exclude": ["../../.env", "node_modules"],
"exclude": ["../../.env"],
"compilerOptions": {
"baseUrl": ".",
"paths": {

View File

@@ -42,7 +42,7 @@ export default function typographyStyles({ theme }: PluginUtils) {
// Base
color: "var(--tw-prose-body)",
fontSize: theme("fontSize.base")[0],
fontSize: theme("fontSize.sm")[0],
lineHeight: theme("lineHeight.7"),
// Layout
@@ -192,14 +192,14 @@ export default function typographyStyles({ theme }: PluginUtils) {
color: "var(--tw-prose-headings)",
fontWeight: "600",
fontSize: theme("fontSize.lg")[0],
...theme("fontSize.xl")[1],
...theme("fontSize.lg")[1],
marginTop: theme("spacing.16"),
marginBottom: theme("spacing.2"),
},
h3: {
color: "var(--tw-prose-headings)",
fontSize: theme("fontSize.base")[0],
...theme("fontSize.lg")[1],
...theme("fontSize.base")[1],
fontWeight: "600",
marginTop: theme("spacing.10"),
marginBottom: theme("spacing.2"),

View File

@@ -12,24 +12,24 @@
},
"dependencies": {
"@formbricks/ui": "workspace:*",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",
"@storybook/addon-essentials": "^8.0.10",
"@storybook/addon-interactions": "^8.0.10",
"@storybook/addon-links": "^8.0.10",
"@storybook/addon-onboarding": "^8.0.10",
"@storybook/blocks": "^8.0.10",
"@storybook/react": "^8.0.10",
"@storybook/react-vite": "^8.0.10",
"@storybook/addon-essentials": "^8.0.9",
"@storybook/addon-interactions": "^8.0.9",
"@storybook/addon-links": "^8.0.9",
"@storybook/addon-onboarding": "^8.0.9",
"@storybook/blocks": "^8.0.9",
"@storybook/react": "^8.0.9",
"@storybook/react-vite": "^8.0.9",
"@storybook/testing-library": "^0.2.2",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"@vitejs/plugin-react": "^4.2.1",
"esbuild": "^0.21.1",
"esbuild": "^0.20.2",
"tsup": "^8.0.2",
"vite": "^5.2.11"
"vite": "^5.2.10"
}
}

View File

@@ -1,57 +0,0 @@
import FormbricksClient from "@/app/(app)/components/FormbricksClient";
import PosthogIdentify from "@/app/(app)/environments/[environmentId]/components/PosthogIdentify";
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";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
import ToasterClient from "@formbricks/ui/ToasterClient";
export default async function EnvLayout({ children, params }) {
const session = await getServerSession(authOptions);
if (!session || !session.user) {
return redirect(`/auth/login`);
}
const hasAccess = await hasUserEnvironmentAccess(session.user.id, params.environmentId);
if (!hasAccess) {
throw new AuthorizationError("Not authorized");
}
const team = await getTeamByEnvironmentId(params.environmentId);
if (!team) {
throw new Error("Team not found");
}
const environment = await getEnvironment(params.environmentId);
if (!environment) {
throw new Error("Environment not found");
}
return (
<>
<ResponseFilterProvider>
<PosthogIdentify
session={session}
environmentId={params.environmentId}
teamId={team.id}
teamName={team.name}
inAppSurveyBillingStatus={team.billing.features.inAppSurvey.status}
linkSurveyBillingStatus={team.billing.features.linkSurvey.status}
userTargetingBillingStatus={team.billing.features.userTargeting.status}
/>
<FormbricksClient session={session} />
<ToasterClient />
<div className="flex h-screen flex-col">
<DevEnvironmentBanner environment={environment} />
<div className="h-full overflow-y-auto bg-slate-50">{children}</div>
</div>
</ResponseFilterProvider>
</>
);
}

View File

@@ -1,67 +0,0 @@
"use client";
import { TActionClass } from "@formbricks/types/actionClasses";
import { TSurvey } from "@formbricks/types/surveys";
import { ModalWithTabs } from "@formbricks/ui/ModalWithTabs";
import { CreateNewActionTab } from "./CreateNewActionTab";
import { SavedActionsTab } from "./SavedActionsTab";
interface AddActionModalProps {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
environmentId: string;
actionClasses: TActionClass[];
setActionClasses: React.Dispatch<React.SetStateAction<TActionClass[]>>;
isViewer: boolean;
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
}
export const AddActionModal = ({
open,
setOpen,
actionClasses,
setActionClasses,
localSurvey,
setLocalSurvey,
isViewer,
environmentId,
}: AddActionModalProps) => {
const tabs = [
{
title: "Select saved action",
children: (
<SavedActionsTab
actionClasses={actionClasses}
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
setOpen={setOpen}
/>
),
},
{
title: "Capture new action",
children: (
<CreateNewActionTab
actionClasses={actionClasses}
setActionClasses={setActionClasses}
setOpen={setOpen}
isViewer={isViewer}
setLocalSurvey={setLocalSurvey}
environmentId={environmentId}
/>
),
},
];
return (
<ModalWithTabs
label="Add action"
open={open}
setOpen={setOpen}
tabs={tabs}
size="md"
closeOnOutsideClick={false}
/>
);
};

View File

@@ -1,297 +0,0 @@
import { isValidCssSelector } from "@/app/lib/actionClass/actionClass";
import { Terminal } from "lucide-react";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { MatchType, testURLmatch } from "@formbricks/lib/utils/testUrlMatch";
import { TActionClass, TActionClassInput, TActionClassNoCodeConfig } from "@formbricks/types/actionClasses";
import { TSurvey } from "@formbricks/types/surveys";
import { CssSelector, InnerHtmlSelector, PageUrlSelector } from "@formbricks/ui/Actions";
import { Alert, AlertDescription, AlertTitle } from "@formbricks/ui/Alert";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
import { Label } from "@formbricks/ui/Label";
import { TabBar } from "@formbricks/ui/TabBar";
import { createActionClassAction } from "../actions";
interface CreateNewActionTabProps {
actionClasses: TActionClass[];
setActionClasses: React.Dispatch<React.SetStateAction<TActionClass[]>>;
isViewer: boolean;
setLocalSurvey?: React.Dispatch<React.SetStateAction<TSurvey>>;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
environmentId: string;
}
export const CreateNewActionTab = ({
actionClasses,
setActionClasses,
setOpen,
isViewer,
setLocalSurvey,
environmentId,
}: CreateNewActionTabProps) => {
const { register, control, handleSubmit, watch, reset } = useForm<TActionClass>({
defaultValues: {
name: "",
description: "",
type: "noCode",
key: "",
noCodeConfig: {
pageUrl: {
rule: "contains",
value: "",
},
cssSelector: {
value: "",
},
innerHtml: {
value: "",
},
},
},
});
const [type, setType] = useState("noCode");
const [isPageUrl, setIsPageUrl] = useState(false);
const [isCssSelector, setIsCssSelector] = useState(false);
const [isInnerHtml, setIsInnerText] = useState(false);
const [isCreatingAction, setIsCreatingAction] = useState(false);
const [testUrl, setTestUrl] = useState("");
const [isMatch, setIsMatch] = useState("");
const actionClassNames = useMemo(
() => actionClasses.map((actionClass) => actionClass.name),
[actionClasses]
);
const actionClassKeys = useMemo(
() =>
actionClasses
.filter((actionClass) => actionClass.type === "code")
.map((actionClass) => actionClass.key),
[actionClasses]
);
const filterNoCodeConfig = (noCodeConfig: TActionClassNoCodeConfig): TActionClassNoCodeConfig => {
const { pageUrl, innerHtml, cssSelector } = noCodeConfig;
const filteredNoCodeConfig: TActionClassNoCodeConfig = {};
if (isPageUrl && pageUrl?.rule && pageUrl?.value) {
filteredNoCodeConfig.pageUrl = { rule: pageUrl.rule, value: pageUrl.value };
}
if (isInnerHtml && innerHtml?.value) {
filteredNoCodeConfig.innerHtml = { value: innerHtml.value };
}
if (isCssSelector && cssSelector?.value) {
filteredNoCodeConfig.cssSelector = { value: cssSelector.value };
}
return filteredNoCodeConfig;
};
const handleMatchClick = () => {
const match = testURLmatch(
testUrl,
watch("noCodeConfig.pageUrl.value"),
watch("noCodeConfig.pageUrl.rule") as MatchType
);
setIsMatch(match);
if (match === "yes") toast.success("Your survey would be shown on this URL.");
if (match === "no") toast.error("Your survey would not be shown.");
};
const submitHandler = async (data: Partial<TActionClass>) => {
const { noCodeConfig } = data;
try {
if (isViewer) {
throw new Error("You are not authorised to perform this action.");
}
setIsCreatingAction(true);
if (!data.name || data.name?.trim() === "") {
throw new Error("Please give your action a name");
}
if (data.name && actionClassNames.includes(data.name)) {
throw new Error(`Action with name ${data.name} already exist`);
}
if (type === "noCode") {
if (!isPageUrl && !isCssSelector && !isInnerHtml)
throw new Error("Please select at least one selector");
if (isCssSelector && !isValidCssSelector(noCodeConfig?.cssSelector?.value))
throw new Error("Please enter a valid CSS Selector");
if (isPageUrl && noCodeConfig?.pageUrl?.rule === undefined)
throw new Error("Please select a rule for page URL");
}
if (type === "code" && !data.key) {
throw new Error("Please enter a code key");
}
if (data.key && actionClassKeys.includes(data.key)) {
throw new Error(`Action with key ${data.key} already exist`);
}
const updatedAction: TActionClassInput = {
name: data.name.trim(),
description: data.description,
environmentId,
type: type as TActionClass["type"],
};
if (type === "noCode") {
const filteredNoCodeConfig = filterNoCodeConfig(noCodeConfig as TActionClassNoCodeConfig);
updatedAction.noCodeConfig = filteredNoCodeConfig;
} else {
updatedAction.key = data.key;
}
const newActionClass: TActionClass = await createActionClassAction(updatedAction);
if (setActionClasses) {
setActionClasses((prevActionClasses: TActionClass[]) => [...prevActionClasses, newActionClass]);
}
if (setLocalSurvey) {
setLocalSurvey((prev) => ({
...prev,
triggers: prev.triggers.concat({ actionClass: newActionClass }),
}));
}
reset();
resetAllStates();
} catch (e: any) {
toast.error(e.message);
} finally {
setIsCreatingAction(false);
}
};
const resetAllStates = () => {
setType("noCode");
setIsCssSelector(false);
setIsPageUrl(false);
setIsInnerText(false);
setTestUrl("");
setIsMatch("");
reset();
setOpen(false);
};
return (
<div>
<form onSubmit={handleSubmit(submitHandler)}>
<div className="w-full space-y-4">
<div className="grid w-full grid-cols-2 gap-x-4">
<div className="col-span-1">
<Label htmlFor="actionNameInput">What did your user do?</Label>
<Input id="actionNameInput" placeholder="E.g. Clicked Download" {...register("name")} />
</div>
<div className="col-span-1">
<Label htmlFor="actionDescriptionInput">Description</Label>
<Input
id="actionDescriptionInput"
placeholder="User clicked Download Button "
{...register("description")}
/>
</div>
</div>
<div onClick={(e) => e.stopPropagation()}>
<Label>Type</Label>
<div className="w-3/5">
<TabBar
tabs={[
{
id: "noCode",
label: "No code",
},
{
id: "code",
label: "Code",
},
]}
activeId={type}
setActiveId={setType}
tabStyle="button"
className="rounded-md bg-white"
activeTabClassName="bg-slate-100"
/>
</div>
</div>
<div className="max-h-60 overflow-y-auto">
{type === "code" ? (
<>
<div className="col-span-1">
<Label htmlFor="codeActionKeyInput">Key</Label>
<Input
id="codeActionKeyInput"
placeholder="e.g. download_cta_click_on_home"
{...register("key")}
className="mb-2 w-1/2"
/>
</div>
<Alert className="bg-slate-100">
<Terminal className="h-4 w-4" />
<AlertTitle>How do Code Actions work?</AlertTitle>
<AlertDescription>
You can track code action anywhere in your app using{" "}
<span className="rounded bg-white px-2 py-1 text-xs">
formbricks.track(&quot;{watch("key")}&quot;)
</span>{" "}
in your code. Read more in our{" "}
<a href="https://formbricks.com/docs/actions/code" target="_blank" className="underline">
docs
</a>
.
</AlertDescription>
</Alert>
</>
) : (
<>
<div>
<Label>Select By</Label>
</div>
<CssSelector
isCssSelector={isCssSelector}
setIsCssSelector={setIsCssSelector}
register={register}
/>
<PageUrlSelector
isPageUrl={isPageUrl}
setIsPageUrl={setIsPageUrl}
register={register}
control={control}
testUrl={testUrl}
setTestUrl={setTestUrl}
isMatch={isMatch}
setIsMatch={setIsMatch}
handleMatchClick={handleMatchClick}
/>
<InnerHtmlSelector
isInnerHtml={isInnerHtml}
setIsInnerHtml={setIsInnerText}
register={register}
/>
</>
)}
</div>
</div>
<div className="flex justify-end pt-6">
<div className="flex space-x-2">
<Button type="button" variant="minimal" onClick={resetAllStates}>
Cancel
</Button>
<Button variant="darkCTA" type="submit" loading={isCreatingAction}>
Create action
</Button>
</div>
</div>
</form>
</div>
);
};

View File

@@ -1,90 +0,0 @@
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
import { useState } from "react";
import { TActionClass } from "@formbricks/types/actionClasses";
import { TSurvey } from "@formbricks/types/surveys";
import { Input } from "@formbricks/ui/Input";
interface SavedActionsTabProps {
actionClasses: TActionClass[];
localSurvey: TSurvey;
setLocalSurvey: React.Dispatch<React.SetStateAction<TSurvey>>;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
export const SavedActionsTab = ({
actionClasses,
localSurvey,
setLocalSurvey,
setOpen,
}: SavedActionsTabProps) => {
const availableActions = actionClasses.filter(
(actionClass) => !localSurvey.triggers.some((trigger) => trigger.actionClass.id === actionClass.id)
);
const [filteredActionClasses, setFilteredActionClasses] = useState<TActionClass[]>(availableActions);
const codeActions = filteredActionClasses.filter((actionClass) => actionClass.type === "code");
const noCodeActions = filteredActionClasses.filter((actionClass) => actionClass.type === "noCode");
const automaticActions = filteredActionClasses.filter((actionClass) => actionClass.type === "automatic");
const handleActionClick = (action: TActionClass) => {
setLocalSurvey((prev) => ({
...prev,
triggers: prev.triggers.concat({ actionClass: action }),
}));
setOpen(false);
};
return (
<div>
<Input
type="text"
onChange={(e) => {
setFilteredActionClasses(
availableActions.filter((actionClass) =>
actionClass.name.toLowerCase().includes(e.target.value.toLowerCase())
)
);
}}
className="mb-2 bg-white"
placeholder="Search actions"
id="search-actions"
/>
<div className="max-h-96 overflow-y-auto">
{[automaticActions, noCodeActions, codeActions].map(
(actions, i) =>
actions.length > 0 && (
<div key={i} className="me-4">
<h2 className="mb-2 mt-4 font-semibold">
{i === 0 ? "Automatic" : i === 1 ? "No code" : "Code"}
</h2>
<div className="flex flex-col gap-2">
{actions.map((action) => (
<div
key={action.id}
className="cursor-pointer rounded-md border border-slate-300 bg-white px-4 py-2 hover:bg-slate-100"
onClick={() => handleActionClick(action)}>
<div className="mt-1 flex items-center">
<div className="mr-1.5 h-4 w-4 text-slate-600">
{action.type === "code" ? (
<Code2Icon className="h-4 w-4" />
) : action.type === "noCode" ? (
<MousePointerClickIcon className="h-4 w-4" />
) : action.type === "automatic" ? (
<SparklesIcon className="h-4 w-4" />
) : null}
</div>
<h4 className="text-sm font-semibold text-slate-600">{action.name}</h4>
</div>
<p className="mt-1 text-xs text-gray-500">{action.description}</p>
</div>
))}
</div>
</div>
)
)}
</div>
</div>
);
};

View File

@@ -1,5 +0,0 @@
import { LoadingSkeleton } from "./components/LoadingSkeleton";
export default function Loading() {
return <LoadingSkeleton />;
}

View File

@@ -1,45 +0,0 @@
import { TSurvey } from "@formbricks/types/surveys";
export const minimalSurvey: TSurvey = {
id: "someUniqueId1",
createdAt: new Date(),
updatedAt: new Date(),
name: "Minimal Survey",
type: "app",
environmentId: "someEnvId1",
createdBy: null,
status: "draft",
displayOption: "displayOnce",
autoClose: null,
triggers: [],
redirectUrl: null,
recontactDays: null,
welcomeCard: {
enabled: false,
headline: { default: "Welcome!" },
html: { default: "Thanks for providing your feedback - let's go!" },
timeToFinish: false,
showResponseCount: false,
},
questions: [],
thankYouCard: {
enabled: false,
},
hiddenFields: {
enabled: false,
},
delay: 0, // No delay
displayPercentage: null,
autoComplete: null,
runOnDate: null,
closeOnDate: null,
surveyClosedMessage: {
enabled: false,
},
productOverwrites: null,
singleUse: null,
styling: null,
resultShareKey: null,
segment: null,
languages: [],
};

View File

@@ -1,20 +0,0 @@
"use client";
import { ArrowLeftIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { Button } from "@formbricks/ui/Button";
export const BackButton = () => {
const router = useRouter();
return (
<Button
variant="secondary"
StartIcon={ArrowLeftIcon}
onClick={() => {
router.back();
}}>
Back
</Button>
);
};

View File

@@ -1,15 +0,0 @@
"use client";
import { BackButton } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/templates/components/BackButton";
export const MenuBar = () => {
return (
<>
<div className="border-b border-slate-200 bg-white px-5 py-3 sm:flex sm:items-center sm:justify-between">
<div className="flex items-center space-x-2 whitespace-nowrap">
<BackButton />
</div>
</div>
</>
);
};

View File

@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
import { Button } from "@formbricks/ui/Button";
import { Confetti } from "@formbricks/ui/Confetti";
import { ContentWrapper } from "@formbricks/ui/ContentWrapper";
interface ConfirmationPageProps {
environmentId: string;
@@ -14,24 +15,25 @@ export default function ConfirmationPage({ environmentId }: ConfirmationPageProp
useEffect(() => {
setShowConfetti(true);
}, []);
return (
<div className="h-full w-full">
{showConfetti && <Confetti />}
<div className="mx-auto max-w-sm py-8 sm:px-6 lg:px-8">
<div className="my-6 sm:flex-auto">
<h1 className="text-center text-xl font-semibold text-slate-900">Upgrade successful</h1>
<p className="mt-2 text-center text-sm text-slate-700">
Thanks a lot for upgrading your Formbricks subscription.
</p>
<ContentWrapper>
<div className="mx-auto max-w-sm py-8 sm:px-6 lg:px-8">
<div className="my-6 sm:flex-auto">
<h1 className="text-center text-xl font-semibold text-slate-900">Upgrade successful</h1>
<p className="mt-2 text-center text-sm text-slate-700">
Thanks a lot for upgrading your Formbricks subscription.
</p>
</div>
<Button
variant="darkCTA"
className="w-full justify-center"
href={`/environments/${environmentId}/settings/billing`}>
Back to billing overview
</Button>
</div>
<Button
variant="darkCTA"
className="w-full justify-center"
href={`/environments/${environmentId}/settings/billing`}>
Back to billing overview
</Button>
</div>
</ContentWrapper>
</div>
);
}

View File

@@ -1,15 +1,9 @@
import ConfirmationPage from "@/app/(app)/billing-confirmation/components/ConfirmationPage";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import ConfirmationPage from "./components/ConfirmationPage";
export const dynamic = "force-dynamic";
export default function BillingConfirmation({ searchParams }) {
const { environmentId } = searchParams;
return (
<PageContentWrapper>
<ConfirmationPage environmentId={environmentId?.toString()} />
</PageContentWrapper>
);
return <ConfirmationPage environmentId={environmentId?.toString()} />;
}

View File

@@ -1,38 +0,0 @@
import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation";
import { CircleHelpIcon } from "lucide-react";
import { Metadata } from "next";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { Button } from "@formbricks/ui/Button";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
import { AttributeClassesTable } from "./components/AttributeClassesTable";
export const metadata: Metadata = {
title: "Attributes",
};
export default async function AttributesPage({ params }) {
let attributeClasses = await getAttributeClasses(params.environmentId);
const HowToAddAttributesButton = (
<Button
size="sm"
href="https://formbricks.com/docs/app-surveys/user-identification#setting-custom-user-attributes"
variant="secondary"
target="_blank"
EndIcon={CircleHelpIcon}>
How to add attributes
</Button>
);
return (
<PageContentWrapper>
<PageHeader pageTitle="People" cta={HowToAddAttributesButton}>
<PeopleSecondaryNavigation activeId="attributes" environmentId={params.environmentId} />
</PageHeader>
<AttributeClassesTable attributeClasses={attributeClasses} />
</PageContentWrapper>
);
}

View File

@@ -1,76 +0,0 @@
import ActivitySection from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ActivitySection";
import AttributesSection from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/AttributesSection";
import { DeletePersonButton } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/DeletePersonButton";
import ResponseSection from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseSection";
import { getServerSession } from "next-auth";
import { getAttributes } from "@formbricks/lib/attribute/service";
import { authOptions } from "@formbricks/lib/authOptions";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { getPerson } from "@formbricks/lib/person/service";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
export default async function PersonPage({ params }) {
const [environment, environmentTags, product, session, team, person, attributes] = await Promise.all([
getEnvironment(params.environmentId),
getTagsByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
getServerSession(authOptions),
getTeamByEnvironmentId(params.environmentId),
getPerson(params.personId),
getAttributes(params.personId),
]);
if (!product) {
throw new Error("Product not found");
}
if (!environment) {
throw new Error("Environment not found");
}
if (!session) {
throw new Error("Session not found");
}
if (!team) {
throw new Error("Team not found");
}
if (!person) {
throw new Error("Person not found");
}
const currentUserMembership = await getMembershipByUserIdTeamId(session?.user.id, team.id);
const { isViewer } = getAccessFlags(currentUserMembership?.role);
const getDeletePersonButton = () => {
return (
<DeletePersonButton environmentId={environment.id} personId={params.personId} isViewer={isViewer} />
);
};
return (
<PageContentWrapper>
<PageHeader pageTitle={getPersonIdentifier(person, attributes)} cta={getDeletePersonButton()} />
<section className="pb-24 pt-6">
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
<AttributesSection personId={params.personId} />
<ResponseSection
environment={environment}
personId={params.personId}
environmentTags={environmentTags}
/>
<ActivitySection environmentId={params.environmentId} personId={params.personId} />
</div>
</section>
</PageContentWrapper>
);
}

View File

@@ -1,28 +0,0 @@
import { SecondaryNavigation } from "@formbricks/ui/SecondaryNavigation";
interface PeopleSegmentsTabsProps {
activeId: string;
environmentId: string;
}
export const PeopleSecondaryNavigation = ({ activeId, environmentId }: PeopleSegmentsTabsProps) => {
const navigation = [
{
id: "people",
label: "People",
href: `/environments/${environmentId}/people`,
},
{
id: "segments",
label: "Segments",
href: `/environments/${environmentId}/segments`,
},
{
id: "attributes",
label: "Attributes",
href: `/environments/${environmentId}/attributes`,
},
];
return <SecondaryNavigation navigation={navigation} activeId={activeId} />;
};

View File

@@ -1,50 +0,0 @@
"use server";
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
import { deleteSegment, getSegment, updateSegment } from "@formbricks/lib/segment/service";
import { AuthorizationError } from "@formbricks/types/errors";
import { TSegmentUpdateInput, ZSegmentFilters } from "@formbricks/types/segment";
export const deleteBasicSegmentAction = async (environmentId: string, segmentId: string) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
if (!environmentAccess) throw new AuthorizationError("Not authorized");
const foundSegment = await getSegment(segmentId);
if (!foundSegment) {
throw new Error(`Segment with id ${segmentId} not found`);
}
return await deleteSegment(segmentId);
};
export const updateBasicSegmentAction = async (
environmentId: string,
segmentId: string,
data: TSegmentUpdateInput
) => {
const session = await getServerSession(authOptions);
if (!session) throw new AuthorizationError("Not authorized");
const environmentAccess = hasUserEnvironmentAccess(session.user.id, environmentId);
if (!environmentAccess) throw new AuthorizationError("Not authorized");
const { filters } = data;
if (filters) {
const parsedFilters = ZSegmentFilters.safeParse(filters);
if (!parsedFilters.success) {
const errMsg =
parsedFilters.error.issues.find((issue) => issue.code === "custom")?.message || "Invalid filters";
throw new Error(errMsg);
}
}
return await updateSegment(segmentId, data);
};

View File

@@ -1,6 +1,7 @@
"use client";
import { useMemo, useState } from "react";
import { useState } from "react";
import { useMemo } from "react";
import { TAttributeClass } from "@formbricks/types/attributeClasses";
import { Switch } from "@formbricks/ui/Switch";
@@ -8,6 +9,7 @@ import { Switch } from "@formbricks/ui/Switch";
import { AttributeDetailModal } from "./AttributeDetailModal";
import { AttributeClassDataRow } from "./AttributeRowData";
import { AttributeTableHeading } from "./AttributeTableHeading";
import { HowToAddAttributesButton } from "./HowToAddAttributesButton";
import { UploadAttributesModal } from "./UploadAttributesModal";
interface AttributeClassesTableProps {
@@ -43,15 +45,16 @@ export const AttributeClassesTable = ({ attributeClasses }: AttributeClassesTabl
return (
<>
{hasArchived && (
<div className="my-4 flex items-center justify-end text-right">
<div className="mb-6 flex items-center justify-end text-right">
{hasArchived && (
<div className="flex items-center text-sm font-medium">
Show archived
<Switch className="mx-3" checked={showArchived} onCheckedChange={toggleShowArchived} />
</div>
</div>
)}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
)}
<HowToAddAttributesButton />
</div>
<div className="rounded-lg border border-slate-200">
<AttributeTableHeading />
<div className="grid-cols-7">
{displayedAttributeClasses.map((attributeClass, index) => (

View File

@@ -5,10 +5,12 @@ import { Badge } from "@formbricks/ui/Badge";
export const AttributeClassDataRow = ({ attributeClass }) => {
return (
<div className="m-2 grid h-16 grid-cols-5 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
<div className="m-2 grid h-16 grid-cols-5 content-center rounded-lg hover:bg-slate-100">
<div className="col-span-5 flex items-center pl-6 text-sm sm:col-span-3">
<div className="flex items-center">
<TagIcon className="h-5 w-5 flex-shrink-0 text-slate-500" />
<div className="h-10 w-10 flex-shrink-0">
<TagIcon className="h-8 w-8 flex-shrink-0 text-slate-500" />
</div>
<div className="ml-4 text-left">
<div className="font-medium text-slate-900">
{attributeClass.name}

View File

@@ -1,7 +1,7 @@
export const AttributeTableHeading = () => {
return (
<>
<div className="grid h-12 grid-cols-5 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6 ">Name</div>
<div className="hidden text-center sm:block">Created</div>
<div className="hidden text-center sm:block">Last Updated</div>

View File

@@ -0,0 +1,61 @@
import SurveyNavBarName from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/attributes/components/SurveyNavBarName";
import Link from "next/link";
import { cn } from "@formbricks/lib/cn";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getSurvey } from "@formbricks/lib/survey/service";
interface SecondNavbarProps {
tabs: { id: string; label: string; href: string; icon?: React.ReactNode }[];
activeId: string;
surveyId?: string;
environmentId: string;
}
export default async function SecondNavbar({
tabs,
activeId,
surveyId,
environmentId,
...props
}: SecondNavbarProps) {
const product = await getProductByEnvironmentId(environmentId!);
if (!product) {
throw new Error("Product not found");
}
let survey;
if (surveyId) {
survey = await getSurvey(surveyId);
}
return (
<div {...props}>
<div className="grid h-14 w-full grid-cols-3 items-center justify-items-stretch border-b bg-white px-4">
<div className="justify-self-start">
{survey && environmentId && (
<SurveyNavBarName surveyName={survey.name} productName={product.name} />
)}
</div>{" "}
<nav className="flex h-full items-center space-x-4 justify-self-center" aria-label="Tabs">
{tabs.map((tab) => (
<Link
key={tab.id}
href={tab.href}
className={cn(
tab.id === activeId
? " border-brand-dark border-b-2 font-semibold text-slate-900"
: "text-slate-500 hover:text-slate-700",
"flex h-full items-center px-3 text-sm font-medium"
)}
aria-current={tab.id === activeId ? "page" : undefined}>
{tab.icon && <div className="mr-2 h-5 w-5">{tab.icon}</div>}
{tab.label}
</Link>
))}
</nav>
<div className="justify-self-end"></div>
</div>
</div>
);
}

View File

@@ -0,0 +1,21 @@
interface SurveyNavBarNameProps {
surveyName: string;
productName: string;
}
export default function SurveyNavBarName({ surveyName, productName }: SurveyNavBarNameProps) {
return (
<div className="hidden items-center space-x-2 whitespace-nowrap md:flex">
{/* <Button
variant="secondary"
StartIcon={ArrowLeftIcon}
onClick={() => {
router.back();
}}>
Back
</Button> */}
<p className="pl-4 font-semibold">{productName} / </p>
<span>{surveyName}</span>
</div>
);
}

View File

@@ -0,0 +1,12 @@
import PeopleSegmentsTabs from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/components/PeopleSegmentsTabs";
import { ContentWrapper } from "@formbricks/ui/ContentWrapper";
export default function ActionsAndAttributesLayout({ params, children }) {
return (
<>
<PeopleSegmentsTabs activeId="attributes" environmentId={params.environmentId} />
<ContentWrapper>{children}</ContentWrapper>
</>
);
}

View File

@@ -1,8 +1,21 @@
import { TagIcon } from "lucide-react";
import { HelpCircleIcon, TagIcon } from "lucide-react";
import { Button } from "@formbricks/ui/Button";
export default function Loading() {
return (
<>
<div className="mb-6 text-right">
<div className="mb-6 flex items-center justify-end text-right">
<Button
variant="secondary"
className="pointer-events-none animate-pulse cursor-not-allowed select-none">
<HelpCircleIcon className="mr-2 h-4 w-4" />
Loading Attributes
</Button>
</div>
</div>
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6 ">Name</div>

View File

@@ -0,0 +1,19 @@
import { Metadata } from "next";
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
import { AttributeClassesTable } from "./components/AttributeClassesTable";
export const metadata: Metadata = {
title: "Attributes",
};
export default async function AttributesPage({ params }) {
let attributeClasses = await getAttributeClasses(params.environmentId);
return (
<>
<AttributeClassesTable attributeClasses={attributeClasses} />
</>
);
}

View File

@@ -1,4 +1,4 @@
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ActivityTimeline";
import ActivityTimeline from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ActivityTimeline";
import { getActionsByPersonId } from "@formbricks/lib/action/service";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";

View File

@@ -1,20 +1,21 @@
"use client";
import { deletePersonAction } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/actions";
import { deletePersonAction } from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/actions";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import toast from "react-hot-toast";
import { TMembershipRole } from "@formbricks/types/memberships";
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
interface DeletePersonButtonProps {
environmentId: string;
personId: string;
isViewer: boolean;
membershipRole?: TMembershipRole;
}
export const DeletePersonButton = ({ environmentId, personId, isViewer }: DeletePersonButtonProps) => {
export function DeletePersonButton({ environmentId, personId }: DeletePersonButtonProps) {
const router = useRouter();
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
@@ -33,11 +34,6 @@ export const DeletePersonButton = ({ environmentId, personId, isViewer }: Delete
setIsDeletingPerson(false);
}
};
if (isViewer) {
return null;
}
return (
<>
<button
@@ -55,4 +51,4 @@ export const DeletePersonButton = ({ environmentId, personId, isViewer }: Delete
/>
</>
);
};
}

View File

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

View File

@@ -1,4 +1,4 @@
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponseTimeline";
import ResponseTimeline from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ResponseTimeline";
import { getServerSession } from "next-auth";
import { authOptions } from "@formbricks/lib/authOptions";

View File

@@ -1,6 +1,6 @@
"use client";
import ResponseFeed from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ResponsesFeed";
import ResponseFeed from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ResponsesFeed";
import { ArrowDownUpIcon } from "lucide-react";
import { useEffect, useState } from "react";

View File

@@ -1,7 +1,7 @@
import {
ActivityItemIcon,
ActivityItemPopover,
} from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/ActivityItemComponents";
} from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ActivityItemComponents";
import { ArrowDownUpIcon } from "lucide-react";
import { TrashIcon } from "lucide-react";
@@ -23,7 +23,6 @@ export default function Loading() {
name: "Loading User Acitivity",
description: null,
type: "automatic",
key: "",
noCodeConfig: null,
environmentId: "testEnvironment",
},
@@ -40,7 +39,6 @@ export default function Loading() {
updatedAt: new Date(),
name: "Loading User Acitivity",
description: null,
key: "",
type: "automatic",
noCodeConfig: null,
environmentId: "testEnvironment",

View File

@@ -0,0 +1,44 @@
import ActivitySection from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ActivitySection";
import AttributesSection from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/AttributesSection";
import HeadingSection from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/HeadingSection";
import ResponseSection from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/[personId]/components/ResponseSection";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
export default async function PersonPage({ params }) {
const [environment, environmentTags, product] = await Promise.all([
getEnvironment(params.environmentId),
getTagsByEnvironmentId(params.environmentId),
getProductByEnvironmentId(params.environmentId),
]);
if (!product) {
throw new Error("Product not found");
}
if (!environment) {
throw new Error("Environment not found");
}
return (
<div>
<main className="mx-auto px-4 sm:px-6 lg:px-8">
<>
<HeadingSection environmentId={params.environmentId} personId={params.personId} />
<section className="pb-24 pt-6">
<div className="grid grid-cols-1 gap-x-8 md:grid-cols-4">
<AttributesSection personId={params.personId} />
<ResponseSection
environment={environment}
personId={params.personId}
environmentTags={environmentTags}
/>
<ActivitySection environmentId={params.environmentId} personId={params.personId} />
</div>
</section>
</>
</main>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import SecondNavbar from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/attributes/components/SecondNavbar";
import { TagIcon, UserIcon, UsersIcon } from "lucide-react";
interface PeopleSegmentsTabsProps {
activeId: string;
environmentId: string;
isUserTargetingAllowed?: boolean;
}
export default function PeopleSegmentsTabs({ activeId, environmentId }: PeopleSegmentsTabsProps) {
let tabs = [
{
id: "people",
label: "People",
icon: <UserIcon className="h-5 w-5" />,
href: `/environments/${environmentId}/people`,
},
{
id: "segments",
label: "Segments",
icon: <UsersIcon className="h-5 w-5" />,
href: `/environments/${environmentId}/segments`,
},
{
id: "attributes",
label: "Attributes",
icon: <TagIcon className="h-5 w-5" />,
href: `/environments/${environmentId}/attributes`,
},
];
return <SecondNavbar tabs={tabs} activeId={activeId} environmentId={environmentId} />;
}

View File

@@ -2,7 +2,7 @@ import Link from "next/link";
import React from "react";
import { getAttributes } from "@formbricks/lib/attribute/service";
import { getPersonIdentifier } from "@formbricks/lib/person/utils";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { TPerson } from "@formbricks/types/people";
import { PersonAvatar } from "@formbricks/ui/Avatars";

View File

@@ -0,0 +1,17 @@
import PeopleSegmentsTabs from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/components/PeopleSegmentsTabs";
import { Metadata } from "next";
import { ContentWrapper } from "@formbricks/ui/ContentWrapper";
export const metadata: Metadata = {
title: "People",
};
export default async function PeopleLayout({ params, children }) {
return (
<>
<PeopleSegmentsTabs activeId="people" environmentId={params.environmentId} />
<ContentWrapper>{children}</ContentWrapper>
</>
);
}

View File

@@ -1,6 +1,14 @@
import HowToAddPeopleButton from "@/app/(app)/environments/[environmentId]/components/HowToAddPeopleButton";
export default function Loading() {
return (
<>
<div className="mb-6 text-right">
<div className="mb-6 flex items-center justify-end text-right">
<HowToAddPeopleButton />
</div>
</div>
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6">User</div>

View File

@@ -1,14 +1,10 @@
import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation";
import { CircleHelpIcon } from "lucide-react";
import HowToAddPeopleButton from "@/app/(app)/environments/[environmentId]/components/HowToAddPeopleButton";
import { ITEMS_PER_PAGE } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getPeople, getPeopleCount } from "@formbricks/lib/person/service";
import { TPerson } from "@formbricks/types/people";
import { Button } from "@formbricks/ui/Button";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
import { Pagination } from "@formbricks/ui/Pagination";
import { PersonCard } from "./components/PersonCard";
@@ -40,38 +36,28 @@ export default async function PeoplePage({
people = await getPeople(params.environmentId, pageNumber);
}
const HowToAddPeopleButton = (
<Button
size="sm"
href="https://formbricks.com/docs/app-surveys/user-identification"
variant="secondary"
target="_blank"
EndIcon={CircleHelpIcon}>
How to add people
</Button>
);
return (
<PageContentWrapper>
<PageHeader pageTitle="People" cta={HowToAddPeopleButton}>
<PeopleSecondaryNavigation activeId="people" environmentId={params.environmentId} />
</PageHeader>
<>
<div className="mb-6 text-right">
<div className="mb-6 flex items-center justify-end text-right">
<HowToAddPeopleButton />
</div>
</div>
{people.length === 0 ? (
<EmptySpaceFiller
type="table"
environment={environment}
emptyMessage="Your users will appear here as soon as they use your app ⏲️"
noWidgetRequired={true}
/>
) : (
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="grid h-12 grid-cols-7 content-center border-b border-slate-200 text-left text-sm font-semibold text-slate-900">
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6 ">User</div>
<div className="col-span-2 hidden text-center sm:block">User ID</div>
<div className="col-span-2 hidden text-center sm:block">Email</div>
</div>
{people.map((person) => (
<PersonCard person={person} key={person.id} />
<PersonCard person={person} />
))}
</div>
)}
@@ -83,6 +69,6 @@ export default async function PeoplePage({
itemsPerPage={ITEMS_PER_PAGE}
/>
)}
</PageContentWrapper>
</>
);
}

View File

@@ -1,6 +1,7 @@
"use client";
import { FilterIcon, PlusIcon, UsersIcon } from "lucide-react";
import { UsersIcon } from "lucide-react";
import { FilterIcon } from "lucide-react";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import toast from "react-hot-toast";
@@ -117,9 +118,11 @@ const BasicCreateSegmentModal = ({
return (
<>
<Button variant="darkCTA" size="sm" onClick={() => setOpen(true)} EndIcon={PlusIcon}>
Create segment
</Button>
<div className="mb-4 flex justify-end">
<Button variant="darkCTA" onClick={() => setOpen(true)}>
Create Segment
</Button>
</div>
<Modal
open={open}
@@ -246,7 +249,7 @@ const BasicCreateSegmentModal = ({
onClick={() => {
handleCreateSegment();
}}>
Create segment
Create Segment
</Button>
</div>
</div>

View File

@@ -1,5 +1,9 @@
"use client";
import {
deleteBasicSegmentAction,
updateBasicSegmentAction,
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
import { FilterIcon, Trash2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
@@ -16,8 +20,6 @@ import BasicSegmentEditor from "@formbricks/ui/Targeting/BasicSegmentEditor";
import ConfirmDeleteSegmentModal from "@formbricks/ui/Targeting/ConfirmDeleteSegmentModal";
import { UpgradePlanNotice } from "@formbricks/ui/UpgradePlanNotice";
import { deleteBasicSegmentAction, updateBasicSegmentAction } from "../actions";
type TBasicSegmentSettingsTabProps = {
environmentId: string;
setOpen: (open: boolean) => void;

View File

@@ -0,0 +1,31 @@
import PeopleSegmentsTabs from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/people/components/PeopleSegmentsTabs";
import { Metadata } from "next";
import { getAdvancedTargetingPermission } from "@formbricks/ee/lib/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import { ContentWrapper } from "@formbricks/ui/ContentWrapper";
export const metadata: Metadata = {
title: "Segments",
};
export default async function PeopleLayout({ params, children }) {
const team = await getTeamByEnvironmentId(params.environmentId);
if (!team) {
throw new Error("Team not found");
}
const isUserTargetingAllowed = await getAdvancedTargetingPermission(team);
return (
<>
<PeopleSegmentsTabs
activeId="segments"
environmentId={params.environmentId}
isUserTargetingAllowed={isUserTargetingAllowed}
/>
<ContentWrapper>{children}</ContentWrapper>
</>
);
}

View File

@@ -1,6 +1,5 @@
import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation";
import BasicCreateSegmentModal from "@/app/(app)/environments/[environmentId]/(people)/segments/components/BasicCreateSegmentModal";
import SegmentTable from "@/app/(app)/environments/[environmentId]/(people)/segments/components/SegmentTable";
import BasicCreateSegmentModal from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/segments/components/BasicCreateSegmentModal";
import SegmentTable from "@/app/(app)/environments/[environmentId]/(peopleAndSegments)/segments/components/SegmentTable";
import CreateSegmentModal from "@formbricks/ee/advancedTargeting/components/CreateSegmentModal";
import { ACTIONS_TO_EXCLUDE } from "@formbricks/ee/advancedTargeting/lib/constants";
@@ -12,8 +11,6 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
import { getSegments } from "@formbricks/lib/segment/service";
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper";
import { PageHeader } from "@formbricks/ui/PageHeader";
export default async function SegmentsPage({ params }) {
const [environment, segments, attributeClasses, actionClassesFromServer, team] = await Promise.all([
@@ -52,33 +49,28 @@ export default async function SegmentsPage({ params }) {
return true;
});
const renderCreateSegmentButton = () =>
isAdvancedTargetingAllowed ? (
<CreateSegmentModal
environmentId={params.environmentId}
actionClasses={actionClasses}
attributeClasses={attributeClasses}
segments={filteredSegments}
/>
) : (
<BasicCreateSegmentModal
attributeClasses={attributeClasses}
environmentId={params.environmentId}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
/>
);
return (
<PageContentWrapper>
<PageHeader pageTitle="People" cta={renderCreateSegmentButton()}>
<PeopleSecondaryNavigation activeId="segments" environmentId={params.environmentId} />
</PageHeader>
<>
{isAdvancedTargetingAllowed ? (
<CreateSegmentModal
environmentId={params.environmentId}
actionClasses={actionClasses}
attributeClasses={attributeClasses}
segments={filteredSegments}
/>
) : (
<BasicCreateSegmentModal
attributeClasses={attributeClasses}
environmentId={params.environmentId}
isFormbricksCloud={IS_FORMBRICKS_CLOUD}
/>
)}
{filteredSegments.length === 0 ? (
<EmptySpaceFiller
type="table"
environment={environment}
emptyMessage="No segments yet. Add your first one to get started."
noWidgetRequired={true}
/>
) : (
<SegmentTable
@@ -88,6 +80,6 @@ export default async function SegmentsPage({ params }) {
isAdvancedTargetingAllowed={isAdvancedTargetingAllowed}
/>
)}
</PageContentWrapper>
</>
);
}

View File

@@ -1,12 +1,16 @@
"use client";
import { MousePointerClickIcon } from "lucide-react";
import { useState } from "react";
import { useMembershipRole } from "@formbricks/lib/membership/hooks/useMembershipRole";
import { getAccessFlags } from "@formbricks/lib/membership/utils";
import { TActionClass } from "@formbricks/types/actionClasses";
import { Button } from "@formbricks/ui/Button";
import { ErrorComponent } from "@formbricks/ui/ErrorComponent";
import ActionDetailModal from "./ActionDetailModal";
import AddNoCodeActionModal from "./AddActionModal";
interface ActionClassesTableProps {
environmentId: string;
@@ -22,14 +26,15 @@ export default function ActionClassesTable({
isUserTargetingEnabled,
}: ActionClassesTableProps) {
const [isActionDetailModalOpen, setActionDetailModalOpen] = useState(false);
const { membershipRole, error } = useMembershipRole(environmentId);
const [isAddActionModalOpen, setAddActionModalOpen] = useState(false);
const { membershipRole, isLoading, error } = useMembershipRole(environmentId);
const { isViewer } = getAccessFlags(membershipRole);
const [activeActionClass, setActiveActionClass] = useState<TActionClass>({
environmentId,
id: "",
name: "",
type: "noCode",
key: "",
description: "",
noCodeConfig: null,
createdAt: new Date(),
@@ -41,15 +46,28 @@ export default function ActionClassesTable({
setActiveActionClass(actionClass);
setActionDetailModalOpen(true);
};
if (error) {
return <ErrorComponent />;
}
return (
<>
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
{!isViewer && (
<div className="mb-6 text-right">
<Button
loading={isLoading}
variant="darkCTA"
onClick={() => {
setAddActionModalOpen(true);
}}>
<MousePointerClickIcon className="mr-2 h-5 w-5 text-white" />
{isLoading ? "Loading" : "Add Action"}
</Button>
</div>
)}
<div className="rounded-lg border border-slate-200">
{TableHeading}
<div id="actionClassesWrapper" className="flex flex-col">
<div className="grid-cols-7" id="actionClassesWrapper">
{actionClasses.map((actionClass, index) => (
<button
onClick={(e) => {
@@ -67,11 +85,17 @@ export default function ActionClassesTable({
environmentId={environmentId}
open={isActionDetailModalOpen}
setOpen={setActionDetailModalOpen}
actionClasses={actionClasses}
actionClass={activeActionClass}
membershipRole={membershipRole}
isUserTargetingEnabled={isUserTargetingEnabled}
/>
<AddNoCodeActionModal
environmentId={environmentId}
open={isAddActionModalOpen}
actionClasses={actionClasses}
setOpen={setAddActionModalOpen}
isViewer={isViewer}
/>
</>
);
}

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