Compare commits

..

43 Commits

Author SHA1 Message Date
Shubham Palriwala
9d1cb8f595 fix: (docs) mention google drive api for sheets integration in self host (#1905) 2024-01-16 12:50:24 +00:00
Dhruwang Jariwala
81e9ac0e12 fix: next button tweak (#1903) 2024-01-16 09:34:44 +00:00
Shubham Palriwala
be4534da2d fix: formbricks docs survey endpoint (#1901) 2024-01-16 07:14:58 +00:00
Shubham Palriwala
f4a31ad563 feat: unit tests for survey service (#1813)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-01-16 05:41:16 +00:00
Dhruwang Jariwala
82302360fa test: unit test for display services (#1832)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-01-15 16:21:33 +00:00
Shubham Palriwala
f6f45d74d5 feat: (codespaces) run formbricks app on load (#1871) 2024-01-15 16:13:54 +00:00
Anshuman Pandey
04a47b3d0a fix: adds vite dev mode (#1893) 2024-01-15 14:06:06 +00:00
Matti Nannt
cc64d7dfe9 chore: prepare 1.4.2 release (#1899) 2024-01-13 09:17:21 +00:00
Shubham Palriwala
0c30dfbcf3 feat: docs for source tracking (#1887)
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-01-12 18:57:52 +00:00
Anshuman Pandey
3e9f61792f fix: fixes duplicate named file uploads (#1888) 2024-01-12 16:50:16 +00:00
Shubham Palriwala
ad63be3005 feat: add country location to response metadata (#1892) 2024-01-12 16:43:53 +00:00
Dhruwang Jariwala
1635297226 fix: warning for duplicate action name (#1897) 2024-01-12 15:16:03 +00:00
Dhruwang Jariwala
0ff7bb56ec fix: download response (#1890) 2024-01-11 19:41:22 +00:00
Dhruwang Jariwala
f3666b8745 fix: added missing functionality for image upload in file upload and cal question (#1886) 2024-01-11 17:37:14 +00:00
Dhruwang Jariwala
ff864e3c82 feat: hide progress bar toggle (#1883) 2024-01-11 09:47:36 +00:00
Dhruwang Jariwala
cc56584db6 fix: animated background issue (#1884)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-01-11 08:44:15 +00:00
Matti Nannt
440c12699c chore: add tax settings to stripe checkout (#1880) 2024-01-10 16:52:48 +00:00
Anshuman Pandey
8002d3e71f fix: global error handler for js package (#1863)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-01-10 16:33:18 +00:00
Matti Nannt
0534421538 chore: move wordpress in the docs navigation (#1879) 2024-01-10 16:27:58 +00:00
Matti Nannt
fa33460a16 chore: update npm dependencies (#1870) 2024-01-10 11:41:09 +00:00
Anshuman Pandey
f12dec7b8b fix: date picker overflow UI (#1872)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-01-10 10:53:07 +00:00
Shubham Palriwala
f2ad7c4fbf docs: overwriting css styles (#1874)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-01-10 10:52:44 +00:00
Matti Nannt
e6ce5373a2 chore: migrate to new Vercel speed insights package (#1876) 2024-01-10 10:43:03 +00:00
Johannes
90f0614aac docs: wordpress setup guide (#1873) 2024-01-10 10:35:42 +00:00
Matti Nannt
a0d7921c01 fix: get response count in analysis layout more efficiently (#1875) 2024-01-10 09:45:49 +00:00
Dhruwang Jariwala
a1fa3d6dbb chore: server side pagination for responses (#1869)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2024-01-10 08:42:40 +00:00
Dhruwang Jariwala
1d7d07b3c6 fix: avoid scaling on mobile preview (#1868) 2024-01-09 14:59:43 +00:00
Dhruwang Jariwala
c376b12461 fix: unrespoonsive welcome card editor (#1867) 2024-01-09 14:16:57 +00:00
Dima Ivashchuk
c5d9f63267 docs: Add Lost Pixel to open source friends (#1860) 2024-01-09 13:59:45 +00:00
Shubham Palriwala
9ec5d668df fix: replace hardcoded js versions with dynamic fetching (#1856) 2024-01-09 10:13:28 +00:00
Johannes
659ef3f92c fix: linked to http instead of https (#1865) 2024-01-08 21:04:58 +00:00
Johannes
3e2452b10f fix: seo issues to improve site health (#1864) 2024-01-08 20:30:15 +00:00
Dhruwang Jariwala
1f79416367 fix: github linting warnings (#1852)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-01-08 12:36:20 +00:00
Anshuman Pandey
d3e0e67bd9 feat: api key auth for private files (#1861)
Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
2024-01-08 12:33:58 +00:00
Anshuman Pandey
abe98be561 feat: adds e2e test for invite functionality (#1846) 2024-01-08 12:04:25 +00:00
Dhruwang Jariwala
f23b4f63fa chore: lint warnings in web (#1854) 2024-01-08 12:00:00 +00:00
Marc Klingen
5679c38029 docs: add Langfuse to OSS friends (#1859) 2024-01-06 22:30:09 +00:00
Johannes
a815270784 fix: make env id more easily discoverable (#1858) 2024-01-05 20:53:23 +00:00
Olasunkanmi Balogun
2d97e9c797 docs: Add best hotjar alt 2024 (#1843)
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-01-05 20:26:40 +00:00
Dhruwang Jariwala
5c90862137 fix: unblock completed response (#1857) 2024-01-05 18:09:23 +00:00
Nya Candy
206926a0a9 fix: docker compose upload volume mapping (#1822)
Co-authored-by: Shubham Palriwala <spalriwalau@gmail.com>
2024-01-05 08:38:59 +00:00
Johannes
4e0fe7e6fb fix: update logic jump error message for clarity (#1855) 2024-01-04 23:10:31 +00:00
Greg Bergé
9230ce558f docs: add Argos to OSS friends (#1847)
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
2024-01-04 17:04:12 +00:00
159 changed files with 3466 additions and 1852 deletions

View File

@@ -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"

View File

@@ -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 -->

View File

@@ -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>
---

View File

@@ -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. Its 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. Its 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 @@ Youre 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
Were currently building full-screen survey pop-ups. Youll be able to prevent users from closing the survey
unless they respond to it. Its 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 Were currently building full-screen survey pop-ups. Youll be able to prevent
users from closing the survey unless they respond to it. Its 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>
###

View File

@@ -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. Its 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. Its 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! Youre 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>
###

View File

@@ -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 dont 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. Its 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. Its 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 @@ Youre 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! Youre 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>
###

View File

@@ -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. Thats it! 🎉
<Note>
## Formbricks Widget running?
We assume that you have already installed the Formbricks Widget in your web app. Its required to display messages
and surveys in your app. If not, please follow the [Quick Start Guide (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. Its 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 dont 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, youll 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! Youre 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>
###

View File

@@ -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. Its 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. Its 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 @@ Youre 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 youll 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. Its 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! Youre 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>
###

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -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.
[![Open in Github Codespaces](https://img.shields.io/badge/Open%20in-Github%20Codespaces-blue?logo=Github)](https://Github.com/codespaces/new?machine=standardLinux32gb&repo=500289888&ref=main&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=EastUs2)
@@ -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}

View File

@@ -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.
**Cant figure it out? [Join our Discord!](https://formbricks.com/discord)**
---

View File

@@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View 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 youre in!
When you see this screen, youre 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 youll 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 dont 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -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" },
],
},

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -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: {

View File

@@ -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.",

View File

@@ -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 todays 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!

View File

@@ -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 its 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 youre 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, [its 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.
Lets 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 Deloittes 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. HotJars 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 HotJars 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.
Lets 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.
Heres 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. Its 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>;

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -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 👀",

View File

@@ -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,

View File

@@ -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",

View File

@@ -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.",

View File

@@ -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",

View File

@@ -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 💸",

View File

@@ -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 🎉",

View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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 && \

View File

@@ -84,6 +84,7 @@ export default function ActionClassesTable({
<AddNoCodeActionModal
environmentId={environmentId}
open={isAddActionModalOpen}
actionClasses={actionClasses}
setOpen={setAddActionModalOpen}
isViewer={isViewer}
/>

View File

@@ -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>

View File

@@ -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

View File

@@ -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 ? (

View File

@@ -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

View File

@@ -31,7 +31,7 @@ export default function ResponseTimeline({
useEffect(() => {
setSortedResponses(responsesAscending ? [...responses].reverse() : responses);
}, [responsesAscending]);
}, [responsesAscending, responses]);
return (
<div className="md:col-span-2">

View File

@@ -198,7 +198,7 @@ export default function SettingsNavbar({
hidden: false,
},
],
[environmentId, isFormbricksCloud, pathname]
[environmentId, isFormbricksCloud, pathname, isPricingDisabled, isViewer]
);
if (!navigation) return null;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) ? (

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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&apos;re done 🎉</p>

View File

@@ -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"

View File

@@ -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;
}

View File

@@ -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`,
};

View File

@@ -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(() => {

View File

@@ -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

View File

@@ -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");

View File

@@ -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>

View File

@@ -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;

View File

@@ -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 (

View File

@@ -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(() => {

View File

@@ -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) => {

View File

@@ -64,6 +64,7 @@ const ResponseFilter = () => {
if (!isOpen) {
clearItem();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
const handleAddNewFilter = () => {

View File

@@ -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 && (
<>

View File

@@ -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

View File

@@ -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 && (
<>

View File

@@ -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);
}}
/>

View File

@@ -40,7 +40,7 @@ export default function QuestionsView({
acc[question.id] = createId();
return acc;
}, {});
}, []);
}, [localSurvey.questions]);
const [backButtonLabel, setbackButtonLabel] = useState(null);

View File

@@ -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) {

View File

@@ -47,7 +47,7 @@ export default function SettingsView({
localSurvey={localSurvey}
setLocalSurvey={setLocalSurvey}
environmentId={environment.id}
actionClasses={actionClasses}
propActionClasses={actionClasses}
membershipRole={membershipRole}
/>

View File

@@ -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>

View File

@@ -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(() => {

View File

@@ -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);

View File

@@ -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}
/>
</>

View File

@@ -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 }}

View File

@@ -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}

View File

@@ -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">

View File

@@ -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;

View File

@@ -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);

View File

@@ -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(() => {

View File

@@ -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">

View File

@@ -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(() => {

View File

@@ -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) => {

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