mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-24 07:00:34 -06:00
Compare commits
28 Commits
v2.0.0
...
action-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2254c24cad | ||
|
|
0f28cea7c8 | ||
|
|
868ee71e34 | ||
|
|
1dbbaca8fd | ||
|
|
8b895b4b43 | ||
|
|
f81eae3691 | ||
|
|
317013eee7 | ||
|
|
d19470f71a | ||
|
|
3bf5693af1 | ||
|
|
70cf58b55c | ||
|
|
68fc25587c | ||
|
|
4c54c0d934 | ||
|
|
93b49969f9 | ||
|
|
2839e49ccb | ||
|
|
e1dae1bd98 | ||
|
|
606305e54b | ||
|
|
0d1ded1139 | ||
|
|
6b1b2895f8 | ||
|
|
a3cb37b128 | ||
|
|
b27314cec6 | ||
|
|
1891f286e7 | ||
|
|
48409ced60 | ||
|
|
9a7e5bfa8d | ||
|
|
adcba0139a | ||
|
|
0d3f7103af | ||
|
|
a51d68d23f | ||
|
|
0d33c27295 | ||
|
|
4fa4528771 |
3
.github/actions/cache-build-web/action.yml
vendored
3
.github/actions/cache-build-web/action.yml
vendored
@@ -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
|
||||
|
||||
3
.github/workflows/e2e.yml
vendored
3
.github/workflows/e2e.yml
vendored
@@ -2,6 +2,9 @@ name: E2E Tests
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- action-test
|
||||
jobs:
|
||||
build:
|
||||
name: Run E2E Tests
|
||||
|
||||
@@ -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
|
||||
|
||||
4
.github/workflows/release-docker-github.yml
vendored
4
.github/workflows/release-docker-github.yml
vendored
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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 'Code Action'. 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">
|
||||
|
||||
@@ -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 'New Session'. 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 'Exit Intent'. 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 '50% Scroll'. You can also
|
||||
scroll down to trigger the 50% scroll.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
1
apps/docs/.gitignore
vendored
1
apps/docs/.gitignore
vendored
@@ -35,4 +35,3 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
|
||||
public/sitemap*.xml
|
||||
public/robots.txt
|
||||
@@ -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&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
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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. It’s 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. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
|
||||
(takes 15mins max.)](/app-surveys/quickstart)
|
||||
</Note>
|
||||
|
||||
|
||||
@@ -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. It’s 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. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
|
||||
(takes 15mins max.)](/app-surveys/quickstart)
|
||||
</Note>
|
||||
|
||||
|
||||
@@ -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. It’s 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. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
|
||||
(takes 15mins max.)](/app-surveys/quickstart)
|
||||
</Note>
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ To display an Interview Prompt in your app you want to proceed as follows:
|
||||
3. That’s it! 🎉
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running?
|
||||
We assume that you have already installed the Formbricks Widget in your web app. It’s 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. It’s 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>
|
||||
|
||||
###
|
||||
|
||||
@@ -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. It’s 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. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
|
||||
(15mins).](/app-surveys/quickstart)
|
||||
</Note>
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -59,6 +59,6 @@ That’s 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
|
||||
|
||||
---
|
||||
|
||||
@@ -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&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>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 |
@@ -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"
|
||||
|
||||
24
apps/docs/app/introduction/why-is-it-better/page.mdx
Normal file
24
apps/docs/app/introduction/why-is-it-better/page.mdx
Normal 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 🎉
|
||||
@@ -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. |
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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&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
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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 © {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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# *
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Host
|
||||
Host: https://formbricks.com
|
||||
|
||||
# Sitemaps
|
||||
Sitemap: https://formbricks.com/sitemap.xml
|
||||
@@ -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"],
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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("{watch("key")}")
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { LoadingSkeleton } from "./components/LoadingSkeleton";
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingSkeleton />;
|
||||
}
|
||||
@@ -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: [],
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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()} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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) => (
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user