Compare commits
43 Commits
v1.4.1
...
tweak/than
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d1cb8f595 | ||
|
|
81e9ac0e12 | ||
|
|
be4534da2d | ||
|
|
f4a31ad563 | ||
|
|
82302360fa | ||
|
|
f6f45d74d5 | ||
|
|
04a47b3d0a | ||
|
|
cc64d7dfe9 | ||
|
|
0c30dfbcf3 | ||
|
|
3e9f61792f | ||
|
|
ad63be3005 | ||
|
|
1635297226 | ||
|
|
0ff7bb56ec | ||
|
|
f3666b8745 | ||
|
|
ff864e3c82 | ||
|
|
cc56584db6 | ||
|
|
440c12699c | ||
|
|
8002d3e71f | ||
|
|
0534421538 | ||
|
|
fa33460a16 | ||
|
|
f12dec7b8b | ||
|
|
f2ad7c4fbf | ||
|
|
e6ce5373a2 | ||
|
|
90f0614aac | ||
|
|
a0d7921c01 | ||
|
|
a1fa3d6dbb | ||
|
|
1d7d07b3c6 | ||
|
|
c376b12461 | ||
|
|
c5d9f63267 | ||
|
|
9ec5d668df | ||
|
|
659ef3f92c | ||
|
|
3e2452b10f | ||
|
|
1f79416367 | ||
|
|
d3e0e67bd9 | ||
|
|
abe98be561 | ||
|
|
f23b4f63fa | ||
|
|
5679c38029 | ||
|
|
a815270784 | ||
|
|
2d97e9c797 | ||
|
|
5c90862137 | ||
|
|
206926a0a9 | ||
|
|
4e0fe7e6fb | ||
|
|
9230ce558f |
@@ -22,6 +22,7 @@
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "cp .env.example .env && sed -i '/^ENCRYPTION_KEY=/c\\ENCRYPTION_KEY='$(openssl rand -hex 32) .env && sed -i '/^NEXTAUTH_SECRET=/c\\NEXTAUTH_SECRET='$(openssl rand -hex 32) .env && pnpm install && pnpm db:migrate:dev",
|
||||
"postAttachCommand": "pnpm dev --filter=web... --filter=demo...",
|
||||
|
||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "node"
|
||||
|
||||
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,3 +1,5 @@
|
||||
<!-- We require pull request titles to follow the Conventional Commits specification ( https://www.conventionalcommits.org/en/v1.0.0/#summary ). Please make sure your title follow these conventions -->
|
||||
|
||||
## What does this PR do?
|
||||
|
||||
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
||||
@@ -8,18 +10,6 @@ Fixes # (issue)
|
||||
Loom Video: https://www.loom.com/
|
||||
-->
|
||||
|
||||
## Type of change
|
||||
|
||||
<!-- Please mark the relevant points by using [x] -->
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
||||
- [ ] Enhancement (small improvements)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change adds a new database migration
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
## How should this be tested?
|
||||
|
||||
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
|
||||
export const metadata = {
|
||||
title: "Formbricks Responses API Documentation - Manage Your Survey Data Seamlessly",
|
||||
title: "Formbricks Actions API Documentation - Manage Your Survey Data Seamlessly",
|
||||
description:
|
||||
"Unlock the full potential of Formbricks' Client Actions API. Create Actions right from the API.",
|
||||
};
|
||||
@@ -13,8 +13,8 @@ export const metadata = {
|
||||
The Public Client API is designed for the JavaScript SDK and does not require authentication. It's primarily used for creating persons, sessions, and responses within the Formbricks platform. This API is ideal for client-side interactions, as it doesn't expose sensitive information.
|
||||
|
||||
This API can be used to:
|
||||
- [Add Action for User](#add-action-for-user)
|
||||
|
||||
- [Add Action for User](#add-action-for-user)
|
||||
|
||||
---
|
||||
|
||||
@@ -85,4 +85,3 @@ Adds an Actions for a given User by their User ID
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import Image from "next/image";
|
||||
import DemoPreview from "@/components/dummyUI/DemoPreview";
|
||||
import Image from "next/image";
|
||||
|
||||
import CreateChurnFlow from "./create-cancel-flow.webp";
|
||||
import ChangeText from "./change-text.webp";
|
||||
import TriggerInnerText from "./trigger-inner-text.webp";
|
||||
import TriggerCSS from "./trigger-css-selector.webp";
|
||||
import TriggerPageUrl from "./trigger-page-url.webp";
|
||||
import RecontactOptions from "./recontact-options.webp";
|
||||
import CreateChurnFlow from "./create-cancel-flow.webp";
|
||||
import PublishSurvey from "./publish-survey.webp";
|
||||
import RecontactOptions from "./recontact-options.webp";
|
||||
import SelectAction from "./select-action.webp";
|
||||
import TriggerCSS from "./trigger-css-selector.webp";
|
||||
import TriggerInnerText from "./trigger-inner-text.webp";
|
||||
import TriggerPageUrl from "./trigger-page-url.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Mastering Churn Surveys with Formbricks | Essential Tips & Steps",
|
||||
description: "Learn how to effectively utilize Formbricks' Churn Surveys to gain deeper insights into user departures. Dive into a step-by-step guide to craft, trigger, and optimize your churn surveys, ensuring you capture invaluable feedback at critical junctures",
|
||||
description:
|
||||
"Learn how to effectively utilize Formbricks' Churn Surveys to gain deeper insights into user departures. Dive into a step-by-step guide to craft, trigger, and optimize your churn surveys, ensuring you capture invaluable feedback at critical junctures",
|
||||
};
|
||||
|
||||
#### Best Practices
|
||||
@@ -39,15 +40,15 @@ The Churn Survey is among the most effective ways to identify weaknesses in your
|
||||
|
||||
To run the Churn Survey in your app you want to proceed as follows:
|
||||
|
||||
1. Create new Churn Survey at [app.formbricks.com](http://app.formbricks.com/)
|
||||
1. Create new Churn Survey at [app.formbricks.com](https://app.formbricks.com/)
|
||||
2. Set up the user action to display survey at right point in time
|
||||
3. Choose correct recontact options to never miss a feedback
|
||||
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 (takes 15mins max.)](/docs/getting-started/quickstart-in-app-survey)
|
||||
## 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.)](/docs/getting-started/quickstart-in-app-survey)
|
||||
</Note>
|
||||
|
||||
### 1. Create new Churn Survey
|
||||
@@ -60,7 +61,7 @@ Click on "Create Survey" and choose the template “Churn Survey”:
|
||||
src={CreateChurnFlow}
|
||||
alt="Create churn survey by template"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 2. Update questions (if you like)
|
||||
@@ -71,7 +72,7 @@ You’re free to update the question and answer options. However, based on our e
|
||||
src={ChangeText}
|
||||
alt="Change text content"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
_Want to change the button color? You can do so in the product settings._
|
||||
@@ -92,7 +93,7 @@ To create the trigger for your Churn Survey, you have two options to choose from
|
||||
src={TriggerInnerText}
|
||||
alt="Set the trigger by inner Text"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. **Trigger by CSS Selector:** In case you have more than one button saying “Cancel Subscription” in your app and only want to display the survey when one of them is clicked, you want to be more specific. The best way to do that is to give this button the HTML `id=“cancel-subscription”` and set your user action up like so:
|
||||
@@ -101,7 +102,7 @@ To create the trigger for your Churn Survey, you have two options to choose from
|
||||
src={TriggerCSS}
|
||||
alt="Set the trigger by CSS Selector"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. **Trigger by pageURL:** Lastly, you could also display your survey on a subpage “/subscription-cancelled” where you forward users once they cancelled the trial subscription. You can then create a user Action with the type `pageURL` with the following settings:
|
||||
@@ -110,7 +111,7 @@ To create the trigger for your Churn Survey, you have two options to choose from
|
||||
src={TriggerPageUrl}
|
||||
alt="Set the trigger by page URL"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Whenever a user visits this page, matches the filter conditions above and the recontact options (below) the survey will be displayed ✅
|
||||
@@ -118,10 +119,9 @@ Whenever a user visits this page, matches the filter conditions above and the re
|
||||
Here is our complete [Actions manual](/docs/actions/why) covering [Code](/docs/actions/code) and [No-Code](/docs/actions/no-code) Actions.
|
||||
|
||||
<Note>
|
||||
## Pre-churn flow coming soon
|
||||
We’re currently building full-screen survey pop-ups. You’ll be able to prevent users from closing the survey
|
||||
unless they respond to it. It’s certainly debatable if you want that but you could force them to click through
|
||||
the survey before letting them cancel 🤷
|
||||
## Pre-churn flow coming soon We’re currently building full-screen survey pop-ups. You’ll be able to prevent
|
||||
users from closing the survey unless they respond to it. It’s certainly debatable if you want that but you
|
||||
could force them to click through the survey before letting them cancel 🤷
|
||||
</Note>
|
||||
|
||||
### 5. Select Action in the “When to ask” card
|
||||
@@ -130,7 +130,7 @@ Here is our complete [Actions manual](/docs/actions/why) covering [Code](/docs/a
|
||||
src={SelectAction}
|
||||
alt="Select feedback button action"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 6. Last step: Set Recontact Options correctly
|
||||
@@ -141,7 +141,7 @@ Lastly, scroll down to “Recontact Options”. Here you have to choose the corr
|
||||
src={RecontactOptions}
|
||||
alt="Set recontact options"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
These settings make sure the survey is always displayed, when a user wants to Cancel their subscription.
|
||||
@@ -152,13 +152,13 @@ These settings make sure the survey is always displayed, when a user wants to Ca
|
||||
src={PublishSurvey}
|
||||
alt="Publish survey"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
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 Churn Survey in your app. Please follow [this
|
||||
tutorial (Step 4 onwards)](/docs/getting-started/quickstart-in-app-survey) to install the widget.
|
||||
## Formbricks Widget running? You need to have the Formbricks Widget installed to display the Churn Survey
|
||||
in your app. Please follow [this tutorial (Step 4 onwards)](/docs/getting-started/quickstart-in-app-survey)
|
||||
to install the widget.
|
||||
</Note>
|
||||
|
||||
###
|
||||
|
||||
@@ -11,7 +11,8 @@ import SelectAction from "./select-action.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Setting Up Feature Chaser Surveys with Formbricks: A Comprehensive Guide",
|
||||
description: "Learn how to harness the power of Formbricks to gather targeted user feedback on specific features. Dive deep into creating, triggering, and publishing the Feature Chaser survey to enhance your product with actionable insights for specific users.",
|
||||
description:
|
||||
"Learn how to harness the power of Formbricks to gather targeted user feedback on specific features. Dive deep into creating, triggering, and publishing the Feature Chaser survey to enhance your product with actionable insights for specific users.",
|
||||
};
|
||||
|
||||
#### Best Practices
|
||||
@@ -38,13 +39,13 @@ Product analytics never tell you why a feature is used - and why not. Following
|
||||
|
||||
To run the Feature Chaser survey in your app you want to proceed as follows:
|
||||
|
||||
1. Create new Feature Chaser survey at [app.formbricks.com](http://app.formbricks.com/)
|
||||
1. Create new Feature Chaser survey at [app.formbricks.com](https://app.formbricks.com/)
|
||||
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 app. It’s required to display messages
|
||||
and surveys in your app. If not, please follow the [Quick Start Guide (takes 15mins max.)](/docs/getting-started/quickstart-in-app-survey)
|
||||
## 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.)](/docs/getting-started/quickstart-in-app-survey)
|
||||
</Note>
|
||||
|
||||
### 1. Create new Feature Chaser
|
||||
@@ -57,7 +58,7 @@ Click on "Create Survey" and choose the template “Feature Chaser”:
|
||||
src={CreateSurvey}
|
||||
alt="Create survey by template"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 2. Update questions
|
||||
@@ -68,7 +69,7 @@ The questions you want to ask are dependent on your feature and can be very spec
|
||||
src={ChangeText}
|
||||
alt="Change text content"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Save, and move over to where the magic happens: The “Audience” tab.
|
||||
@@ -87,7 +88,7 @@ There are two ways to track a button:
|
||||
src={ActionText}
|
||||
alt="Set the trigger by inner Text"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. **Trigger by CSS Selector:** In case you have more than one button saying “Export Report” in your app and only want to display the survey when one of them is clicked, you want to be more specific. The best way to do that is to give this button the HTML `id=“export-report-featurename”` and set your user action up like so:
|
||||
@@ -96,7 +97,7 @@ There are two ways to track a button:
|
||||
src={ActionCSS}
|
||||
alt="Set the trigger by CSS Selector"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Please follow our [Actions manual](/docs/actions/why) for an in-depth description of how Actions work.
|
||||
@@ -107,7 +108,7 @@ Please follow our [Actions manual](/docs/actions/why) for an in-depth descriptio
|
||||
src={SelectAction}
|
||||
alt="Select PMF trigger button action"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 5. Last step: Set Recontact Options correctly
|
||||
@@ -118,17 +119,17 @@ Lastly, scroll down to “Recontact Options”. Here you have full freedom to de
|
||||
src={RecontactOptions}
|
||||
alt="Set recontact options"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 7. Congrats! You’re ready to publish your survey 💃
|
||||
|
||||
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
<Image 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 Feature Chaser in your app. Please follow [this
|
||||
tutorial (Step 4 onwards)](/docs/getting-started/quickstart-in-app-survey) to install the widget.
|
||||
## Formbricks Widget running? You need to have the Formbricks Widget installed to display the Feature Chaser
|
||||
in your app. Please follow [this tutorial (Step 4 onwards)](/docs/getting-started/quickstart-in-app-survey)
|
||||
to install the widget.
|
||||
</Note>
|
||||
|
||||
###
|
||||
|
||||
@@ -11,7 +11,8 @@ import SelectAction from "./select-action.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Boost Your Trial Conversion Rates with Formbricks: Comprehensive Guide",
|
||||
description: "Unlock the secret to converting more trial users into paying customers using Formbricks. Understand insights behind trial cancellations and tailor your offering to fit user needs. Dive into our step-by-step tutorial and improve your conversion strategy today",
|
||||
description:
|
||||
"Unlock the secret to converting more trial users into paying customers using Formbricks. Understand insights behind trial cancellations and tailor your offering to fit user needs. Dive into our step-by-step tutorial and improve your conversion strategy today",
|
||||
};
|
||||
|
||||
#### Best Practices
|
||||
@@ -37,14 +38,14 @@ The better you understand why free users don’t convert to paid users, the high
|
||||
|
||||
To display the Trial Conversion Survey in your app you want to proceed as follows:
|
||||
|
||||
1. Create new Trial Conversion Survey at [app.formbricks.com](http://app.formbricks.com/)
|
||||
1. Create new Trial Conversion Survey at [app.formbricks.com](https://app.formbricks.com/)
|
||||
2. Set up the user action to display survey at right point in time
|
||||
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 (takes 15mins max.)](/docs/getting-started/quickstart-in-app-survey)
|
||||
## 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.)](/docs/getting-started/quickstart-in-app-survey)
|
||||
</Note>
|
||||
|
||||
### 1. Create new Trial Conversion Survey
|
||||
@@ -57,7 +58,7 @@ Click on "Create Survey" and choose the template “Improve Trial Conversion”:
|
||||
src={CreateSurvey}
|
||||
alt="Create survey by template"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 2. Update questions (if you like)
|
||||
@@ -68,7 +69,7 @@ You’re free to update the questions and answer options. However, based on our
|
||||
src={ChangeText}
|
||||
alt="Change text content"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
_Want to change the button color? You can do so in the product settings!_
|
||||
@@ -78,8 +79,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 days.
|
||||
## Filter by attribute coming soon We're working on pre-segmenting users by attributes. We will update this
|
||||
manual in the next days.
|
||||
</Note>
|
||||
|
||||
Pre-segmentation isn't relevant for this survey because you likely want to solve all people who cancel their trial. You probably have a specific user action e.g. clicking on "Cancel Trial" you can use to only display the survey to users trialing your product.
|
||||
@@ -94,7 +95,7 @@ How you trigger your survey depends on your product. There are two options:
|
||||
src={ActionPageurl}
|
||||
alt="Change text content"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Whenever a user visits this page, the survey will be displayed ✅
|
||||
@@ -105,7 +106,7 @@ Whenever a user visits this page, the survey will be displayed ✅
|
||||
src={ActionText}
|
||||
alt="Change text content"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Please have a look at our complete [Actions manual](/docs/actions/why) if you have questions.
|
||||
@@ -116,7 +117,7 @@ Please have a look at our complete [Actions manual](/docs/actions/why) if you ha
|
||||
src={SelectAction}
|
||||
alt="Select feedback button action"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 6. Last step: Set Recontact Options correctly
|
||||
@@ -127,17 +128,17 @@ Lastly, scroll down to “Recontact Options”. Here you have to choose the corr
|
||||
src={RecontactOptions}
|
||||
alt="Set recontact options"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 7. Congrats! You’re ready to publish your survey 💃
|
||||
|
||||
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
<Image 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)](/docs/getting-started/quickstart-in-app-survey) 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)](/docs/getting-started/quickstart-in-app-survey)
|
||||
to install the widget.
|
||||
</Note>
|
||||
|
||||
###
|
||||
|
||||
@@ -14,7 +14,8 @@ import SelectAction from "./select-action.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Maximize User Interview Participation with In-app Interview Prompts",
|
||||
description: "Engage with your power users seamlessly using Formbricks' In-app Interview Prompt. Ditch traditional email invites and experience way more more respondents. Dive into our comprehensive guide on setting up auto-scheduled interviews today and enhance your user understanding",
|
||||
description:
|
||||
"Engage with your power users seamlessly using Formbricks' In-app Interview Prompt. Ditch traditional email invites and experience way more more respondents. Dive into our comprehensive guide on setting up auto-scheduled interviews today and enhance your user understanding",
|
||||
};
|
||||
|
||||
#### Best Practices
|
||||
@@ -42,14 +43,14 @@ Product analytics and in-app surveys are incomplete without user interviews. Set
|
||||
|
||||
To display an Interview Prompt in your app you want to proceed as follows:
|
||||
|
||||
1. Create new Interview Prompt at [app.formbricks.com](http://app.formbricks.com/)
|
||||
1. Create new Interview Prompt at [app.formbricks.com](https://app.formbricks.com/)
|
||||
2. Adjust content and settings
|
||||
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 (15mins).](/docs/getting-started/quickstart-in-app-survey)
|
||||
## 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).](/docs/getting-started/quickstart-in-app-survey)
|
||||
</Note>
|
||||
|
||||
### 1. Create new Interview Prompt
|
||||
@@ -62,7 +63,7 @@ Click on "Create Survey" and choose the template “Interview Prompt”:
|
||||
src={CreatePrompt}
|
||||
alt="Create interview prompt by template"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 2. Update prompt and CTA
|
||||
@@ -73,7 +74,7 @@ Update the prompt, description and button text to match your products tonality.
|
||||
src={ChangeText}
|
||||
alt="Change text content"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
In the button settings you have to make sure it is set to “External URL”. In the URL field, copy your booking link (e.g. https://cal.com/company/user-interview). If you don’t have a booking link yet, head over to [cal.com](http://cal.com) and get one - they have the best free plan out there!
|
||||
@@ -82,7 +83,7 @@ In the button settings you have to make sure it is set to “External URL”. In
|
||||
src={InterviewExample}
|
||||
alt="Add CSS action"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Save, and move over to the “Audience” tab.
|
||||
@@ -90,8 +91,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.
|
||||
@@ -104,12 +105,12 @@ Great, now only the “Power User” segment will see our Interview Prompt. But
|
||||
|
||||
To create the trigger to show your Interview Prompt, go to the “Audience” tab, find the “When to send” card and choose “Add Action”. We will now use our super cool No-Code User Action Tracker:
|
||||
|
||||
<Image src={AddAction} alt="Add action" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
<Image src={AddAction} alt="Add action" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
|
||||
|
||||
<Note>
|
||||
## You can also add actions in your code
|
||||
You can also create [Code Actions](/docs/actions/code) using `formbricks.track("Eventname")` - they will automatically
|
||||
appear in your Actions overview as long as the SDK is embedded.
|
||||
## You can also add actions in your code You can also create [Code Actions](/docs/actions/code) using
|
||||
`formbricks.track("Eventname")` - they will automatically appear in your Actions overview as long as the SDK
|
||||
is embedded.
|
||||
</Note>
|
||||
|
||||
Generally, we have two types of user actions: Page views and clicks. The Interview Prompt, you’ll likely want to display it on a page visit since you already filter who sees the prompt by attributes.
|
||||
@@ -120,18 +121,18 @@ Generally, we have two types of user actions: Page views and clicks. The Intervi
|
||||
src={ActionPageurl}
|
||||
alt="Add page URL action"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. **innerText & CSS-Selector:** When a user clicks an element (like a button) with a specific text content or CSS selector, the prompt will be displayed as long as the other conditions also match.
|
||||
|
||||
<div className="flex max-w-full flex-col sm:max-w-3xl lg:gap-1">
|
||||
<Image src={ActionCSS} alt="Add CSS action" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
<Image src={ActionCSS} alt="Add CSS action" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
|
||||
<Image
|
||||
src={ActionInner}
|
||||
alt="Add inner text action"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +142,7 @@ Generally, we have two types of user actions: Page views and clicks. The Intervi
|
||||
src={SelectAction}
|
||||
alt="Select feedback button action"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 6. Set Recontact Options correctly
|
||||
@@ -152,17 +153,17 @@ Scroll down to “Recontact Options”. Here you have to choose the correct sett
|
||||
src={RecontactOptions}
|
||||
alt="Set recontact options"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 7. Congrats! You’re ready to publish your survey 💃 🤸
|
||||
|
||||
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
<Image 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)](/docs/getting-started/quickstart-in-app-survey) 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)](/docs/getting-started/quickstart-in-app-survey)
|
||||
to install the widget.
|
||||
</Note>
|
||||
|
||||
###
|
||||
|
||||
@@ -11,7 +11,8 @@ import SelectAction from "./select-action.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "How to Set Up a Product-Market Fit Survey Using Formbricks - Step-by-Step Guide",
|
||||
description: "Learn to leverage Formbricks to create and implement a Product-Market Fit survey in your web app. Follow our detailed step-by-step guide to measure and understand your PMF effectively. Ensure high data quality, efficient triggers, and actionable insights.",
|
||||
description:
|
||||
"Learn to leverage Formbricks to create and implement a Product-Market Fit survey in your web app. Follow our detailed step-by-step guide to measure and understand your PMF effectively. Ensure high data quality, efficient triggers, and actionable insights.",
|
||||
};
|
||||
|
||||
#### Best Practices
|
||||
@@ -36,14 +37,14 @@ Measuring and understanding your PMF is essential to build a large, successful b
|
||||
|
||||
To display the Product-Market Fit survey in your app you want to proceed as follows:
|
||||
|
||||
1. Create new Product-Market Fit survey at [app.formbricks.com](http://app.formbricks.com/)
|
||||
1. Create new Product-Market Fit survey at [app.formbricks.com](https://app.formbricks.com/)
|
||||
2. Setup pre-segmentation to assure high data quality
|
||||
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 (15mins).](/docs/getting-started/quickstart-in-app-survey)
|
||||
## 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).](/docs/getting-started/quickstart-in-app-survey)
|
||||
</Note>
|
||||
|
||||
### 1. Create new PMF survey
|
||||
@@ -56,7 +57,7 @@ Click on "Create Survey" and choose one of the PMF survey templates. The first o
|
||||
src={CreateSurvey}
|
||||
alt="Create survey by template"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 2. Update questions (if you like)
|
||||
@@ -67,7 +68,7 @@ You’re free to update the question and answer options. However, based on our e
|
||||
src={ChangeText}
|
||||
alt="Change text content"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
_Want to change the button color? You can do so in the product settings!_
|
||||
@@ -77,8 +78,8 @@ Save, and move over to where the magic happens: 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 days.
|
||||
## Filter by attribute coming soon We're working on pre-segmenting users by attributes. We will update this
|
||||
manual in the next days.
|
||||
</Note>
|
||||
|
||||
To run this survey properly, you should pre-segment your user base. As touched upon earlier: if you ask every user you’ll get lots of opinions which are often misleading. You only want to gather feedback from people who invested the time to get to know and use your product:
|
||||
@@ -96,25 +97,31 @@ This way you make sure that you separate potentially misleading opinions from va
|
||||
### 4. Set up a trigger for the Product-Market Fit survey:
|
||||
|
||||
You need a trigger to display the survey but in this case, the filtering does all the work. It’s up to you to decide to display the survey after the user viewed a specific subpage (pageURL) or after clicking an element. Have a look at the [Actions manual](/docs/actions/why) if you are not sure how to set them up:
|
||||
|
||||
<Col>
|
||||
<div>
|
||||
<Image src={ActionCSS} alt="Add CSS action" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
<Image
|
||||
src={ActionPageurl}
|
||||
alt="Add inner text action"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Image
|
||||
src={ActionCSS}
|
||||
alt="Add CSS action"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
<Image
|
||||
src={ActionPageurl}
|
||||
alt="Add inner text action"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
### 5. Select Action in the “When to ask” card
|
||||
<Col>
|
||||
<Image
|
||||
src={SelectAction}
|
||||
alt="Select PMF trigger button action"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
/>
|
||||
<Image
|
||||
src={SelectAction}
|
||||
alt="Select PMF trigger button action"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
</Col>
|
||||
### 6. Last step: Set Recontact Options correctly
|
||||
|
||||
@@ -124,17 +131,17 @@ Lastly, scroll down to “Recontact Options”. Here you have to choose the corr
|
||||
src={RecontactOptions}
|
||||
alt="Set recontact options"
|
||||
quality="100"
|
||||
className="rounded-lg max-w-full sm:max-w-3xl"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
### 7. Congrats! You’re ready to publish your survey 💃
|
||||
|
||||
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg max-w-full sm:max-w-3xl" />
|
||||
<Image 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)](/docs/getting-started/quickstart-in-app-survey) 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)](/docs/getting-started/quickstart-in-app-survey)
|
||||
to install the widget.
|
||||
</Note>
|
||||
|
||||
###
|
||||
|
||||
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 76 KiB |
@@ -6,12 +6,9 @@ import GitpodPorts from "./gitpod/ports.webp";
|
||||
import GitpodPreparing from "./gitpod/preparing.webp";
|
||||
import GitpodRunning from "./gitpod/running.webp";
|
||||
|
||||
import GithubCodespaceEnvFile from "./github-codespaces/env.webp";
|
||||
import GithubCodespaceLoading from "./github-codespaces/loading.webp";
|
||||
import GithubCodespaceNew from "./github-codespaces/new.webp";
|
||||
import GithubCodespacePorts from "./github-codespaces/ports.webp";
|
||||
import GithubCodespaceRun from "./github-codespaces/run.webp";
|
||||
import GithubCodespaceTerminal from "./github-codespaces/terminal.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Formbricks Development Setup: Complete Guide to Local Environment Configuration for Dev",
|
||||
@@ -38,7 +35,7 @@ This will open a fully configured workspace in your browser with all the necessa
|
||||
|
||||
### [Github Codespaces](#Github-codespaces)
|
||||
|
||||
This will open a Github VSCode Interface on the cloud for you. This setup will have the Formbricks codebase and all the dependencies installed. Click the button below to configure your instance and open the project in Github Codespaces. For a detailed guide, visit the [Github Codespaces Setup Guide](#github-codespaces-guide) section below.
|
||||
This will open a Github VSCode Interface on the cloud for you. This setup will have the Formbricks codebase, all the dependencies installed & Formbricks running. Click the button below to configure your instance and open the project in Github Codespaces. For a detailed guide, visit the [Github Codespaces Setup Guide](#github-codespaces-guide) section below.
|
||||
|
||||
[](https://Github.com/codespaces/new?machine=standardLinux32gb&repo=500289888&ref=main&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=EastUs2)
|
||||
|
||||
@@ -225,44 +222,7 @@ These URLs and port numbers represent various services and endpoints within your
|
||||
|
||||
3. Once the Codespace is loaded, you will be redirected to the VSCode editor. You can start working on your project in this environment.
|
||||
|
||||
4. Make the changes you want to, and now, to run the app, we first need to configure the .env file. Copy the .env.example and edit the variables as mentioned in the file itself.
|
||||
|
||||
<Image
|
||||
src={GithubCodespaceEnvFile}
|
||||
alt="Github Codespace Env File"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
5. Once you have configured the .env, it's now time to run the app and see the changes. Lets open the terminal first
|
||||
|
||||
<Image
|
||||
src={GithubCodespaceTerminal}
|
||||
alt="Github Codespace Open Terminal"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
6. Now, run the following command to run the app
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Run the entire Formbricks Stack">
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
<Image
|
||||
src={GithubCodespaceRun}
|
||||
alt="Run on Github Codespace"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
7. Monitor the logs in the terminal and once you see the following, you are good to go!
|
||||
4. Monitor the logs in the terminal and once you see the following, you are good to go!
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="The WebApp is running">
|
||||
@@ -280,7 +240,7 @@ pnpm dev
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
8. Right next to the Terminal, you will see a **Ports** tab, click on it to see the ports and their respective URLs. Now access the Forwarded Address for port 3000 and you should be able to visit your Formbricks App!
|
||||
5. Right next to the Terminal, you will see a **Ports** tab, click on it to see the ports and their respective URLs. Now access the Forwarded Address for port 3000 and you should be able to visit your Formbricks App!
|
||||
|
||||
<Image
|
||||
src={GithubCodespacePorts}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Libraries } from "@/components/docs/Libraries";
|
||||
import Image from "next/image";
|
||||
|
||||
import SetupChecklist from "./env-id.webp";
|
||||
import WidgetNotConnected from "./widget-not-connected.webp";
|
||||
import WidgetConnected from "./widget-connected.webp";
|
||||
import ReactApp from "./react-in-app-survey-app-popup-form.webp";
|
||||
import WidgetConnected from "./widget-connected.webp";
|
||||
import WidgetNotConnected from "./widget-not-connected.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Integrate Formbricks: Comprehensive Framework Guide & Integration Tutorial",
|
||||
@@ -222,9 +223,10 @@ Refer to our [Example NextJS App Directory project](https://github.com/formbrick
|
||||
|
||||
```tsx {{ title: 'Typescript' }}
|
||||
// other import
|
||||
import formbricks from "@formbricks/js";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import formbricks from "@formbricks/js";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
formbricks.init({
|
||||
@@ -386,6 +388,43 @@ To this:
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Overwrite CSS Styles for In-App Surveys
|
||||
|
||||
You can overwrite the default CSS styles for the in-app surveys by adding the following CSS to your global CSS file (eg. `globals.css`):
|
||||
|
||||
Make sure that you do not change the CSS variable names as they are used by Formbricks to identify the CSS variables. You can change the values to your liking. We have filled in some sample values for you to change according to your desired appearance.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Overwrite Formbricks CSS">
|
||||
|
||||
```css
|
||||
/* Formbricks CSS */
|
||||
--fb-brand-color: red;
|
||||
--fb-brand-text-color: white;
|
||||
--fb-border-color: green;
|
||||
--fb-border-color-highlight: rgb(13, 13, 12);
|
||||
--fb-focus-color: red;
|
||||
--fb-heading-color: yellow;
|
||||
--fb-subheading-color: green;
|
||||
--fb-info-text-color: orange;
|
||||
--fb-signature-text-color: blue;
|
||||
--fb-survey-background-color: black;
|
||||
--fb-accent-background-color: rgb(13, 13, 12);
|
||||
--fb-accent-background-color-selected: red;
|
||||
--fb-placeholder-color: white;
|
||||
--fb-shadow-color: var(--fb-brand-color);
|
||||
--fb-rating-fill: rgb(13, 13, 12);
|
||||
--fb-rating-hover: green;
|
||||
--fb-back-btn-border: blue;
|
||||
--fb-submit-btn-border: transparent;
|
||||
--fb-rating-selected: black;
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
We have an example of this in our [Demo project](https://github.com/formbricks/formbricks/blob/main/apps/demo/styles/globals.css.) here.
|
||||
|
||||
**Can’t figure it out? [Join our Discord!](https://formbricks.com/discord)**
|
||||
|
||||
---
|
||||
|
||||
@@ -123,6 +123,7 @@ We will first create a Google Cloud Project and then enable the Google Sheets AP
|
||||
- `GOOGLE_SHEETS_CLIENT_ID` - Client ID
|
||||
- `GOOGLE_SHEETS_CLIENT_SECRET` - Client Secret
|
||||
16. Also use the **same Authorized redirect URI** in the `GOOGLE_SHEETS_REDIRECT_URL` environment variable.
|
||||
17. One last that we need to do is to **enable the Google Drive API** for the project. For that, go to the "**APIs & Services**" section and click on the "**Enable APIs and Services**" button and search for "**Google Drive API**" and enable it.
|
||||
|
||||
### By now, your environment variables should include the below ones as well:
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 45 KiB |
133
apps/formbricks-com/app/docs/integrations/wordpress/page.mdx
Normal file
@@ -0,0 +1,133 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import Img1 from "./1-wordpress-targeted-survey-on-website-free.webp";
|
||||
import Img2 from "./2-run-website-survey-wordpress-targeted-for-free.webp";
|
||||
import Img3 from "./3-wordpress-setup-survey-on-website-targeted-free-open-source.webp";
|
||||
import Img4 from "./4-wordpress-website-survey-target-visitor-free.webp";
|
||||
import Img5 from "./step-4-copy-to-wordpress-for-free-targeted-survey.webp";
|
||||
import Img6 from "./6-targeted-survey-on-wordpress-website-for-free.webp";
|
||||
import Img7 from "./7-wordpress-free-hotjar-survey-open-source-website-survey-hotjar.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Run targeted surveys on your WordPress page",
|
||||
description:
|
||||
"Target specific visitors with a survey on your WordPress page using Formbricks for free. Show survey on specific page or on button click.",
|
||||
};
|
||||
|
||||
#### WordPress
|
||||
|
||||
# Connect Formbricks with your WordPress page
|
||||
|
||||
If you want to run a targeted survey on your WordPress website, Formbricks is the way to go! With our generous free plan and open source tech, you get everything you need to get started and keep full control over your data.
|
||||
|
||||
## TLDR
|
||||
|
||||
1. Install the Formbricks WordPress plugin
|
||||
2. Create a [free Formbricks account](https://app.formbricks.com/auth/signup)
|
||||
3. Find and copy the `environment id`
|
||||
4. Copy the environment id into the right field in the plugin settings
|
||||
5. Create survey on trigger “New Session” to test it
|
||||
|
||||
## Step 1: Install the Formbricks WordPress plugin
|
||||
|
||||
As long as the Formbricks plugin is in review, please download it from our [GitHub repository directly.](https://github.com/formbricks/wordpress)
|
||||
|
||||
<Image
|
||||
src={Img1}
|
||||
alt="Run targeted website survye on any WordPress site"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Step 2: Create a Formbricks Account
|
||||
|
||||
This is super straight forward: Go to [app.formbricks.com/auth/signup](https://app.formbricks.com/auth/signup), create the account, verify your email and you’re in!
|
||||
|
||||
When you see this screen, you’re there:
|
||||
|
||||
<Image
|
||||
src={Img2}
|
||||
alt="Free HotJar survey alternative open source"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Step 3: Find and copy the environmentId
|
||||
|
||||
Go to Settings > Setup Checklist where you’ll find your environmentId:
|
||||
|
||||
<Image
|
||||
src={Img3}
|
||||
alt="Run targeted surveys for free on WordPress pages"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Step 4: Copy the environmentId to the WordPress Plugin Settings
|
||||
|
||||
In your WordPress instance, go to the Formbricks Plugin settings and copy the environmentId in the correct field:
|
||||
|
||||
<Image
|
||||
src={Img5}
|
||||
alt="Free and open source HotJar survey on WordPress page"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Then click the button at the bottom to see if the connection worked.
|
||||
|
||||
<Note>If you don’t use our Cloud, you also have to update the API Host</Note>
|
||||
|
||||
Great!
|
||||
|
||||
## Step 5: Create survey on trigger “New Session”
|
||||
|
||||
Now that all is setup, we create a survey to display an example survey. Pick any template here:
|
||||
|
||||
<Image
|
||||
src={Img2}
|
||||
alt="Free HotJar survey alternative open source"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Keep the content for now, click on the Settings tab:
|
||||
|
||||
<Image
|
||||
src={Img4}
|
||||
alt="Free and open source HotJar survey on WordPress page"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
Here we do three things:
|
||||
|
||||
1. Change survey type to “In-App Survey”
|
||||
2. Select trigger “New Session”
|
||||
3. Publish
|
||||
|
||||
<Image
|
||||
src={Img6}
|
||||
alt="Open Source survey on targeted website wordpress"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
When you see this page, you did it!
|
||||
|
||||
<Image
|
||||
src={Img7}
|
||||
alt="Run free an open source targeted survey on any page"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Step 6: Reload your page to check out your survey 🤓
|
||||
|
||||
You did it! Reload the WordPress page and your survey should appear!
|
||||
|
||||
|
||||
## Doesn't work?
|
||||
Join our [Discord to get help 🤓](https://formbricks.com/discord)
|
||||
|
After Width: | Height: | Size: 76 KiB |
@@ -0,0 +1,70 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import ShareLink from "./share-link.webp";
|
||||
import ViewResponse from "./view-response.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Source Tracking",
|
||||
description: "Track the source of your users in an easy & compliant way!",
|
||||
};
|
||||
|
||||
#### Link Surveys
|
||||
|
||||
# Source Tracking
|
||||
|
||||
Understand the source a survey respondent comes from when responding to your survey - all while keeping data privacy standards high!
|
||||
|
||||
Check out this video to learn more about source tracking in link surveys:
|
||||
|
||||
{/* Replace link below with our new link on Source Tracking */}
|
||||
<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
|
||||
|
||||
Source tracking for link surveys is essential when you:
|
||||
|
||||
- Want to analyze the origin of your survey respondents.
|
||||
- Aim to ensure compliance with tracking and data collection regulations.
|
||||
|
||||
## Code Example
|
||||
<Col>
|
||||
<CodeGroup title="Example Source as Google">
|
||||
|
||||
```sh
|
||||
https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?source=Google
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
## How it Works
|
||||
|
||||
To track the source of users in your link surveys effectively, follow these steps:
|
||||
|
||||
1. **Generate Survey URL**: Create a Link Survey and get the sharable link. Append `?source=YourSouce` to the link to reference it with your campaigns and sources.
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Example Source as Google">
|
||||
|
||||
```sh
|
||||
https://formbricks.com/clin3dxja02k8l80hpwmx4bjy?source=Google
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
2. **Collect Data**: When users access the survey through these links, the URL parameters will capture the source information from which they were shared.
|
||||
|
||||
3. **View Responses**: Use the collected source data to analyze where your survey respondents are coming from. You can hover over the user icon in the responses tab to see the source of the user.
|
||||
|
||||
<Image
|
||||
src={ViewResponse}
|
||||
alt="View Source in Response"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
4. **Analyse Data**: Download all the responses as a CSV/Excel and get access to the source information. This can provide valuable insights into your audience.
|
||||
|
||||
Source tracking allows you to make informed decisions based on the origin of your survey participants, helping you tailor your surveys and marketing strategies accordingly.
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
@@ -218,6 +218,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Data Prefilling", href: "/docs/link-surveys/data-prefilling" },
|
||||
{ title: "Identify Users", href: "/docs/link-surveys/user-identification" },
|
||||
{ title: "Single Use Links", href: "/docs/link-surveys/single-use-links" },
|
||||
{ title: "Source Tracking", href: "/docs/link-surveys/source-tracking" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -240,6 +241,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Notion", href: "/docs/integrations/notion" },
|
||||
{ title: "Make.com", href: "/docs/integrations/make" },
|
||||
{ title: "n8n", href: "/docs/integrations/n8n" },
|
||||
{ title: "Wordpress", href: "/docs/integrations/wordpress" },
|
||||
{ title: "Zapier", href: "/docs/integrations/zapier" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import jsPackageJson from "@/../../packages/js/package.json";
|
||||
import clsx from "clsx";
|
||||
import { useState } from "react";
|
||||
import { IoLogoHtml5, IoLogoNpm } from "react-icons/io5";
|
||||
@@ -45,7 +46,7 @@ export const SetupInstructions: React.FC = ({}) => {
|
||||
return (
|
||||
<div>
|
||||
<TabBar tabs={tabs} activeId={activeTab} setActiveId={setActiveTab} />
|
||||
<div className="h-80 max-w-lg px-4 sm:max-w-lg md:max-w-lg">
|
||||
<div className="h-84 max-w-lg px-4 sm:max-w-lg md:max-w-lg">
|
||||
{activeTab === "npm" ? (
|
||||
<>
|
||||
<CodeBlock>npm install @formbricks/js</CodeBlock>
|
||||
@@ -61,7 +62,19 @@ if (typeof window !== "undefined") {
|
||||
</>
|
||||
) : activeTab === "html" ? (
|
||||
<CodeBlock>{`<script type="text/javascript">
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.4.0/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init("claDadXk29dak92dK9","https://app.formbricks.com")},500)}();
|
||||
|
||||
!function(){
|
||||
var jsPackageJson = require('@/package.json'); // Make sure the path is correct
|
||||
var t = document.createElement("script");
|
||||
t.type = "text/javascript";
|
||||
t.async = true;
|
||||
t.src = "https://unpkg.com/@formbricks/js@^${jsPackageJson.version}/dist/index.umd.js";
|
||||
var e = document.getElementsByTagName("script")[0];
|
||||
e.parentNode.insertBefore(t, e);
|
||||
setTimeout(function(){
|
||||
window.formbricks.init("claDadXk29dak92dK9","https://app.formbricks.com")
|
||||
}, 500);
|
||||
}();
|
||||
</script>`}</CodeBlock>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
import AuthorOla from "@/images/blog/ola-content-writer.png";
|
||||
import AuthorOla from "@/images/blog/ola-content-writer.jpg";
|
||||
import Image from "next/image";
|
||||
|
||||
interface AuthorBoxProps {
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function LayoutMdx({ meta, children }: Props) {
|
||||
)}
|
||||
</header>
|
||||
)}
|
||||
<Prose className="prose-h2:text-2xl prose-li:text-base prose-h2:mt-4 prose-p:text-base prose-p:mb-4 prose-h3:text-xl prose-a:text-slate-900 prose-a:hover:text-slate-900 prose-a:text-decoration-brand prose-a:not-italic ">
|
||||
<Prose className="prose-h2:text-2xl prose-li:text-base prose-h2:mt-4 prose-p:text-base prose-p:mb-4 prose-h3:text-xl prose-a:text-slate-900 prose-a:hover:text-slate-900 prose-a:text-decoration-brand prose-a:not-italic prose-ul:pl-12">
|
||||
{children}
|
||||
</Prose>
|
||||
</article>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -19,8 +20,10 @@ export default function MetaInformation({
|
||||
section,
|
||||
tags,
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
const pageTitle = `${title}`;
|
||||
const BASE_URL = `https://${process.env.VERCEL_URL}`;
|
||||
const canonicalLink = `${BASE_URL}${router.asPath}`;
|
||||
return (
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
@@ -30,7 +33,7 @@ export default function MetaInformation({
|
||||
<meta name="image" content={`https://${BASE_URL}/favicon.ico`} />
|
||||
<meta property="og:image" content={`https://${BASE_URL}/social-image.png`} />
|
||||
<link rel="icon" type="image/x-icon" href={`https://${BASE_URL}/favicon.ico`} />
|
||||
<link rel="canonical" href="https://formbricks.com/" />
|
||||
<link rel="canonical" href={canonicalLink} />
|
||||
<meta name="msapplication-TileColor" content="#00C4B8" />
|
||||
<meta name="msapplication-TileImage" content={`https://${BASE_URL}/favicon.ico`} />
|
||||
<meta property="og:image:alt" content="Open Source Experience Management, Privacy-first" />
|
||||
|
||||
BIN
apps/formbricks-com/images/blog/ola-content-writer.jpg
Normal file
|
After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 75 KiB |
@@ -1,19 +1,18 @@
|
||||
export const handleFeedbackSubmit = async (YesNo: string, pageUrl: string | null) => {
|
||||
const response_data = {
|
||||
data: {
|
||||
isHelpful: YesNo,
|
||||
pageUrl: pageUrl,
|
||||
},
|
||||
isHelpful: YesNo,
|
||||
pageUrl: pageUrl,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
response: response_data,
|
||||
surveyId: process.env.NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID,
|
||||
finished: true,
|
||||
data: response_data,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_FORMBRICKS_COM_API_HOST}/api/v1/client/environments/${process.env.NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID}/responses`,
|
||||
`${process.env.NEXT_PUBLIC_FORMBRICKS_COM_API_HOST}/api/v1/client/${process.env.NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID}/responses`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
|
||||
@@ -10,6 +10,11 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
description: "Build build custom software on top of your data.",
|
||||
href: "https://www.appsmith.com",
|
||||
},
|
||||
{
|
||||
name: "Argos",
|
||||
description: "Argos provides the developer tools to debug tests and detect visual regressions..",
|
||||
href: "https://argos-ci.com",
|
||||
},
|
||||
{
|
||||
name: "BoxyHQ",
|
||||
description:
|
||||
@@ -104,6 +109,16 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
|
||||
"Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.",
|
||||
href: "https://infisical.com",
|
||||
},
|
||||
{
|
||||
name: "Langfuse",
|
||||
description: "Open source LLM engineering platform. Debug, analyze and iterate together.",
|
||||
href: "https://langfuse.com",
|
||||
},
|
||||
{
|
||||
name: "Lost Pixel",
|
||||
description: "Open source visual regression testing alternative to Percy & Chromatic",
|
||||
href: "https://lost-pixel.com",
|
||||
},
|
||||
{
|
||||
name: "Mockoon",
|
||||
description: "Mockoon is the easiest and quickest way to design and run mock REST APIs.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import AuthorOla from "@/images/blog/ola-content-writer.png";
|
||||
import Image from "next/image";
|
||||
|
||||
import Appcues from "./best-feedback-app-2024-appcues-feedback-app.png";
|
||||
import Header from "./best-feedback-app-2024-free-in-app-header-image.webp";
|
||||
import Formbricks from "./formbricks-best-open-source-feedback-app.png";
|
||||
@@ -91,7 +91,7 @@ Among the plethora of tools available in today’s market, this section will gui
|
||||
className="w-full rounded-lg"
|
||||
/>
|
||||
|
||||
[Formbricks](http://formbricks.com/) is an open-source micro-survey solution designed to gather specific user feedback at the perfect moment in their journey. It allows you to create and deploy targeted surveys within your app without disrupting the user experience.
|
||||
[Formbricks](https://formbricks.com/) is an open-source micro-survey solution designed to gather specific user feedback at the perfect moment in their journey. It allows you to create and deploy targeted surveys within your app without disrupting the user experience.
|
||||
|
||||
Formbricks boasts a user-friendly interface, making it easy for both technical and non-technical users to create and deploy micro-surveys. The no-code editor removes the need for coding knowledge, while the intuitive design guides you through the process. Being the only open-source feedback app out there, privacy-focused users will love this!
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 136 KiB |
@@ -0,0 +1,251 @@
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import Image from "next/image";
|
||||
|
||||
import Formbricks from "./formbricks-best-open-source-hotjar-alternative.webp";
|
||||
import FullStory from "./fullstory-comprehensive-analytics-tool.webp";
|
||||
import Smartlook from "./g2-crowd-award-winner-Smartlook.webp";
|
||||
import Header from "./header-best-hotjar-alternatives-2024-incl-open-source-solutions.webp";
|
||||
import LuckyOrange from "./lucky-orange-best-analytics-tool-2024.webp";
|
||||
import MouseFlow from "./mouseflow-best-hotjar-alternatives-2024.webp";
|
||||
|
||||
export const meta = {
|
||||
title: "Best HotJar Alternatives 2024 incl. Open Source",
|
||||
description:
|
||||
"Looking for HotJar alternatives? We curated a list of the best HotJar alternatives going into 2024 for you.",
|
||||
date: "2023-12-29",
|
||||
publishedTime: "2023-12-29T12:00:00",
|
||||
authors: ["Olasunkanmi Balogun"],
|
||||
section: "Feedback Apps",
|
||||
tags: ["Feedback Apps", "Formbricks", "Smartlook", "Lucky Orange", "Fullstory", "Mouseflow"],
|
||||
};
|
||||
|
||||
<Image src={Header} alt="Get the best HotJar features with these 5 tools." className="w-full rounded-lg" />
|
||||
|
||||
<AuthorBox
|
||||
name="Olasunkanmi Balogun"
|
||||
title="Content Writer"
|
||||
date="December 29th, 2023"
|
||||
duration="10"
|
||||
author={"Ola"}
|
||||
/>
|
||||
|
||||
HotJar is a popular product experience insights platform that provides you with valuable data and insights into how users interact with your websites. It offers features such as heatmaps and recordings, surveys, and funnels to help you get these insights.
|
||||
|
||||
But while it’s a staple in user behavior analytics, this article introduces a wide range of alternatives just waiting to be discovered. These options are just for you, whether you're a budget-conscious blogger or an enterprise giant.
|
||||
|
||||
As we discuss these options, the next section will guide you through the essential criteria to consider when comparing them to HotJar. These criteria will empower you to pinpoint the perfect fit for your specific requirements.
|
||||
|
||||
## How we compare HotJar alternatives 👇
|
||||
|
||||
We'll categorize the criteria into three main factors to improve your choice.
|
||||
|
||||
1. **Feature depth**
|
||||
|
||||
- **Surveys & Forms**: Can you gather users’ voices through polls and surveys to understand your audience better?
|
||||
- **Heatmaps & Recordings**: Do you want basic click maps or detailed session replays with visitor insights?
|
||||
- **Integrations**: Does it work well with your existing third-party analytics tools?
|
||||
|
||||
2. **Pricing**:
|
||||
|
||||
- **Freemium Plans**
|
||||
- **Premium Plans**
|
||||
|
||||
3. **Privacy**: Is it fully GDPR, CCPA, or HIPAA-compliant?
|
||||
4. **Extensibility**: How extensible and customizable are each of the tools?
|
||||
|
||||
Now, we will explore these options based on the factors mentioned above.
|
||||
|
||||
## 5 Free HotJar Alternatives in 2024
|
||||
|
||||
Let's have a look at the best HotJar alternatives in 2024, including open source options - all of which start free!
|
||||
|
||||
### Formbricks - The Open Source HotJar Ask Alternative
|
||||
|
||||
<Image
|
||||
src={Formbricks}
|
||||
alt="Formbricks is a free and open source survey software for in app micro surveys. Ask any user segment at any point in the user journey."
|
||||
className="w-full rounded-lg"
|
||||
/>
|
||||
|
||||
[Formbricks](https://formbricks.com/) is an open source micro-survey solution designed to gather specific user feedback at the perfect moment in the journey. It allows you to create and deploy **targeted surveys within your app or on public websites** without disrupting the user experience.
|
||||
|
||||
It's super good at one thing: making your forms and survey experiences awesome. It shows you why people abandon your forms, which questions cause churn, and how to fix them to get more people to finish. No heatmaps or fancy recordings, just laser focus on surveys.
|
||||
|
||||
Formbricks compares to HotJar based on the aforementioned factors:
|
||||
|
||||
**Feature depth**:
|
||||
|
||||
- **Surveys & Forms**: Formbricks specializes 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. You can seamlessly integrate surveys into web, mobile, and desktop applications. If forms and surveys are your primary concern, this is the best tool for you.
|
||||
- **Heatmaps & Recordings**: Formbricks focuses on forms and surveys. No fancy website heatmaps here for now, but you get detailed insights into individual form fields and how users interact with them. If you’re looking for open source heatmaps, [OpenReplay](https://openreplay.com/) might be worth checking out.
|
||||
- **Integrations:** HotJar plays well with lots of other tools, while Formbricks is still young in this aspect. But it works with the most popular ones, like Zapier, Make.com, Airtable, Notion, Slack, etc. The Formbricks team and [open source community](/community) are working on adding more all the time.
|
||||
|
||||
**Pricing**: Both tools have free plans, but HotJar's paid plans can get expensive. Formbricks is generally cheaper, especially if you only care about targeted surveys. Formbricks has a very generous free plan to get started easily. Paid plans begin at $30 per month for link surveys and $0.15 per submission for web and in-app surveys, **after your survey submission exceeds 250 submissions.** If you self-host Formbricks, [it’s completely free.](/pricing)
|
||||
|
||||
**Privacy:** Formbricks Cloud is hosted in Germany and has full GDPR as well as CCPA compliance. Since Formbricks is easily self-hostable, keeping full control over your data is smooth.
|
||||
|
||||
**Extensibility:** Unlike HotJar, Formbricks is an open source solution. It provides APIs that allow you to build anything on top, below, and around it as per your customization needs - your imagination is the limit.
|
||||
|
||||
Let’s see how Formbricks compares side-by-side with HotJar.
|
||||
|
||||
| Factors | Formbricks | HotJar |
|
||||
| ---------------------- | ---------- | ------ |
|
||||
| Surveys & Forms | 🟢 | 🟢 |
|
||||
| Heatmaps & Recordings | 🔴 | 🟢 |
|
||||
| Integrations | 🟡🟢 | 🟢 |
|
||||
| Pricing | 🟢 | 🟡🟢 |
|
||||
| Privacy and compliance | 🟢 | 🟡 |
|
||||
| Extensibility | 🟢 | 🟡 |
|
||||
|
||||
### Smartlook - G2 Crowd Award Winner
|
||||
|
||||
<Image
|
||||
src={Smartlook}
|
||||
alt="Smartlook is a comprehensive product analytics and visual user insights tool designed to help businesses gain deep insights into user behavior on their websites or mobile applications."
|
||||
className="w-full rounded-lg"
|
||||
/>
|
||||
|
||||
Smartlook, which was recently acquired by Cisco, is a comprehensive product analytics and visual user insights tool designed to help businesses gain deep insights into user behavior on their websites or mobile applications. It offers a range of features that enable organizations to understand, analyze, and optimize the user experience.
|
||||
|
||||
Smartlook has also received numerous awards and recognition, including the G2 Crowd Awards for **Top 100 Software Products** and **Best Products for Marketers**, as well as being named on Deloitte’s Technology Fast 50 in Central Europe.
|
||||
|
||||
Below is an overview of how Smartlook compares with HotJar.
|
||||
|
||||
**Features**:
|
||||
|
||||
- **Surveys**: Both platforms include survey features, but Smartlook does not offer standalone surveys, unlike HotJar. Instead, you can create surveys through its integration with Survicate (for a deeper look into Survicate, go [here](https://formbricks.com/blog/best-feedback-app-and-how-to-use-them)).
|
||||
- **Heatmaps & Recordings**: They both offer heatmap and recording features. Although Smartlook provides a more comprehensive insight into recordings by combining them with funnel analysis, this will help you pinpoint the exact recordings you need.
|
||||
- **Integrations:** Both platforms work well with many external tools. However, if you're a big enterprise seeking a broader selection of tools, HotJar is the better choice.
|
||||
|
||||
**Pricing**: Compared to HotJar, Smartlook is relatively more expensive. Its pro plan provides only 30 heatmaps a month and three months of storage. HotJar’s equivalent business plan offers unlimited heatmaps with 12 months of data storage.
|
||||
|
||||
**Privacy and Compliance:** Smartlook stores all data on EU servers. If you collect personal data with Smartlook, GDPR applies. Smartlook is also fully CCPA-compliant.
|
||||
|
||||
**Extensibility:** Smartlook, like HotJar, offers practical methods for programmatically accessing information on different resources. The API empowers you to analyze visitor data more comprehensively and delve deeper into the values captured by Smartlook. However, it may not offer the precise level of control and flexibility needed for intricate integrations or custom workflows.
|
||||
|
||||
| Factors | Smartlook | HotJar |
|
||||
| ---------------------- | --------- | ------ |
|
||||
| Surveys & Forms | 🟡🟢 | 🟢 |
|
||||
| Heatmaps & Recordings | 🟢 | 🟢 |
|
||||
| Integrations | 🟢 | 🟢 |
|
||||
| Pricing | 🔴 | 🟢 |
|
||||
| Privacy and compliance | 🟢 | 🟢 |
|
||||
| Extensibility | 🟡 | 🟡 |
|
||||
|
||||
### Lucky Orange
|
||||
|
||||
<Image
|
||||
src={LuckyOrange}
|
||||
alt="Lucky Orange is a tool for web analytics and conversion optimization. It helps businesses understand how users behave on their websites."
|
||||
className="w-full rounded-lg"
|
||||
/>
|
||||
|
||||
Lucky Orange is a tool for web analytics and conversion optimization. It helps businesses understand how users behave on their websites. Its features include **surveys**, **session recordings**, **live view,** and **conversion funnels**. These features move beyond vanity metrics, aiming to uncover the reasons behind visitors' actions on your website.
|
||||
|
||||
**Feature depth**:
|
||||
|
||||
- **Surveys:** Like HotJar, Lucky Orange offers survey features, but in a more limited fashion. You can choose from four survey types that suit your needs. They include **multiple-choice**, **like-or-dislike**, **rating**, and **open-ended** surveys. You can also customize how your survey is triggered based on your users’ location on your website and their devices, or if you want a delay before your survey is triggered.
|
||||
- **Heatmaps & Recordings:** Both Lucky Orange and HotJar offer session recordings; however, while this feature is on par with HotJar’s features like filtering, some users still report that the [session viewer crashes when watching a desktop session on the mobile screen](https://www.g2.com/products/lucky-orange/reviews/lucky-orange-review-7862805).
|
||||
- **Integrations**: Lucky Orange offers a smaller but growing selection of integrations compared to HotJar, focusing on essentials like Google Analytics, CMS platforms, and marketing automation tools.
|
||||
|
||||
**Pricing**: Lucky Orange offers pricing plans suitable for businesses of different sizes, including a 7-day free trial; as of the time of writing this article, they begin at $32 per month. Each of these plans is based on sessions but only has 60-day data storage, unlike Hojar, which provides data storage for 365 days.
|
||||
|
||||
**Privacy & Compliance:** Lucky Orange tools, like HotJar, are fully CCPA and GDPR-compliant. This means that it does not store sensitive information, ensuring a secure and trustworthy environment for user data.
|
||||
|
||||
**Extensibility:** Lucky Orange currently works with fewer outside tools than HotJar. This might be a drawback for large companies. However, they're planning to add support for a public API in the future. This means you'll be able to build on top of it.
|
||||
|
||||
| Factors | Lucky Orange | HotJar |
|
||||
| ---------------------- | ------------ | ------ |
|
||||
| Surveys & Forms | 🟢 | 🟢 |
|
||||
| Heatmaps & Recordings | 🟡🟢 | 🟢 |
|
||||
| Integrations | 🟡 | 🟢 |
|
||||
| Pricing | 🟡 | 🟢 |
|
||||
| Privacy and compliance | 🟢 | 🟢 |
|
||||
| Extensibility | 🟡 | 🟢 |
|
||||
|
||||
### FullStory
|
||||
|
||||
<Image
|
||||
src={FullStory}
|
||||
alt="FullStory is a comprehensive user experience analytics platform that enables businesses to gain detailed insights into user interactions with their websites and applications."
|
||||
className="w-full rounded-lg"
|
||||
/>
|
||||
|
||||
FullStory is a comprehensive user experience analytics platform that enables businesses to gain detailed insights into user interactions with their websites and applications. Through features such as session recordings, dynamic heatmaps, and advanced analytics, FullStory provides a nuanced understanding of user behavior.
|
||||
|
||||
Let’s see how FullStory compares to HotJar in terms of the factors we mentioned earlier:
|
||||
|
||||
**Features:**
|
||||
|
||||
- **Surveys:** Both FullStory and HotJar offer survey features, with FullStory utilizing integration with third-party tools like Survicate and SurveyMonkey for versatility and in-depth feedback.
|
||||
|
||||
FullStory wins here if you are looking for a versatile tool to integrate with other existing third-party survey tools. However, if you want an all-in-one solution, HotJar is your go-to solution.
|
||||
|
||||
- **Heatmaps & Recordings:** FullStory's interactive heatmaps and detailed visuals help you better understand how users interact with your website, improving the analysis of page activities. This feature is similar to what HotJar offers.
|
||||
|
||||
However, a notable difference is that in FullStory, you can't save a session to watch later. So, if you find a recording interesting and want to see it again, you'll need to search for it manually when you want to revisit it.
|
||||
|
||||
- **Integrations:** Like HotJar, FullStory integrates seamlessly with various third-party tools, enhancing its versatility and allowing users to integrate it into their existing tech stack.
|
||||
|
||||
**Pricing:** FullStory does not have a free plan, and the price for paid plans is available upon request from the sales team. Although their pricing page states that you get a 14-day free trial for their business plan.
|
||||
|
||||
**Privacy & Compliance:** You are in full control of what data FullStory captures and saves. FullStory is not just GDPR and CCPA-compliant but also holds a SOC 2 Type II attestation and a SOC 3 report.
|
||||
|
||||
**Extensibility:** FullStory also provides several APIs, like HotJar, including a Webhooks API that enables developers to build on top of its functionality and integrate it into their workflows. However, they might not provide the level of control and flexibility required for more complex integrations or custom workflows.
|
||||
|
||||
Here’s how FullStory and HotJar compare side by side:
|
||||
|
||||
| Factors | FullStory | HotJar |
|
||||
| ---------------------- | --------- | ------ |
|
||||
| Surveys & Forms | 🟢 | 🟢 |
|
||||
| Heatmaps & Recordings | 🟢 | 🟢 |
|
||||
| Integrations | 🟢 | 🟢 |
|
||||
| Pricing | 🟡 | 🟢 |
|
||||
| Privacy and compliance | 🟢 | 🟢 |
|
||||
| Extensibility | 🟡 | 🟡 |
|
||||
|
||||
### Mouseflow
|
||||
|
||||
<Image
|
||||
src={MouseFlow}
|
||||
alt="Mouseflow is a web analytics tool designed to provide insights into user behavior on websites."
|
||||
className="w-full rounded-lg"
|
||||
/>
|
||||
|
||||
Mouseflow is a web analytics tool designed to provide insights into user behavior on websites. It offers features such as session recordings, heatmaps, surveys, and funnel analysis to help businesses optimize user experiences and conversions.
|
||||
|
||||
**Feature depth**
|
||||
|
||||
- **Surveys**: Mouseflow provides you with a funnel-like analysis for in-depth form analytics, which is not available on HotJar. With Mouseflow, you can replay sessions from visitors who dropped out or succeeded in completing the form. It also helps you analyze how users interact with each of your form fields.
|
||||
- **Heatmaps & Recordings:** Mouseflow, like Hojar, provides heatmaps and session recordings to visualize user interactions and behaviors, aiding in the analysis of website engagement.
|
||||
|
||||
However, HotJar samples the data you collect daily. That is, you are allowed to review just a small fraction of the whole set of data you receive daily. For example, if you have 3000 recordings per month, you are allowed to record just 100 daily sessions on standard plans.
|
||||
|
||||
- **Integrations:** Mouseflow, like HotJar, integrates with about 58 third-party tools, including other analytics, eCommerce, CMS, and marketing platforms in your stack.
|
||||
|
||||
**Pricing**: Mouseflow's pricing is tiered based on usage and additional features. It offers a range of plans to accommodate businesses of different sizes.
|
||||
|
||||
Its free plan comes with 500 recordings per month, unlimited page views, and a month of storage, all for one website. Paid plans begin at $31/month, however, if you are an enterprise, you can contact their sales team to create a customized plan.
|
||||
|
||||
**Privacy & Compliance:** Mouseflow is committed to data protection. It’s compliant with GDPR, CCPA, CPRA, and VCDPA.
|
||||
|
||||
**Extensibility:** Like HotJar, Mouseflow's API and Webhooks enable developers to build custom integrations, connecting them to virtually any platform or tool imaginable. However, they might not provide the level of control and flexibility required for more complex integrations or custom workflows.
|
||||
|
||||
| Factors | Mouseflow | HotJar |
|
||||
| ---------------------- | --------- | ------ |
|
||||
| Surveys & Forms | 🟢 | 🟡🟢 |
|
||||
| Heatmaps & Recordings | 🟢 | 🟡🟢 |
|
||||
| Integrations | 🟢 | 🟢 |
|
||||
| Pricing | 🟢 | 🟢 |
|
||||
| Privacy and compliance | 🟢 | 🟢 |
|
||||
| Extensibility | 🟡 | 🟡 |
|
||||
|
||||
## So, which option is the better fit for you?
|
||||
|
||||
If you're seeking a comprehensive solution encompassing heatmaps, recordings, surveys, and a strong focus on privacy, MouseFlow emerges as a prime choice.
|
||||
|
||||
On the other hand, if your primary emphasis is on highly targeted surveys, [Formbricks](http://www.formbricks.com/) stands out as the optimal solution.
|
||||
|
||||
What's even more noteworthy is that it is the sole open-source solution for website surveys available. This translates to not just being completely free to use if you self-host, but also offering the freedom for modification due to its extensibility and the liberty to seamlessly integrate with your preferred tools.
|
||||
|
||||
export default ({ children }) => <LayoutMdx meta={meta}>{children}</LayoutMdx>;
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 61 KiB |
@@ -1,7 +1,6 @@
|
||||
import Image from "next/image";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
import Formbricks from "./open-source-survey-software-free-2023-formbricks-typeform-alternative.png";
|
||||
import Typebot from "./typebot-open-source-free-conversational-form-builder-survey-software-opensource.jpg";
|
||||
import LimeSurvey from "./free-survey-tool-limesurvey-open-source-software-opensource.png";
|
||||
|
||||
@@ -2,7 +2,6 @@ import Image from "next/image";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import { Callout } from "@/components/shared/Callout";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
import TweetPeer from "./peer-tweet-typeform-open-source.png";
|
||||
import SnoopForms from "./snoopforms-open-source-typeform-alternative.png";
|
||||
import TwitterResult from "./twitter-results-PMF-cal.png";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Image from "next/image";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
import EmailGIF from "./email-embed.gif";
|
||||
import FigmaMock from "./figma-mock.webp";
|
||||
import PiyushPR from "./pr-merged.webp";
|
||||
|
||||
@@ -6,7 +6,6 @@ import Mail from "./github-accelerator-selection-mail.png";
|
||||
import Teams from "./github-accelerator-2022-teams.png";
|
||||
import NewsletterSignup from "@/components/shared/NewsletterSignup";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
|
||||
export const meta = {
|
||||
title: "Our GitHub Accelerator Experience 👀",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import Image from "next/image";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import TitleImage from "./formbricks-sponsored-by-github-accelerator-2023.webp";
|
||||
import NewsletterSignup from "@/components/shared/NewsletterSignup";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import NewsletterSignup from "@/components/shared/NewsletterSignup";
|
||||
import Image from "next/image";
|
||||
|
||||
import TitleImage from "./formbricks-sponsored-by-github-accelerator-2023.webp";
|
||||
|
||||
export const meta = {
|
||||
title: "Formbricks Joins GitHub Accelerator's Inaugural Cohort 💃",
|
||||
description:
|
||||
@@ -15,14 +16,14 @@ export const meta = {
|
||||
tags: ["GitHub Accelerator", "Open-Source"],
|
||||
};
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" author={"Johannes"}/>
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" author={"Johannes"} />
|
||||
|
||||
_We're getting ready to take our open-source experience management platform to new heights, thanks to being part of the first-ever GitHub Accelerator program!_
|
||||
|
||||
<Image
|
||||
src={TitleImage}
|
||||
alt="GitHub sponsors Formbricks to join their open-source accelerator program"
|
||||
className="rounded-lg w-full"
|
||||
className="w-full rounded-lg"
|
||||
/>
|
||||
|
||||
## Hey there,
|
||||
|
||||
@@ -5,7 +5,7 @@ import HeaderImage from "./create-a-new-survey-with-formbricks.png";
|
||||
import GitpodImage from "./setup-formbricks-via-gitpod.png";
|
||||
import PackagesFolderImage from "./formbricks-packages-folder.png";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
|
||||
export const meta = {
|
||||
title: "Join the FormTribe 🔥",
|
||||
description: "Here is everything you need to know about joining the Formbricks community",
|
||||
|
||||
@@ -5,7 +5,7 @@ import WhyWeDoIt from "./why-we-do-it.png";
|
||||
import EverythinEverywhereAllAtOnce from "./everything_everywhere_all_at_once.png";
|
||||
import ResponsiveEmbed from "react-responsive-embed";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
|
||||
|
||||
export const meta = {
|
||||
title: "Open source forms will save the world.",
|
||||
|
||||
@@ -7,10 +7,9 @@ import Snoopforms from "./snoopforms-how-it-began.png";
|
||||
import FormbricksSneak from "./formbricks-sneak.png";
|
||||
import Wrestling from "./wrestling.jpg";
|
||||
import TypeformValue from "./typeform-value-prop.png";
|
||||
import ResponsiveEmbed from "react-responsive-embed";
|
||||
import { Callout } from "@/components/shared/Callout";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
|
||||
|
||||
export const meta = {
|
||||
title: "Why Qualtrics beats Typeform, especially Open-Source",
|
||||
|
||||
@@ -2,7 +2,7 @@ import Image from "next/image";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import Preseed from "./preseed-header.webp";
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
|
||||
|
||||
export const meta = {
|
||||
title: "We raised Pre-Seed Funding 💸",
|
||||
|
||||
@@ -3,7 +3,7 @@ import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import HeaderImage from "./formbricks-logo-header-open-source-form-infrastructure.svg";
|
||||
import HeroAnimation from "../../../components/shared/HeroAnimation.tsx";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
|
||||
|
||||
export const meta = {
|
||||
title: "snoopForms → Formbricks 🎉",
|
||||
|
||||
@@ -7,7 +7,7 @@ import HeaderImage from "./formbricks-logo.svg";
|
||||
import ProprietaryDependence from "./propietary-dependence.jpeg";
|
||||
import ResponsiveEmbed from "react-responsive-embed";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
|
||||
|
||||
export const meta = {
|
||||
title: "Why Open-Source + No-Code is the Future of Enterprise & Goverment Software",
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
"@storybook/react": "^7.6.7",
|
||||
"@storybook/react-vite": "^7.6.7",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
||||
"@typescript-eslint/parser": "^6.17.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"esbuild": "^0.19.11",
|
||||
"tsup": "^8.0.1",
|
||||
"vite": "^5.0.10"
|
||||
"vite": "^5.0.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,10 @@ EXPOSE 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
USER nextjs
|
||||
|
||||
# Prepare volume for uploads
|
||||
RUN mkdir -p /home/nextjs/apps/web/uploads/
|
||||
VOLUME /home/nextjs/apps/web/uploads/
|
||||
|
||||
CMD supercronic -quiet /app/docker/cronjobs & \
|
||||
if [ "$NEXTAUTH_SECRET" != "RANDOM_STRING" ]; then \
|
||||
pnpm dlx prisma migrate deploy && \
|
||||
|
||||
@@ -84,6 +84,7 @@ export default function ActionClassesTable({
|
||||
<AddNoCodeActionModal
|
||||
environmentId={environmentId}
|
||||
open={isAddActionModalOpen}
|
||||
actionClasses={actionClasses}
|
||||
setOpen={setAddActionModalOpen}
|
||||
isViewer={isViewer}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,8 @@ interface AddNoCodeActionModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
setActionClassArray?;
|
||||
actionClasses: TActionClass[];
|
||||
setActionClasses?;
|
||||
isViewer: boolean;
|
||||
}
|
||||
|
||||
@@ -45,7 +46,8 @@ export default function AddNoCodeActionModal({
|
||||
environmentId,
|
||||
open,
|
||||
setOpen,
|
||||
setActionClassArray,
|
||||
actionClasses,
|
||||
setActionClasses,
|
||||
isViewer,
|
||||
}: AddNoCodeActionModalProps) {
|
||||
const { register, control, handleSubmit, watch, reset } = useForm();
|
||||
@@ -56,6 +58,7 @@ export default function AddNoCodeActionModal({
|
||||
const [testUrl, setTestUrl] = useState("");
|
||||
const [isMatch, setIsMatch] = useState("");
|
||||
const [type, setType] = useState("noCode");
|
||||
const actionClassNames = actionClasses.map((actionClass) => actionClass.name);
|
||||
|
||||
const filterNoCodeConfig = (noCodeConfig: TActionClassNoCodeConfig): TActionClassNoCodeConfig => {
|
||||
const { pageUrl, innerHtml, cssSelector } = noCodeConfig;
|
||||
@@ -92,7 +95,12 @@ export default function AddNoCodeActionModal({
|
||||
throw new Error("You are not authorised to perform this action.");
|
||||
}
|
||||
setIsCreatingAction(true);
|
||||
if (data.name === "") throw new Error("Please give your action a name");
|
||||
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");
|
||||
@@ -119,11 +127,8 @@ export default function AddNoCodeActionModal({
|
||||
}
|
||||
|
||||
const newActionClass: TActionClass = await createActionClassAction(updatedAction);
|
||||
if (setActionClassArray) {
|
||||
setActionClassArray((prevActionClassArray: TActionClass[]) => [
|
||||
...prevActionClassArray,
|
||||
newActionClass,
|
||||
]);
|
||||
if (setActionClasses) {
|
||||
setActionClasses((prevActionClasses: TActionClass[]) => [...prevActionClasses, newActionClass]);
|
||||
}
|
||||
reset();
|
||||
resetAllStates(false);
|
||||
@@ -178,7 +183,7 @@ export default function AddNoCodeActionModal({
|
||||
<div className="grid w-full grid-cols-2 gap-x-4">
|
||||
<div className="col-span-1">
|
||||
<Label>What did your user do?</Label>
|
||||
<Input placeholder="E.g. Clicked Download" {...register("name", { required: true })} />
|
||||
<Input placeholder="E.g. Clicked Download" {...register("name")} />
|
||||
</div>
|
||||
<div className="col-span-1">
|
||||
<Label>Description</Label>
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function HowToAddAttributesButton() {
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
href="http://formbricks.com/docs/attributes/custom-attributes"
|
||||
href="https://formbricks.com/docs/attributes/custom-attributes"
|
||||
target="_blank">
|
||||
<QuestionMarkCircleIcon className="mr-2 h-4 w-4" />
|
||||
How to add attributes
|
||||
|
||||
@@ -148,7 +148,7 @@ export default function Navigation({
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
[environment.id, pathname]
|
||||
[environment.id, pathname, isViewer]
|
||||
);
|
||||
|
||||
const dropdownnavigation = [
|
||||
@@ -312,7 +312,7 @@ export default function Navigation({
|
||||
{/* User Dropdown */}
|
||||
<div className="hidden lg:ml-6 lg:flex lg:items-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<DropdownMenuTrigger asChild id="userDropdownTrigger">
|
||||
<div tabIndex={0} className="flex cursor-pointer flex-row items-center space-x-5">
|
||||
{session.user.imageUrl ? (
|
||||
<Image
|
||||
@@ -335,7 +335,7 @@ export default function Navigation({
|
||||
<ChevronDownIcon className="h-5 w-5 text-slate-700 hover:text-slate-500" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56">
|
||||
<DropdownMenuContent className="w-56" id="userDropdownContentWrapper">
|
||||
<DropdownMenuLabel className="cursor-default break-all">
|
||||
<span className="ph-no-capture font-normal">Signed in as </span>
|
||||
{session?.user?.name && session?.user?.name.length > 30 ? (
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
|
||||
import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/ResponseFilter";
|
||||
import { getTodayDate } from "@/app/lib/surveys/surveys";
|
||||
import { createContext, useContext, useState } from "react";
|
||||
import { createContext, useCallback, useContext, useState } from "react";
|
||||
|
||||
interface FilterValue {
|
||||
questionType: Partial<QuestionOption>;
|
||||
@@ -60,7 +60,7 @@ function ResponseFilterProvider({ children }: { children: React.ReactNode }) {
|
||||
to: getTodayDate(),
|
||||
});
|
||||
|
||||
const resetState = () => {
|
||||
const resetState = useCallback(() => {
|
||||
setDateRange({
|
||||
from: undefined,
|
||||
to: getTodayDate(),
|
||||
@@ -69,7 +69,7 @@ function ResponseFilterProvider({ children }: { children: React.ReactNode }) {
|
||||
filter: [],
|
||||
onlyComplete: false,
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ResponseFilterContext.Provider
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function ResponseTimeline({
|
||||
|
||||
useEffect(() => {
|
||||
setSortedResponses(responsesAscending ? [...responses].reverse() : responses);
|
||||
}, [responsesAscending]);
|
||||
}, [responsesAscending, responses]);
|
||||
|
||||
return (
|
||||
<div className="md:col-span-2">
|
||||
|
||||
@@ -198,7 +198,7 @@ export default function SettingsNavbar({
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
[environmentId, isFormbricksCloud, pathname]
|
||||
[environmentId, isFormbricksCloud, pathname, isPricingDisabled, isViewer]
|
||||
);
|
||||
|
||||
if (!navigation) return null;
|
||||
|
||||
@@ -79,15 +79,21 @@ export default function AddMemberModal({
|
||||
<div className="flex justify-between rounded-lg p-6">
|
||||
<div className="w-full space-y-4">
|
||||
<div>
|
||||
<Label>Full Name</Label>
|
||||
<Label htmlFor="memberNameInput">Full Name</Label>
|
||||
<Input
|
||||
id="memberNameInput"
|
||||
placeholder="e.g. Hans Wurst"
|
||||
{...register("name", { required: true, validate: (value) => value.trim() !== "" })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Email Adress</Label>
|
||||
<Input type="email" placeholder="hans@wurst.com" {...register("email", { required: true })} />
|
||||
<Label htmlFor="memberEmailInput">Email Address</Label>
|
||||
<Input
|
||||
id="memberEmailInput"
|
||||
type="email"
|
||||
placeholder="hans@wurst.com"
|
||||
{...register("email", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
{canDoRoleManagement && <AddMemberRole control={control} />}
|
||||
</div>
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function MemberActions({ team, member, invite, showDeleteButton }
|
||||
return (
|
||||
<>
|
||||
{showDeleteButton && (
|
||||
<button onClick={() => setDeleteMemberModalOpen(true)}>
|
||||
<button id="deleteMemberButton" onClick={() => setDeleteMemberModalOpen(true)}>
|
||||
<TrashIcon className="h-5 w-5 text-slate-700 hover:text-slate-500" />
|
||||
</button>
|
||||
)}
|
||||
@@ -109,7 +109,8 @@ export default function MemberActions({ team, member, invite, showDeleteButton }
|
||||
<button
|
||||
onClick={() => {
|
||||
handleShareInvite();
|
||||
}}>
|
||||
}}
|
||||
id="shareInviteButton">
|
||||
<ShareIcon className="h-5 w-5 text-slate-700 hover:text-slate-500" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
@@ -122,7 +123,8 @@ export default function MemberActions({ team, member, invite, showDeleteButton }
|
||||
<button
|
||||
onClick={() => {
|
||||
handleResendInvite();
|
||||
}}>
|
||||
}}
|
||||
id="resendInviteButton">
|
||||
<PaperAirplaneIcon className="h-5 w-5 text-slate-700 hover:text-slate-500" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -36,10 +36,10 @@ const MembersInfo = async ({
|
||||
const allMembers = [...members, ...invites];
|
||||
|
||||
return (
|
||||
<div className="grid-cols-20">
|
||||
<div className="grid-cols-20" id="membersInfoWrapper">
|
||||
{allMembers.map((member) => (
|
||||
<div
|
||||
className="grid-cols-20 grid h-auto w-full content-center rounded-lg p-0.5 py-2 text-left text-sm text-slate-900"
|
||||
className="singleMemberInfo grid-cols-20 grid h-auto w-full content-center rounded-lg p-0.5 py-2 text-left text-sm text-slate-900"
|
||||
key={member.email}>
|
||||
<div className="h-58 col-span-2 pl-4">
|
||||
{isInvitee(member) ? (
|
||||
|
||||
@@ -43,7 +43,8 @@ export default function ShareInviteModal({ inviteToken, open, setOpen }: ShareIn
|
||||
<p
|
||||
ref={linkTextRef}
|
||||
className="relative mt-3 w-full truncate rounded-lg border border-slate-300 bg-slate-50 p-3 text-center text-slate-800"
|
||||
onClick={() => handleTextSelection()}>
|
||||
onClick={() => handleTextSelection()}
|
||||
id="inviteLinkText">
|
||||
{`${window.location.protocol}//${window.location.host}/invite?token=${inviteToken}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import CodeBlock from "@formbricks/ui/CodeBlock";
|
||||
|
||||
export default function SetupInstructions({ environmentId }: { environmentId: string }) {
|
||||
return (
|
||||
<div className="prose prose-slate -mt-3">
|
||||
<CodeBlock language="js">{environmentId}</CodeBlock>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import jsPackageJson from "@/../../packages/js/package.json";
|
||||
import packageJson from "@/package.json";
|
||||
import Link from "next/link";
|
||||
import "prismjs/themes/prism.css";
|
||||
@@ -109,7 +110,7 @@ if (typeof window !== "undefined") {
|
||||
</p>
|
||||
<CodeBlock language="js">{`<!-- START Formbricks Surveys -->
|
||||
<script type="text/javascript">
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^1.4.0/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: "${environmentId}", apiHost: "${window.location.protocol}//${window.location.host}"})},500)}();
|
||||
!function(){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://unpkg.com/@formbricks/js@^${jsPackageJson.version}/dist/index.umd.js";var e=document.getElementsByTagName("script")[0];e.parentNode.insertBefore(t,e),setTimeout(function(){window.formbricks.init({environmentId: "${environmentId}", apiHost: "${window.location.protocol}//${window.location.host}"})},500)}();
|
||||
</script>
|
||||
<!-- END Formbricks Surveys -->`}</CodeBlock>
|
||||
<p className="text-lg font-semibold text-slate-800">You're done 🎉</p>
|
||||
|
||||
@@ -5,6 +5,7 @@ import EnvironmentNotice from "@formbricks/ui/EnvironmentNotice";
|
||||
|
||||
import SettingsCard from "../components/SettingsCard";
|
||||
import SettingsTitle from "../components/SettingsTitle";
|
||||
import EnvironmentIdField from "./components/EnvironmentIdField";
|
||||
import SetupInstructions from "./components/SetupInstructions";
|
||||
|
||||
export default async function ProfileSettingsPage({ params }) {
|
||||
@@ -18,7 +19,11 @@ export default async function ProfileSettingsPage({ params }) {
|
||||
description="Check if the Formbricks widget is alive and kicking.">
|
||||
<WidgetStatusIndicator environmentId={params.environmentId} type="large" />
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard
|
||||
title="Your EnvironmentId"
|
||||
description="This Id uniquely identifies this Formbricks environment.">
|
||||
<EnvironmentIdField environmentId={params.environmentId} />
|
||||
</SettingsCard>
|
||||
<SettingsCard
|
||||
title="How to setup"
|
||||
description="Follow these steps to setup the Formbricks widget within your app"
|
||||
|
||||
@@ -1,7 +1,30 @@
|
||||
"use server";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getResponses } from "@formbricks/lib/response/service";
|
||||
import { canUserAccessSurvey } from "@formbricks/lib/survey/auth";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
|
||||
export default async function revalidateSurveyIdPath(environmentId: string, surveyId: string) {
|
||||
revalidatePath(`/environments/${environmentId}/surveys/${surveyId}`);
|
||||
}
|
||||
|
||||
export async function getMoreResponses(
|
||||
surveyId: string,
|
||||
page: number,
|
||||
batchSize?: number
|
||||
): Promise<TResponse[]> {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) throw new AuthorizationError("Not authorized");
|
||||
|
||||
const isAuthorized = await canUserAccessSurvey(session.user.id, surveyId);
|
||||
if (!isAuthorized) throw new AuthorizationError("Not authorized");
|
||||
|
||||
batchSize = batchSize ?? 10;
|
||||
const responses = await getResponses(surveyId, page, batchSize);
|
||||
return responses;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data";
|
||||
import { Metadata } from "next";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
|
||||
type Props = {
|
||||
@@ -14,7 +14,7 @@ export const generateMetadata = async ({ params }: Props): Promise<Metadata> =>
|
||||
const survey = await getSurvey(params.surveyId);
|
||||
|
||||
if (session) {
|
||||
const { responseCount } = await getAnalysisData(params.surveyId, params.environmentId);
|
||||
const responseCount = await getResponseCountBySurveyId(params.surveyId);
|
||||
return {
|
||||
title: `${responseCount} Responses | ${survey?.name} Results`,
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ const ResponsePage = ({
|
||||
if (!searchParams?.get("referer")) {
|
||||
resetState();
|
||||
}
|
||||
}, [searchParams]);
|
||||
}, [searchParams, resetState]);
|
||||
|
||||
// get the filtered array when the selected filter value changes
|
||||
const filterResponses: TResponse[] = useMemo(() => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { getMoreResponses } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
@@ -29,42 +30,48 @@ export default function ResponseTimeline({
|
||||
environmentTags,
|
||||
responsesPerPage,
|
||||
}: ResponseTimelineProps) {
|
||||
const [displayedResponses, setDisplayedResponses] = useState<TResponse[]>([]);
|
||||
const loadingRef = useRef(null);
|
||||
const [fetchedResponses, setFetchedResponses] = useState<TResponse[]>(responses);
|
||||
const [page, setPage] = useState(2);
|
||||
const [hasMoreResponses, setHasMoreResponses] = useState<boolean>(responses.length > 0);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayedResponses(responses.slice(0, responsesPerPage));
|
||||
}, [responses]);
|
||||
const currentLoadingRef = loadingRef.current;
|
||||
|
||||
useEffect(() => {
|
||||
const loadResponses = async () => {
|
||||
const newResponses = await getMoreResponses(survey.id, page);
|
||||
if (newResponses.length === 0) {
|
||||
setHasMoreResponses(false);
|
||||
} else {
|
||||
setPage(page + 1);
|
||||
}
|
||||
setFetchedResponses((prevResponses) => [...prevResponses, ...newResponses]);
|
||||
};
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
setDisplayedResponses((prevResponses) => [
|
||||
...prevResponses,
|
||||
...responses.slice(prevResponses.length, prevResponses.length + responsesPerPage),
|
||||
]);
|
||||
if (hasMoreResponses) loadResponses();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.8 }
|
||||
);
|
||||
|
||||
if (loadingRef.current) {
|
||||
observer.observe(loadingRef.current);
|
||||
if (currentLoadingRef) {
|
||||
observer.observe(currentLoadingRef);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (loadingRef.current) {
|
||||
observer.unobserve(loadingRef.current);
|
||||
if (currentLoadingRef) {
|
||||
observer.unobserve(currentLoadingRef);
|
||||
}
|
||||
};
|
||||
}, [responses]);
|
||||
}, [responses, responsesPerPage, page, survey.id, fetchedResponses.length, hasMoreResponses]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{survey.type === "web" && displayedResponses.length === 0 && !environment.widgetSetupCompleted ? (
|
||||
{survey.type === "web" && fetchedResponses.length === 0 && !environment.widgetSetupCompleted ? (
|
||||
<EmptyInAppSurveys environment={environment} />
|
||||
) : displayedResponses.length === 0 ? (
|
||||
) : fetchedResponses.length === 0 ? (
|
||||
<EmptySpaceFiller
|
||||
type="response"
|
||||
environment={environment}
|
||||
@@ -72,7 +79,7 @@ export default function ResponseTimeline({
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
{displayedResponses.map((response) => {
|
||||
{fetchedResponses.map((response) => {
|
||||
return (
|
||||
<div key={response.id}>
|
||||
<SingleResponseCard
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getAnalysisData } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/data";
|
||||
import ResponsePage from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage";
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
@@ -7,6 +6,8 @@ import { RESPONSES_PER_PAGE, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getMembershipByUserIdTeamId } from "@formbricks/lib/membership/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getResponses } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTagsByEnvironmentId } from "@formbricks/lib/tag/service";
|
||||
import { getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
@@ -16,13 +17,18 @@ export default async function Page({ params }) {
|
||||
if (!session) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
const [{ responses, survey }, environment] = await Promise.all([
|
||||
getAnalysisData(params.surveyId, params.environmentId),
|
||||
const [responses, survey, environment] = await Promise.all([
|
||||
getResponses(params.surveyId, 1),
|
||||
getSurvey(params.surveyId),
|
||||
getEnvironment(params.environmentId),
|
||||
]);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
if (!survey) {
|
||||
throw new Error("Survey not found");
|
||||
}
|
||||
const product = await getProductByEnvironmentId(environment.id);
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
|
||||
@@ -5,6 +5,7 @@ import { DownloadIcon, FileIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { getPersonIdentifier } from "@formbricks/lib/person/util";
|
||||
import { getOriginalFileNameFromUrl } from "@formbricks/lib/storage/utils";
|
||||
import { timeSince } from "@formbricks/lib/time";
|
||||
import type { TSurveyQuestionSummary } from "@formbricks/types/surveys";
|
||||
import { TSurveyFileUploadQuestion } from "@formbricks/types/surveys";
|
||||
@@ -81,29 +82,31 @@ export default function FileUploadSummary({ questionSummary, environmentId }: Fi
|
||||
|
||||
{Array.isArray(response.value) &&
|
||||
(response.value.length > 0 ? (
|
||||
response.value.map((fileUrl, index) => (
|
||||
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
|
||||
<a
|
||||
href={fileUrl as string}
|
||||
key={index}
|
||||
download
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
|
||||
<DownloadIcon className="h-6 text-slate-500" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
response.value.map((fileUrl, index) => {
|
||||
const fileName = getOriginalFileNameFromUrl(fileUrl);
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
{fileUrl.split("/").pop()}
|
||||
</p>
|
||||
return (
|
||||
<div className="relative m-2 rounded-lg bg-slate-200" key={fileUrl}>
|
||||
<a
|
||||
href={fileUrl as string}
|
||||
key={index}
|
||||
download={fileName}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-slate-50 hover:bg-white">
|
||||
<DownloadIcon className="h-6 text-slate-500" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="flex flex-col items-center justify-center p-2">
|
||||
<FileIcon className="h-6 text-slate-500" />
|
||||
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">{fileName}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="flex w-full flex-col items-center justify-center p-2">
|
||||
<p className="mt-2 text-sm font-semibold text-slate-500 dark:text-slate-400">skipped</p>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function ShareEmbedSurvey({
|
||||
product,
|
||||
user,
|
||||
}: ShareEmbedSurveyProps) {
|
||||
const surveyUrl = useMemo(() => webAppUrl + "/s/" + survey.id, [survey]);
|
||||
const surveyUrl = useMemo(() => webAppUrl + "/s/" + survey.id, [survey, webAppUrl]);
|
||||
const isSingleUseLinkSurvey = survey.singleUse?.enabled;
|
||||
const { email } = user;
|
||||
const { brandColor } = product;
|
||||
|
||||
@@ -150,12 +150,13 @@ export default function SummaryDropOffs({ responses, survey, displayCount }: Sum
|
||||
viewsCount: viewsArr,
|
||||
dropoffPercentage: dropoffPercentageArr,
|
||||
};
|
||||
}, [responses, survey.questions, displayCount, initialAvgTtc, avgTtc]);
|
||||
}, [responses, survey.questions, displayCount, initialAvgTtc, avgTtc, survey.welcomeCard.enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
const { newAvgTtc, dropoffCount, viewsCount, dropoffPercentage } = calculateMetrics();
|
||||
setAvgTtc(newAvgTtc);
|
||||
setDropoffMetrics({ dropoffCount, viewsCount, dropoffPercentage });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [responses]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -55,7 +55,7 @@ const SummaryPage = ({
|
||||
if (!searchParams?.get("referer")) {
|
||||
resetState();
|
||||
}
|
||||
}, [searchParams]);
|
||||
}, [searchParams, resetState]);
|
||||
|
||||
// get the filtered array when the selected filter value changes
|
||||
const filterResponses: TResponse[] = useMemo(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
DateRange,
|
||||
useResponseFilter,
|
||||
} from "@/app/(app)/environments/[environmentId]/components/ResponseFilterContext";
|
||||
import { getMoreResponses } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { fetchFile } from "@/app/lib/fetchFile";
|
||||
import { generateQuestionAndFilterOptions, getTodayDate } from "@/app/lib/surveys/surveys";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
@@ -141,7 +142,7 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
|
||||
return "my_survey_responses";
|
||||
}, [survey]);
|
||||
|
||||
function extracMetadataKeys(obj, parentKey = "") {
|
||||
const extracMetadataKeys = useCallback((obj, parentKey = "") => {
|
||||
let keys: string[] = [];
|
||||
|
||||
for (let key in obj) {
|
||||
@@ -153,11 +154,25 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getAllResponsesInBatches = useCallback(async () => {
|
||||
const BATCH_SIZE = 3000;
|
||||
const responses: TResponse[] = [];
|
||||
for (let page = 1; ; page++) {
|
||||
const batchResponses = await getMoreResponses(survey.id, page, BATCH_SIZE);
|
||||
responses.push(...batchResponses);
|
||||
if (batchResponses.length < BATCH_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return responses;
|
||||
}, [survey.id]);
|
||||
|
||||
const downloadResponses = useCallback(
|
||||
async (filter: FilterDownload, filetype: "csv" | "xlsx") => {
|
||||
const downloadResponse = filter === FilterDownload.ALL ? totalResponses : responses;
|
||||
const downloadResponse = filter === FilterDownload.ALL ? await getAllResponsesInBatches() : responses;
|
||||
|
||||
const questionNames = survey.questions?.map((question) => question.headline);
|
||||
const hiddenFieldIds = survey.hiddenFields.fieldIds;
|
||||
const hiddenFieldResponse = {};
|
||||
@@ -269,7 +284,7 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
|
||||
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
},
|
||||
[downloadFileName, responses, totalResponses, survey]
|
||||
[downloadFileName, responses, survey, extracMetadataKeys, getAllResponsesInBatches]
|
||||
);
|
||||
|
||||
const handleDateHoveredChange = (date: Date) => {
|
||||
|
||||
@@ -64,6 +64,7 @@ const ResponseFilter = () => {
|
||||
if (!isOpen) {
|
||||
clearItem();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen]);
|
||||
|
||||
const handleAddNewFilter = () => {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import QuestionFormInput from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionFormInput";
|
||||
import { PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TSurveyCalQuestion } from "@formbricks/types/surveys";
|
||||
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
|
||||
interface CalQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
question: TSurveyCalQuestion;
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
@@ -15,28 +17,24 @@ interface CalQuestionFormProps {
|
||||
}
|
||||
|
||||
export default function CalQuestionForm({
|
||||
localSurvey,
|
||||
question,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
isInValid,
|
||||
}: CalQuestionFormProps): JSX.Element {
|
||||
const [showSubheader, setShowSubheader] = useState(!!question.subheader);
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })}
|
||||
isInvalid={isInValid && question.headline.trim() === ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<QuestionFormInput
|
||||
environmentId={environmentId}
|
||||
isInValid={isInValid}
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
/>
|
||||
<div className="mt-3">
|
||||
{showSubheader && (
|
||||
<>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { md } from "@formbricks/lib/markdownIt";
|
||||
@@ -48,6 +48,9 @@ export default function EditWelcomeCard({
|
||||
},
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
setFirstRender(true);
|
||||
}, [activeQuestionId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import QuestionFormInput from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionFormInput";
|
||||
import { PlusIcon, TrashIcon, XCircleIcon } from "@heroicons/react/24/solid";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "react-hot-toast";
|
||||
@@ -24,6 +25,7 @@ interface FileUploadFormProps {
|
||||
}
|
||||
|
||||
export default function FileUploadQuestionForm({
|
||||
localSurvey,
|
||||
question,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
@@ -102,21 +104,17 @@ export default function FileUploadQuestionForm({
|
||||
return 10;
|
||||
}, [billingInfo, billingInfoError, billingInfoLoading]);
|
||||
|
||||
const environmentId = localSurvey.environmentId;
|
||||
|
||||
return (
|
||||
<form>
|
||||
<div className="mt-3">
|
||||
<Label htmlFor="headline">Question</Label>
|
||||
<div className="mt-2">
|
||||
<Input
|
||||
autoFocus
|
||||
id="headline"
|
||||
name="headline"
|
||||
value={question.headline}
|
||||
onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })}
|
||||
isInvalid={isInValid && question.headline.trim() === ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<QuestionFormInput
|
||||
environmentId={environmentId}
|
||||
isInValid={isInValid}
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
/>
|
||||
<div className="mt-3">
|
||||
{showSubheader && (
|
||||
<>
|
||||
|
||||
@@ -101,6 +101,7 @@ export default function QuestionCard({
|
||||
|
||||
const updateEmptyNextButtonLabels = (labelValue: string) => {
|
||||
localSurvey.questions.forEach((q, index) => {
|
||||
if (index === localSurvey.questions.length - 1) return;
|
||||
if (!q.buttonLabel || q.buttonLabel?.trim() === "") {
|
||||
updateQuestion(index, { buttonLabel: labelValue });
|
||||
}
|
||||
@@ -282,6 +283,7 @@ export default function QuestionCard({
|
||||
/>
|
||||
) : question.type === TSurveyQuestionType.Cal ? (
|
||||
<CalQuestionForm
|
||||
localSurvey={localSurvey}
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
updateQuestion={updateQuestion}
|
||||
@@ -318,6 +320,8 @@ export default function QuestionCard({
|
||||
updateQuestion(questionIdx, { buttonLabel: e.target.value });
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
//If it is the last question then do not update labels
|
||||
if (questionIdx === localSurvey.questions.length - 1) return;
|
||||
updateEmptyNextButtonLabels(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function QuestionsView({
|
||||
acc[question.id] = createId();
|
||||
return acc;
|
||||
}, {});
|
||||
}, []);
|
||||
}, [localSurvey.questions]);
|
||||
|
||||
const [backButtonLabel, setbackButtonLabel] = useState(null);
|
||||
|
||||
|
||||
@@ -248,7 +248,13 @@ export default function ResponseOptionsCard({
|
||||
setCloseOnDate(localSurvey.closeOnDate);
|
||||
setSurveyCloseOnDateToggle(true);
|
||||
}
|
||||
}, [localSurvey]);
|
||||
}, [
|
||||
localSurvey,
|
||||
singleUseMessage.heading,
|
||||
singleUseMessage.subheading,
|
||||
surveyClosedMessage.heading,
|
||||
surveyClosedMessage.subheading,
|
||||
]);
|
||||
|
||||
const handleCheckMark = () => {
|
||||
if (autoComplete) {
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function SettingsView({
|
||||
localSurvey={localSurvey}
|
||||
setLocalSurvey={setLocalSurvey}
|
||||
environmentId={environment.id}
|
||||
actionClasses={actionClasses}
|
||||
propActionClasses={actionClasses}
|
||||
membershipRole={membershipRole}
|
||||
/>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ interface StylingCardProps {
|
||||
|
||||
export default function StylingCard({ localSurvey, setLocalSurvey, colours }: StylingCardProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const progressBarHidden = localSurvey.styling?.hideProgressBar ?? false;
|
||||
const { type, productOverwrites, styling } = localSurvey;
|
||||
const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } =
|
||||
productOverwrites ?? {};
|
||||
@@ -175,6 +175,16 @@ export default function StylingCard({ localSurvey, setLocalSurvey, colours }: St
|
||||
});
|
||||
};
|
||||
|
||||
const toggleProgressBarVisibility = () => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
styling: {
|
||||
...localSurvey.styling,
|
||||
hideProgressBar: !progressBarHidden,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -342,6 +352,23 @@ export default function StylingCard({ localSurvey, setLocalSurvey, colours }: St
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="p-3">
|
||||
<div className="ml-2 flex items-center space-x-1">
|
||||
<Switch
|
||||
id="hideProgressBar"
|
||||
checked={progressBarHidden}
|
||||
onCheckedChange={toggleProgressBarVisibility}
|
||||
/>
|
||||
<Label htmlFor="hideProgressBar" className="cursor-pointer">
|
||||
<div className="ml-2">
|
||||
<h3 className="text-sm font-semibold text-slate-700">Hide Progress Bar</h3>
|
||||
<p className="text-xs font-normal text-slate-500">
|
||||
Disable the visibility of survey progress
|
||||
</p>
|
||||
</div>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.CollapsibleContent>
|
||||
</Collapsible.Root>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function SurveyEditor({
|
||||
setActiveQuestionId(survey.questions[0].id);
|
||||
}
|
||||
}
|
||||
}, [survey]);
|
||||
}, [survey, localSurvey]);
|
||||
|
||||
// when the survey type changes, we need to reset the active question id to the first question
|
||||
useEffect(() => {
|
||||
|
||||
@@ -9,8 +9,7 @@ import toast from "react-hot-toast";
|
||||
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { TSurvey, TSurveyQuestionType } from "@formbricks/types/surveys";
|
||||
import AlertDialog from "@formbricks/ui/AlertDialog";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { DeleteDialog } from "@formbricks/ui/DeleteDialog";
|
||||
@@ -172,19 +171,21 @@ export default function SurveyMenuBar({
|
||||
|
||||
if (validFields < 2) {
|
||||
setInvalidQuestions([question.id]);
|
||||
toast.error("Incomplete logic jumps detected: Please fill or delete them.");
|
||||
toast.error("Incomplete logic jumps detected: Fill or remove them in the Questions tab.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (question.required && logic.condition === "skipped") {
|
||||
toast.error("You have a missing logic condition. Please update or delete it.");
|
||||
toast.error("A logic condition is missing: Please update or delete it in the Questions tab.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const thisLogic = `${logic.condition}-${logic.value}`;
|
||||
if (existingLogicConditions.has(thisLogic)) {
|
||||
setInvalidQuestions([question.id]);
|
||||
toast.error("You have 2 competing logic conditons. Please update or delete one.");
|
||||
toast.error(
|
||||
"There are two competing logic conditons: Please update or delete one in the Questions tab."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
existingLogicConditions.add(thisLogic);
|
||||
|
||||
@@ -27,7 +27,7 @@ interface WhenToSendCardProps {
|
||||
localSurvey: TSurvey;
|
||||
setLocalSurvey: (survey: TSurvey) => void;
|
||||
environmentId: string;
|
||||
actionClasses: TActionClass[];
|
||||
propActionClasses: TActionClass[];
|
||||
membershipRole?: TMembershipRole;
|
||||
}
|
||||
|
||||
@@ -35,13 +35,13 @@ export default function WhenToSendCard({
|
||||
environmentId,
|
||||
localSurvey,
|
||||
setLocalSurvey,
|
||||
actionClasses,
|
||||
propActionClasses,
|
||||
membershipRole,
|
||||
}: WhenToSendCardProps) {
|
||||
const [open, setOpen] = useState(localSurvey.type === "web" ? true : false);
|
||||
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
|
||||
const [activeIndex, setActiveIndex] = useState<number | null>(null);
|
||||
const [actionClassArray, setActionClassArray] = useState<TActionClass[]>(actionClasses);
|
||||
const [actionClasses, setActionClasses] = useState<TActionClass[]>(propActionClasses);
|
||||
const { isViewer } = getAccessFlags(membershipRole);
|
||||
|
||||
const autoClose = localSurvey.autoClose !== null;
|
||||
@@ -55,7 +55,7 @@ export default function WhenToSendCard({
|
||||
const setTriggerEvent = useCallback(
|
||||
(idx: number, actionClassName: string) => {
|
||||
const updatedSurvey = { ...localSurvey };
|
||||
const newActionClass = actionClassArray!.find((actionClass) => {
|
||||
const newActionClass = actionClasses!.find((actionClass) => {
|
||||
return actionClass.name === actionClassName;
|
||||
});
|
||||
if (!newActionClass) {
|
||||
@@ -64,7 +64,7 @@ export default function WhenToSendCard({
|
||||
updatedSurvey.triggers[idx] = newActionClass.name;
|
||||
setLocalSurvey(updatedSurvey);
|
||||
},
|
||||
[actionClassArray, localSurvey, setLocalSurvey]
|
||||
[actionClasses, localSurvey, setLocalSurvey]
|
||||
);
|
||||
|
||||
const removeTriggerEvent = (idx: number) => {
|
||||
@@ -101,7 +101,7 @@ export default function WhenToSendCard({
|
||||
useEffect(() => {
|
||||
if (isAddEventModalOpen) return;
|
||||
if (activeIndex !== null) {
|
||||
const newActionClass = actionClassArray[actionClassArray.length - 1].name;
|
||||
const newActionClass = actionClasses[actionClasses.length - 1].name;
|
||||
const currentActionClass = localSurvey.triggers[activeIndex];
|
||||
|
||||
if (newActionClass !== currentActionClass) {
|
||||
@@ -110,7 +110,7 @@ export default function WhenToSendCard({
|
||||
|
||||
setActiveIndex(null);
|
||||
}
|
||||
}, [actionClassArray, activeIndex, setTriggerEvent]);
|
||||
}, [actionClasses, activeIndex, setTriggerEvent, isAddEventModalOpen, localSurvey.triggers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (localSurvey.type === "link") {
|
||||
@@ -200,7 +200,7 @@ export default function WhenToSendCard({
|
||||
Add Action
|
||||
</button>
|
||||
<SelectSeparator />
|
||||
{actionClassArray.map((actionClass) => (
|
||||
{actionClasses.map((actionClass) => (
|
||||
<SelectItem
|
||||
value={actionClass.name}
|
||||
key={actionClass.name}
|
||||
@@ -279,7 +279,8 @@ export default function WhenToSendCard({
|
||||
environmentId={environmentId}
|
||||
open={isAddEventModalOpen}
|
||||
setOpen={setAddEventModalOpen}
|
||||
setActionClassArray={setActionClassArray}
|
||||
actionClasses={actionClasses}
|
||||
setActionClasses={setActionClasses}
|
||||
isViewer={isViewer}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -20,34 +20,42 @@ export default function Modal({
|
||||
const [show, setShow] = useState(false);
|
||||
const modalRef = useRef<HTMLDivElement | null>(null);
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
const calculateScaling = () => {
|
||||
const scaleValue = (() => {
|
||||
if (windowWidth > 1600) return "1";
|
||||
else if (windowWidth > 1200) return ".9";
|
||||
else if (windowWidth > 900) return ".8";
|
||||
return "0.7";
|
||||
})();
|
||||
|
||||
const getPlacementClass = (() => {
|
||||
switch (placement) {
|
||||
case "bottomLeft":
|
||||
return "bottom left";
|
||||
case "bottomRight":
|
||||
return "bottom right";
|
||||
case "topLeft":
|
||||
return "top left";
|
||||
case "topRight":
|
||||
return "top right";
|
||||
default:
|
||||
return "";
|
||||
const calculateScaling = () => {
|
||||
let scaleValue = "1";
|
||||
|
||||
if (previewMode === "mobile") {
|
||||
scaleValue = "1";
|
||||
} else {
|
||||
if (windowWidth > 1600) {
|
||||
scaleValue = "1";
|
||||
} else if (windowWidth > 1200) {
|
||||
scaleValue = ".9";
|
||||
} else if (windowWidth > 900) {
|
||||
scaleValue = ".8";
|
||||
} else {
|
||||
scaleValue = "0.7";
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
let placementClass = "";
|
||||
|
||||
if (placement === "bottomLeft") {
|
||||
placementClass = "bottom left";
|
||||
} else if (placement === "bottomRight") {
|
||||
placementClass = "bottom right";
|
||||
} else if (placement === "topLeft") {
|
||||
placementClass = "top left";
|
||||
} else if (placement === "topRight") {
|
||||
placementClass = "top right";
|
||||
}
|
||||
|
||||
return {
|
||||
transform: `scale(${scaleValue})`,
|
||||
"transform-origin": getPlacementClass,
|
||||
"transform-origin": placementClass,
|
||||
};
|
||||
};
|
||||
|
||||
const scalingClasses = calculateScaling();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -91,7 +99,7 @@ export default function Modal({
|
||||
: "";
|
||||
|
||||
return (
|
||||
<div aria-live="assertive" className="relative h-full w-full overflow-visible bg-slate-300">
|
||||
<div aria-live="assertive" className="relative h-full w-full bg-slate-300">
|
||||
<div
|
||||
ref={modalRef}
|
||||
style={{ ...highlightBorderColorStyle, ...scalingClasses }}
|
||||
|
||||
@@ -156,20 +156,6 @@ export default function PreviewSurvey({
|
||||
setActiveQuestionId(survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id);
|
||||
}
|
||||
|
||||
function animationTrigger() {
|
||||
let storePreviewMode = previewMode;
|
||||
setPreviewMode("null");
|
||||
setTimeout(() => {
|
||||
setPreviewMode(storePreviewMode);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (survey.styling?.background?.bgType === "animation") {
|
||||
animationTrigger();
|
||||
}
|
||||
}, [survey.styling?.background?.bg]);
|
||||
|
||||
useEffect(() => {
|
||||
if (environment && environment.widgetSetupCompleted) {
|
||||
setWidgetSetupCompleted(true);
|
||||
@@ -300,7 +286,7 @@ export default function PreviewSurvey({
|
||||
</Modal>
|
||||
) : (
|
||||
<MediaBackground survey={survey} ContentRef={ContentRef} isEditorView>
|
||||
<div className="z-0 w-full max-w-md rounded-lg p-4">
|
||||
<div className="z-0 w-full max-w-md rounded-lg p-4">
|
||||
<SurveyInline
|
||||
survey={survey}
|
||||
brandColor={brandColor}
|
||||
|
||||
@@ -35,7 +35,7 @@ const Greeting: React.FC<Greeting> = ({ next, skip, name, session }) => {
|
||||
button.removeEventListener("keydown", handleKeyDown);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
}, [next]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full max-w-xl flex-col justify-around gap-8 px-8">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { PresentationChartLineIcon, InboxStackIcon } from "@heroicons/react/24/solid";
|
||||
import Link from "next/link";
|
||||
import revalidateSurveyIdPath from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/actions";
|
||||
import { InboxStackIcon, PresentationChartLineIcon } from "@heroicons/react/24/solid";
|
||||
import Link from "next/link";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
|
||||
interface SurveyResultsTabProps {
|
||||
activeId: string;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use server";
|
||||
import { createTag } from "@formbricks/lib/tag/service";
|
||||
import { addTagToRespone, deleteTagOnResponse } from "@formbricks/lib/tagOnResponse/service";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth";
|
||||
import { createTag } from "@formbricks/lib/tag/service";
|
||||
import { canUserAccessTagOnResponse } from "@formbricks/lib/tagOnResponse/auth";
|
||||
import { addTagToRespone, deleteTagOnResponse } from "@formbricks/lib/tagOnResponse/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
|
||||
export const createTagAction = async (environmentId: string, tagName: string) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
@@ -46,7 +46,7 @@ const ResponsePage = ({
|
||||
if (!searchParams?.get("referer")) {
|
||||
resetState();
|
||||
}
|
||||
}, [searchParams]);
|
||||
}, [searchParams, resetState]);
|
||||
|
||||
// get the filtered array when the selected filter value changes
|
||||
const filterResponses: TResponse[] = useMemo(() => {
|
||||
|
||||
@@ -31,9 +31,10 @@ export default function ResponseTimeline({
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayedResponses(responses.slice(0, responsesPerPage));
|
||||
}, [responses]);
|
||||
}, [responses, responsesPerPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentLoadingRef = loadingRef.current;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
@@ -46,16 +47,16 @@ export default function ResponseTimeline({
|
||||
{ threshold: 0.8 }
|
||||
);
|
||||
|
||||
if (loadingRef.current) {
|
||||
observer.observe(loadingRef.current);
|
||||
if (currentLoadingRef) {
|
||||
observer.observe(currentLoadingRef);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (loadingRef.current) {
|
||||
observer.unobserve(loadingRef.current);
|
||||
if (currentLoadingRef) {
|
||||
observer.unobserve(currentLoadingRef);
|
||||
}
|
||||
};
|
||||
}, [responses]);
|
||||
}, [responses, responsesPerPage]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -49,7 +49,7 @@ const SummaryPage = ({
|
||||
if (!searchParams?.get("referer")) {
|
||||
resetState();
|
||||
}
|
||||
}, [searchParams]);
|
||||
}, [searchParams, resetState]);
|
||||
|
||||
// get the filtered array when the selected filter value changes
|
||||
const filterResponses: TResponse[] = useMemo(() => {
|
||||
|
||||
@@ -140,7 +140,7 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
|
||||
return "my_survey_responses";
|
||||
}, [survey]);
|
||||
|
||||
function extracMetadataKeys(obj, parentKey = "") {
|
||||
const extracMetadataKeys = useCallback((obj, parentKey = "") => {
|
||||
let keys: string[] = [];
|
||||
|
||||
for (let key in obj) {
|
||||
@@ -152,7 +152,7 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const downloadResponses = useCallback(
|
||||
async (filter: FilterDownload, filetype: "csv" | "xlsx") => {
|
||||
@@ -268,7 +268,7 @@ const CustomFilter = ({ environmentTags, responses, survey, totalResponses }: Cu
|
||||
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
},
|
||||
[downloadFileName, responses, totalResponses, survey]
|
||||
[downloadFileName, responses, totalResponses, survey, extracMetadataKeys]
|
||||
);
|
||||
|
||||
const handleDateHoveredChange = (date: Date) => {
|
||||
|
||||