Compare commits

..

88 Commits
v0.12 ... v0.15

Author SHA1 Message Date
Johannes
ba1a17578f fix autoFocus in FreeText standalone (#354) 2023-06-09 17:08:32 +02:00
Matti Nannt
1243017718 Include formbricks-api into formbricks-js (#352)
* remove debug loglevel from formbricks usage;

* remove license fields from internal packages

* improve package descriptions, add logger message for survey delay

* include formbricks api into formbricks js

* make formbricks errors package private

* update formbricks-js dependencies to include formbricks-api

* update formbricks-js to 0.1.20
2023-06-09 15:10:01 +02:00
Johannes
93c66c0caf Update Notification Email Subject (#350)
* update email noti subject

* add improvement to PR template
2023-06-09 15:02:57 +02:00
Johannes
4bfaf68de2 Smoothen Progressbar animations and minor survey editor improvements (#339)
* auto focus on sign up

* update PR template

* add updatedAt date to survey summary

* add animation to Progress, make timer smoother

* change button size in question card, auto focus

* add transition to js widget, fix auto focus in editor

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-06-09 10:31:22 +02:00
Moritz Rengert
d2aa9b5f04 fix rating question button alignment (#341)
* fix: input in Link Survey / Preview alignment

* fix: input alignment in js package
2023-06-09 10:17:31 +02:00
Johannes
91d4b09453 Feature/template update (#343)
* improve layout and information design of templates view

---------

Co-authored-by: moritzrengert <moritz@rengert.de>
2023-06-09 10:14:02 +02:00
Moritz Rengert
fc6534fa19 feature/delay survey (#345)
* add delay option to survey trigger
2023-06-09 10:08:23 +02:00
Johannes
b7e6ef5bd6 Merge pull request #346 from formbricks/lp/add-careers
add careers page, update OSS friends
2023-06-08 15:45:19 +02:00
Johannes
f0d321b073 add careers page, update OSS friends 2023-06-08 15:41:12 +02:00
Matti Nannt
944c861b18 Fix Formbricks Usage Bug leading to unidentified users (#340)
* move formbricks client to useEffect only

* add formbricks client to onboarding
2023-06-07 11:30:31 +02:00
Moritz Rengert
8a2beab5d1 Add Other Option to Multiple Choice Questions (#314)
* add other options to multiple choice question types

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
Co-authored-by: Johannes <johannes@formbricks.com>
2023-06-06 20:10:47 +02:00
Matti Nannt
d7fb29607a bugfix: mark onboarding responses as finished in formbricks (#338) 2023-06-06 15:00:32 +02:00
Matti Nannt
e2ebad0735 add current page url to formbricks-js logging (#337) 2023-06-06 09:19:18 +02:00
Matti Nannt
bd31d87046 Multiple fixes for Formbricks usage within Formbricks (#336)
* use label instead of id in onboarding analysis, add logout to formbricks usage

* add await option for all sdk commands, fix logout bug in formbricks usage
2023-06-05 17:22:52 +02:00
Matti Nannt
7040755b40 send onboarding results to formbricks (#335) 2023-06-05 11:41:47 +02:00
Matti Nannt
c4e70fbfaa update package dependencies (#333) 2023-06-01 19:16:54 +02:00
Midka
7fa2a260e8 create: api wrapper & errors package (#262)
* add @formbricks/api (api abstraction layer) and @formbricks/errors package to monorepo
* use @formbricks/api in @formbricks/js to expose an api endpoint

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-06-01 14:02:53 +02:00
Johannes
dbe7f138b6 Merge pull request #331 from formbricks/lp/reorder-friends
Reorder OSS Friends (alphabetically)
2023-06-01 13:43:07 +02:00
Johannes
35fc7b2d25 reorder friends 2023-06-01 13:36:44 +02:00
Johannes
37a0914c5a Merge pull request #323 from kof/patch-1
Added Webstudio to oss-friends.tsx
2023-06-01 10:51:43 +02:00
Moritz Rengert
8e43939206 fix: extract activeQuestionId from submit data not the expanded form (#326) 2023-06-01 08:51:32 +02:00
Matti Nannt
c4dd7ae4a2 fix security issue in link (#330) 2023-05-31 18:22:12 +02:00
Matti Nannt
0f6210c559 fix prisma commands with new json plugin (#328)
* add prisma migration, change prisma commands

* remove userAttributes from type definitions
2023-05-31 17:45:47 +02:00
Matti Nannt
965ae44344 Create SECURITY.md (#329) 2023-05-31 17:45:23 +02:00
Matti Nannt
0e94900e2c enhance prisma json types (#327) 2023-05-31 15:57:10 +02:00
Matti Nannt
99bb6932c9 update vercel migration script to fix preview deployment (#325) 2023-05-31 10:31:10 +02:00
Matti Nannt
a2e428f3c9 update readme (#324) 2023-05-31 09:55:52 +02:00
Johannes
78f7b4d03e Duplicate Questions, Add Survey Name to Summary, Update Login Screen (#322)
* Duplicate Questions, Add Survey Name to Summary, Update Login Screen

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-05-31 09:52:58 +02:00
Oleg Isonen
38f1803188 Update oss-friends.tsx
Added Webstudio
2023-05-30 21:13:16 +02:00
Johannes
66c747d1ca Merge pull request #321 from formbricks/fix/logic-in-last-question
fix/logic in last question
2023-05-30 10:53:44 +02:00
Johannes
0b24f1fe09 add "Preview" text to survey preview 2023-05-30 10:52:19 +02:00
moritzrengert
9631776552 fix: remove comments and console.log 2023-05-30 09:51:59 +02:00
moritzrengert
726b734b1a fix: update js package to make last question work with logic. fix build errors 2023-05-30 09:45:27 +02:00
moritzrengert
7ba1cc5055 fix: upade link survey to have logic working on last question 2023-05-30 09:38:59 +02:00
moritzrengert
94a10b2870 fix: thank you card not opening on skip logic, simplify check 2023-05-30 09:32:12 +02:00
moritzrengert
f71cc87b3d fix: update goToNextQuestion logic to work in last question 2023-05-30 08:50:39 +02:00
Johannes
b70b0008c1 Merge pull request #320 from formbricks/lp/add-boxy
Add BoxyHQ to OSS Friends
2023-05-29 17:44:08 +02:00
Johannes
5601f046d7 Add BoxyHQ to OSS Friends 2023-05-29 10:40:56 -05:00
Matti Nannt
a6c703620c update prisma version (#318) 2023-05-29 14:44:58 +02:00
Matti Nannt
8b56225b6e update formbricks js to 0.1.19 (#317) 2023-05-29 14:36:46 +02:00
Matti Nannt
e07a30c12d Add new client Endpoints for App-Router (WIP) (#316) 2023-05-29 14:33:02 +02:00
Moritz Rengert
92ee5529d8 Add option to auto-close (#310)
* add option to aut-close a in-app survey

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-05-29 12:47:10 +02:00
Johannes
cda8513410 Formbricks Branding Signature (#305)
* Add Formbricks Signature Branding (can be deactivated in Look & Feel Settings)

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-05-29 11:46:07 +02:00
Johannes
2912b8bd8d Merge pull request #307 from formbricks/feature/extend-link-context-menu
Extend Menu for Link Surveys
2023-05-29 11:04:04 +02:00
Matti Nannt
1db79ffa6b update package version to 0.1.18 (#315) 2023-05-28 19:14:56 +02:00
Johannes
fe4d6a8ce8 Enable users to create a new team (#299)
* Add option to create a new team

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-05-28 19:11:23 +02:00
Moritz Rengert
7238b28fb5 Add Logic Jumps (#283)
* Add the ability to add Logic Jumps to all Question Types

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-05-28 19:05:01 +02:00
Johannes
ed0135733a Merge branch 'main' of github.com:formbricks/formbricks into feature/extend-link-context-menu 2023-05-28 13:07:08 +02:00
Johannes
e7e772e155 Merge pull request #313 from DidierRLopes/patch-1
Update oss-friends.tsx w OpenBB text
2023-05-28 12:33:02 +02:00
DidierRLopes
2fbaeae9c0 Update oss-friends.tsx w OpenBB text 2023-05-27 14:30:47 -07:00
moritzrengert
37f26fa7c6 fix: show link options only on published surveys 2023-05-27 00:58:37 +02:00
Moritz Rengert
fec3741ef4 Fix Progress Bar in in-App Survey Preview 2023-05-26 12:44:54 +02:00
Matti Nannt
3618310116 update wording in PR checklist (#312) 2023-05-26 12:35:28 +02:00
Konrad Kalemba
0a1680229d Exclude updatedAt field in survey update (#297) 2023-05-26 12:14:38 +02:00
Moritz Rengert
d0f8f8d57d fix: do not create a display if the survey is in preview mode (#304) 2023-05-26 10:29:30 +02:00
Matti Nannt
e7a0821901 fix typo in PR template (#309) 2023-05-26 09:55:24 +02:00
Matti Nannt
8e98003ea2 add hanko to oss friends (#308) 2023-05-26 09:15:08 +02:00
moritzrengert
6ea6bddc5e feat: copy link to clipboard and toast success, open preview in new tab 2023-05-25 21:46:19 +02:00
Johannes
59cb636b6e Merge pull request #306 from formbricks/lp/oss-friends
add oss-friends page
2023-05-25 17:36:25 +02:00
Johannes
5469ffb8d2 add oss-friends page 2023-05-25 17:33:18 +02:00
moritzrengert
852cfacf4b feat: add preview and copy to link survey context menu 2023-05-25 09:20:56 +02:00
Matti Nannt
ba871726a5 Fix Notification emails not sending on vercel (#303) 2023-05-24 18:40:05 +02:00
Matti Nannt
33cbbd07de fix build errors and add types to dropdown menu (#302) 2023-05-24 18:09:56 +02:00
Matti Nannt
1edd69408a fix build errors on link survey (#301) 2023-05-24 17:31:34 +02:00
Johannes
b553080443 update preview UI (#300) 2023-05-24 17:14:56 +02:00
Johannes
4636ac9806 Add email notification settings (#295)
* add email notifications settings with notification options on finished responses

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-05-24 17:07:05 +02:00
Moritz Rengert
9b187a4975 Add Preview-Mode to Link-Survey (#298)
* feat: add ?=preview to preview button url

* feat: add live survey banner and restart button
2023-05-24 16:00:15 +02:00
Moritz Rengert
96344d6123 Add new Pause/Not Found screens for surveys (#285)
* Add new screens for surveys with different status (not found, paused, completed)

---------

Co-authored-by: Johannes <johannes@formbricks.com>
2023-05-23 13:35:13 +02:00
Johannes
69c05a3133 Improvements to Auth Screen based on Design Feedback (#292) 2023-05-23 09:26:10 +02:00
Konrad Kalemba
79a86050c3 fix typo in ErrorComponent (#293) 2023-05-23 09:22:04 +02:00
Thomas Kaul
d07616d4a0 Fix typo in Pricing.tsx (#294) 2023-05-23 09:21:23 +02:00
Matthias Nannt
8ce705e08c update pr template 2023-05-22 13:59:21 +02:00
Matthias Nannt
41c61cd94a add pull request template 2023-05-22 09:38:14 +02:00
Matthias Nannt
204837b844 add google oauth migration 2023-05-22 08:58:21 +02:00
Matthias Nannt
d8f4ee598d add google provider to nextauth 2023-05-19 14:28:46 +02:00
Johannes
c6aabc77b4 Revamp sign up page (#288)
* New cleaner signup/login screen
2023-05-19 12:08:24 +02:00
Johannes
b9cdf329e3 fix checkmark color on disabled 2023-05-19 10:55:10 +02:00
Moritz Rengert
953f04b42a Fix Preview on Delete in Survey Builder & Fix Button Text on light brand color (#284)
* Fix Preview on Delete in Survey Builder
* Fix Button Text on light brand color

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-05-19 08:46:46 +02:00
Johannes
41443267c9 Revamp survey settings (#287)
* improve survey settings flow

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-05-19 08:33:52 +02:00
Matthias Nannt
b76289c9d8 remove console.log, add loading state to response delete button 2023-05-16 14:31:12 +02:00
Shubhdeep Chhabra
c352c9ff5d Add ability to delete products and responses (#276)
* #263 long strings edge cases fixed

* Add ability to delete products and responses

---------

Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-05-16 14:23:14 +02:00
Matthias Nannt
eea8500501 format & update README 2023-05-16 13:28:35 +02:00
Shubhdeep Chhabra
48f53a2120 localhost urls fixed in documentation (#286) 2023-05-16 09:48:51 +02:00
Matti Nannt
26b4fd9b3e Add Attribute Filtering to Surveys (#282)
* add attribute filtering to surveys

---------

Co-authored-by: Johannes <johannes@formbricks.com>
2023-05-15 17:28:37 +02:00
Johannes
80ae43646f Update README.md 2023-05-15 01:22:43 -05:00
Johannes
14ae404b0e fix best practices 2023-05-15 07:55:19 +02:00
Johannes
a652f0df9a fix onboarding in menu 2023-05-12 14:35:56 +02:00
Johannes
b5928e71e5 Add Best Practices to Landingpage (#281)
* Update landingpage
2023-05-12 14:25:17 +02:00
328 changed files with 12663 additions and 4131 deletions

View File

@@ -88,4 +88,9 @@ NEXT_PUBLIC_SENTRY_DSN=
# Configure Github Login
NEXT_PUBLIC_GITHUB_AUTH_ENABLED=0
GITHUB_ID=
GITHUB_SECRET=
GITHUB_SECRET=
# Configure Google Login
NEXT_PUBLIC_GOOGLE_AUTH_ENABLED=0
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

View File

@@ -90,8 +90,19 @@ NEXT_PUBLIC_GITHUB_AUTH_ENABLED=0
GITHUB_ID=
GITHUB_SECRET=
# Configure Google Login
NEXT_PUBLIC_GOOGLE_AUTH_ENABLED=0
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Stripe Billing Variables
NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID=
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET=
# Configure Formbricks usage within Formbricks
NEXT_PUBLIC_FORMBRICKS_API_HOST=
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID=

42
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,42 @@
## 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. -->
Fixes # (issue)
<!-- Please provide a screenshots or a loom video for visual changes to speed up reviews
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 -->
- Test A
- Test B
## Checklist
<!-- We're starting to get more and more contributions. Please help us making this efficient for all of us and go through this checklist. Please tick off what you did -->
- [ ] Read the [contributing guide](https://github.com/formbricks/formbricks/blob/main/CONTRIBUTING.md)
- [ ] Self-reviewed my own code
- [ ] Commented on my code in hard-to-understand bits
- [ ] Ran `pnpm build`
- [ ] Checked for warnings, there are none
- [ ] Removed all `console.logs`
- [ ] Merged the latest changes from main onto my branch with `git pull origin main`
- [ ] My changes don't cause any responsiveness issues
- [ ] Updated the Formbricks Docs if changes were necessary

View File

@@ -51,15 +51,15 @@ Formbricks helps you apply best practices from data-driven work and experience m
### Upcoming Features
| | Feature |
| --- | --------------------------------------------- |
| 👷 | Rating Scale (Numbers + Emojis) Question Type |
| 👷 | Zapier, Slack & Posthog Integration |
| 👷 | Filter Audience by Attributes |
| 🗒️ | Multi-Language Functionality |
| 🗒️ | Auto-complete Surveys after at x responses |
| 🗒️ | Pre-Fill Link-Surveys |
| 🗒️ | E-Mail Surveys |
| | Feature |
| --- | ------------------------------------------ |
| 👷 | Zapier, Slack & Posthog Integration |
| 👷 | Webhooks |
| 🗒️ | Filtering Options in Survey Analysis |
| 🗒️ | Multi-Language Functionality |
| 🗒️ | Auto-complete Surveys after at x responses |
| 🗒️ | Pre-Fill Link-Surveys |
| 🗒️ | E-Mail Surveys |
_👷 In Progress | 🗒️ Up Next_

39
SECURITY.md Normal file
View File

@@ -0,0 +1,39 @@
# Security
Contact: security@formbricks.com
Based on [https://supabase.com/.well-known/security.txt](https://supabase.com/.well-known/security.txt)
At Formbricks, we consider the security of our systems a top priority. But no matter how much effort we put into system security, there can still be vulnerabilities present.
If you discover a vulnerability, we would like to know about it so we can take steps to address it as quickly as possible. We would like to ask you to help us better protect our clients and our systems.
## Out of scope vulnerabilities:
- Clickjacking on pages with no sensitive actions.
- Unauthenticated/logout/login CSRF.
- Attacks requiring MITM or physical access to a user's device.
- Any activity that could lead to the disruption of our service (DoS).
- Content spoofing and text injection issues without showing an attack vector/without being able to modify HTML/CSS.
- Email spoofing
- Missing DNSSEC, CAA, CSP headers
- Lack of Secure or HTTP only flag on non-sensitive cookies
- Deadlinks
## Please do the following:
- E-mail your findings to [security@formbricks.com](mailto:security@formbricks.com).
- Do not run automated scanners on our infrastructure or dashboard. If you wish to do this, contact us and we will set up a sandbox for you.
- Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data,
- Do not reveal the problem to others until it has been resolved,
- Do not use attacks on physical security, social engineering, distributed denial of service, spam or applications of third parties,
- Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible. Usually, the IP address or the URL of the affected system and a description of the vulnerability will be sufficient, but complex vulnerabilities may require further explanation.
## What we promise:
- We will respond to your report within 3 business days with our evaluation of the report and an expected resolution date,
- If you have followed the instructions above, we will not take any legal action against you in regard to the report,
- We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission,
- We will keep you informed of the progress towards resolving the problem,
- In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise), and
- We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved.

View File

@@ -1,4 +1,4 @@
import formbricks from "@formbricks/js";
import formbricks, { PersonId, SurveyId } from "@formbricks/js";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import { useEffect } from "react";
@@ -14,6 +14,7 @@ if (typeof window !== "undefined") {
logLevel: "debug",
});
window.formbricks = formbricks;
formbricks.refresh();
}
}

View File

@@ -68,7 +68,7 @@ export default function AppPage({}) {
<button
className="mr-2 flex max-w-xs items-center rounded-full bg-white text-sm font-medium text-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 lg:rounded-md lg:p-2 lg:hover:bg-slate-50"
onClick={() => {
formbricks.track("Feedback Button Click");
formbricks.track("Cancel Subscription");
}}>
Feedback
</button>
@@ -90,11 +90,20 @@ export default function AppPage({}) {
Set Long UserID
</button>
<button
type="button"
className="rounded-full bg-white p-1 text-slate-400 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2">
<span className="sr-only">View notifications</span>
<BellIcon className="h-6 w-6" aria-hidden="true" />
className="mr-2 flex max-w-xs items-center rounded-full bg-white text-sm font-medium text-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 lg:rounded-md lg:p-2 lg:hover:bg-slate-50"
onClick={() => {
formbricks.setAttribute("Plan", "Free");
}}>
Set attribute &quot;Free&quot;
</button>
<button
className="mr-2 flex max-w-xs items-center rounded-full bg-white text-sm font-medium text-slate-700 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:ring-offset-2 lg:rounded-md lg:p-2 lg:hover:bg-slate-50"
onClick={() => {
formbricks.setAttribute("Plan", "Paid");
}}>
Set attribute &quot;Paid&quot;
</button>
{/* Profile dropdown */}
<div className="relative ml-3">
<div>

View File

@@ -22,10 +22,10 @@ export const DocsFeedback: React.FC = () => {
return (
<div className="mt-6 inline-flex cursor-default items-center rounded-md border border-slate-200 bg-white p-4 text-slate-800 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
{!sharedFeedback ? (
<div>
<div className="text-center md:text-left">
Was this page helpful?
<Popover open={isOpen} onOpenChange={setIsOpen}>
<div className="ml-4 inline-flex space-x-3">
<div className="mt-2 inline-flex space-x-3 md:ml-4 md:mt-0">
{["Yes 👍", " No 👎"].map((option) => (
<PopoverTrigger
key={option}

View File

@@ -161,7 +161,7 @@ export const Layout: React.FC<LayoutProps> = ({ children, meta }) => {
)}
</dl>
<div className="mt-16 rounded-xl border-2 border-slate-200 bg-slate-300 p-8 dark:border-slate-700/50 dark:bg-slate-800">
<h4 className="text-3xl font-semibold text-slate-500 dark:text-slate-50">Need help?</h4>
<h4 className="text-3xl font-semibold text-slate-500 dark:text-slate-50">Need help? 🤓</h4>
<p className="my-4 text-slate-500 dark:text-slate-400">
Join our Discord and ask away. We&apos;re happy to help where we can!
</p>

View File

@@ -0,0 +1,45 @@
import type { CTAQuestion } from "@formbricks/types/questions";
import Headline from "./Headline";
import HtmlBody from "./HtmlBody";
interface CTAQuestionProps {
question: CTAQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function CTAQuestion({ question, onSubmit, lastQuestion, brandColor }: CTAQuestionProps) {
return (
<div>
<Headline headline={question.headline} questionId={question.id} />
<HtmlBody htmlString={question.html || ""} questionId={question.id} />
<div className="mt-4 flex w-full justify-end">
<div></div>
{!question.required && (
<button
type="button"
onClick={() => {
onSubmit({ [question.id]: "dismissed" });
}}
className="mr-4 flex items-center rounded-md px-3 py-3 text-base font-medium leading-4 text-slate-500 hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2 dark:border-slate-400 dark:text-slate-400">
{question.dismissButtonLabel || "Skip"}
</button>
)}
<button
type="button"
onClick={() => {
if (question.buttonExternal && question.buttonUrl) {
window?.open(question.buttonUrl, "_blank")?.focus();
}
onSubmit({ [question.id]: "clicked" });
}}
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import clsx from "clsx";
interface ContentWrapperProps {
children: React.ReactNode;
className?: string;
}
export default function ContentWrapper({ children, className }: ContentWrapperProps) {
return <div className={clsx("mx-auto max-w-7xl p-6", className)}>{children}</div>;
}

View File

@@ -0,0 +1,44 @@
// DemoPreview.tsx
import React, { useEffect, useState } from "react";
import PreviewSurvey from "./PreviewSurvey";
import { findTemplateByName } from "./templates";
import type { Template } from "@formbricks/types/templates";
interface DemoPreviewProps {
template: string;
}
const DemoPreview: React.FC<DemoPreviewProps> = ({ template }) => {
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
const selectedTemplate: Template | undefined = findTemplateByName(template);
useEffect(() => {
if (selectedTemplate) {
setActiveQuestionId(selectedTemplate.preset.questions[0].id);
}
}, [selectedTemplate]);
if (!selectedTemplate) {
return <div>Template not found.</div>;
}
return (
<div className="mx-2 flex items-center justify-center rounded-xl border-2 border-slate-300 bg-slate-200 py-6 transition-transform duration-150 dark:border-slate-500 dark:bg-slate-700 md:mx-0">
<div className="flex flex-col items-center justify-around">
<p className="my-3 text-sm text-slate-500 dark:text-slate-300">Preview</p>
<div className="">
{selectedTemplate && (
<PreviewSurvey
activeQuestionId={activeQuestionId}
questions={selectedTemplate.preset.questions}
brandColor="#94a3b8"
setActiveQuestionId={setActiveQuestionId}
/>
)}
</div>
</div>
</div>
);
};
export default DemoPreview;

View File

@@ -0,0 +1,41 @@
import type { Template } from "@formbricks/types/templates";
import { useEffect, useState } from "react";
import PreviewSurvey from "./PreviewSurvey";
import TemplateList from "./TemplateList";
import { templates } from "./templates";
export default function SurveyTemplatesPage({}) {
const [activeTemplate, setActiveTemplate] = useState<Template | null>(null);
const [activeQuestionId, setActiveQuestionId] = useState<string | null>(null);
useEffect(() => {
if (templates.length > 0) {
setActiveTemplate(templates[0]);
setActiveQuestionId(templates[0]?.preset.questions[0]?.id || null);
}
}, []);
return (
<div className="flex h-screen flex-col overflow-x-auto">
<div className="relative z-0 flex flex-1 overflow-hidden">
<TemplateList
activeTemplate={activeTemplate}
onTemplateClick={(template) => {
setActiveQuestionId(template.preset.questions[0].id);
setActiveTemplate(template);
}}
/>
<aside className="group relative h-full flex-1 flex-shrink-0 overflow-hidden rounded-r-lg bg-slate-200 shadow-inner dark:bg-slate-700 md:flex md:flex-col">
{activeTemplate && (
<PreviewSurvey
activeQuestionId={activeQuestionId}
questions={activeTemplate.preset.questions}
brandColor="#94a3b8"
setActiveQuestionId={setActiveQuestionId}
/>
)}
</aside>
</div>
</div>
);
}

View File

@@ -2,7 +2,7 @@ export const Headline: React.FC<{ headline: string; questionId: string }> = ({ h
return (
<label
htmlFor={questionId}
className="block text-base font-semibold leading-6 text-slate-900 dark:text-slate-100">
className="mb-1.5 block text-base font-semibold leading-6 text-slate-900 dark:text-slate-100">
{headline}
</label>
);

View File

@@ -0,0 +1,11 @@
/* import { cleanHtml } from "../../lib/cleanHtml"; */
import { cleanHtml } from "@formbricks/lib/cleanHtml";
export default function HtmlBody({ htmlString, questionId }: { htmlString: string; questionId: string }) {
return (
<label
htmlFor={questionId}
className="fb-block fb-font-normal fb-leading-6 text-sm text-slate-500 dark:text-slate-300"
dangerouslySetInnerHTML={{ __html: cleanHtml(htmlString) }}></label>
);
}

View File

@@ -1,25 +1,31 @@
import clsx from "clsx";
import { ReactNode, useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
export const Modal: React.FC<{ children: ReactNode; isOpen: boolean }> = ({ children, isOpen }) => {
export default function Modal({
children,
isOpen,
}: {
children: ReactNode;
isOpen: boolean;
reset: () => void;
}) {
const [show, setShow] = useState(false);
useEffect(() => {
setShow(isOpen);
}, [isOpen]);
return (
<div aria-live="assertive" className="pointer-events-none flex items-end px-4 py-6 sm:p-6">
<div className="flex w-full flex-col items-center space-y-4 sm:items-end">
<div aria-live="assertive" className="flex items-end">
<div className="flex w-full flex-col items-center p-4 sm:items-end md:min-w-[390px]">
<div
className={clsx(
className={cn(
show ? "translate-x-0 opacity-100" : "translate-x-28 opacity-0",
"pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white px-4 py-6 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out dark:bg-slate-700 sm:p-6"
"pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white px-4 py-6 shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out dark:bg-slate-900 sm:p-6"
)}>
{children}
</div>
</div>
</div>
);
};
export default Modal;
}

View File

@@ -0,0 +1,107 @@
import { useState, useEffect } from "react";
import { cn } from "@formbricks/lib/cn";
import type { MultipleChoiceMultiQuestion } from "@formbricks/types/questions";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface MultipleChoiceMultiProps {
question: MultipleChoiceMultiQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function MultipleChoiceMultiQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}: MultipleChoiceMultiProps) {
const [selectedChoices, setSelectedChoices] = useState<string[]>([]);
const [isAtLeastOneChecked, setIsAtLeastOneChecked] = useState(false);
useEffect(() => {
setIsAtLeastOneChecked(selectedChoices.length > 0);
}, [selectedChoices]);
return (
<form
onSubmit={(e) => {
e.preventDefault();
if (question.required && selectedChoices.length <= 0) {
return;
}
const data = {
[question.id]: selectedChoices,
};
onSubmit(data);
setSelectedChoices([]); // reset value
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="mt-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="relative space-y-2 rounded-md bg-white dark:bg-slate-900">
{question.choices &&
question.choices.map((choice) => (
<label
key={choice.id}
className={cn(
selectedChoices.includes(choice.label)
? "z-10 border-slate-400 bg-slate-50 dark:border-slate-400 dark:bg-slate-600"
: "border-slate-200 dark:border-slate-600 dark:bg-slate-700 dark:hover:bg-slate-600",
"relative flex cursor-pointer flex-col rounded-md border p-4 focus:outline-none"
)}>
<span className="flex items-center text-sm">
<input
type="checkbox"
id={choice.id}
name={question.id}
value={choice.label}
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0 dark:border-slate-600 dark:bg-slate-500"
aria-labelledby={`${choice.id}-label`}
checked={selectedChoices.includes(choice.label)}
onChange={(e) => {
if (e.currentTarget.checked) {
setSelectedChoices([...selectedChoices, e.currentTarget.value]);
} else {
setSelectedChoices(
selectedChoices.filter((label) => label !== e.currentTarget.value)
);
}
}}
style={{ borderColor: brandColor, color: brandColor }}
/>
<span
id={`${choice.id}-label`}
className="ml-3 font-medium text-slate-900 dark:text-slate-200">
{choice.label}
</span>
</span>
</label>
))}
</div>
</fieldset>
</div>
<input
type="text"
className="clip-[rect(0,0,0,0)] absolute m-[-1px] h-1 w-1 overflow-hidden whitespace-nowrap border-0 p-0 text-transparent caret-transparent focus:border-transparent focus:ring-0"
required={question.required}
value={isAtLeastOneChecked ? "checked" : ""}
onChange={() => {}}
/>
<div className="mt-4 flex w-full justify-between">
<div></div>
<button
type="submit"
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
</form>
);
}

View File

@@ -1,22 +1,22 @@
import clsx from "clsx";
import type { MultipleChoiceSingleQuestion as MultipleChoiceSingleQuestionType } from "./questionTypes";
import { cn } from "@formbricks/lib/cn";
import type { MultipleChoiceSingleQuestion } from "@formbricks/types/questions";
import { useState } from "react";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface MultipleChoiceSingleProps {
question: MultipleChoiceSingleQuestionType;
question: MultipleChoiceSingleQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export const MultipleChoiceSingleQuestion: React.FC<MultipleChoiceSingleProps> = ({
export default function MultipleChoiceSingleQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}) => {
}: MultipleChoiceSingleProps) {
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
return (
<form
@@ -26,9 +26,8 @@ export const MultipleChoiceSingleQuestion: React.FC<MultipleChoiceSingleProps> =
[question.id]: e.currentTarget[question.id].value,
};
e.currentTarget[question.id].value = "";
onSubmit(data);
// reset form
setSelectedChoice(null); // reset form
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
@@ -37,14 +36,14 @@ export const MultipleChoiceSingleQuestion: React.FC<MultipleChoiceSingleProps> =
<legend className="sr-only">Options</legend>
<div className="relative space-y-2 rounded-md">
{question.choices &&
question.choices.map((choice) => (
question.choices.map((choice, idx) => (
<label
key={choice.id}
className={clsx(
className={cn(
selectedChoice === choice.label
? "z-10 border-slate-400 bg-slate-50 dark:border-slate-600 dark:bg-slate-600"
: "border-gray-200 dark:border-slate-500",
"relative flex cursor-pointer flex-col rounded-md border p-4 hover:bg-slate-50 focus:outline-none dark:hover:bg-slate-600"
? "z-10 border-slate-400 bg-slate-50 dark:border-slate-400 dark:bg-slate-600"
: "border-slate-200 dark:border-slate-600 dark:bg-slate-700 dark:hover:bg-slate-600",
"relative flex cursor-pointer flex-col rounded-md border p-4 hover:bg-slate-50 focus:outline-none"
)}>
<span className="flex items-center text-sm">
<input
@@ -52,16 +51,18 @@ export const MultipleChoiceSingleQuestion: React.FC<MultipleChoiceSingleProps> =
id={choice.id}
name={question.id}
value={choice.label}
className="h-4 w-4 border border-gray-300 focus:ring-0 focus:ring-offset-0 dark:bg-slate-500"
className="h-4 w-4 border border-slate-300 focus:ring-0 focus:ring-offset-0 dark:border-slate-600 dark:bg-slate-500"
aria-labelledby={`${choice.id}-label`}
onChange={(e) => {
setSelectedChoice(e.currentTarget.value);
}}
checked={selectedChoice === choice.label}
style={{ borderColor: brandColor, color: brandColor }}
required={question.required && idx === 0}
/>
<span
id={`${choice.id}-label`}
className="ml-3 font-medium text-slate-800 dark:text-slate-300">
className="ml-3 font-medium text-slate-900 dark:text-slate-200">
{choice.label}
</span>
</span>
@@ -81,6 +82,4 @@ export const MultipleChoiceSingleQuestion: React.FC<MultipleChoiceSingleProps> =
</div>
</form>
);
};
export default MultipleChoiceSingleQuestion;
}

View File

@@ -0,0 +1,84 @@
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import type { NPSQuestion } from "@formbricks/types/questions";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface NPSQuestionProps {
question: NPSQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function NPSQuestion({ question, onSubmit, lastQuestion, brandColor }: NPSQuestionProps) {
const [selectedChoice, setSelectedChoice] = useState<number | null>(null);
const handleSelect = (number: number) => {
setSelectedChoice(number);
if (question.required) {
onSubmit({
[question.id]: number,
});
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
const data = {
[question.id]: selectedChoice,
};
onSubmit(data);
// reset form
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="my-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="flex">
{Array.from({ length: 11 }, (_, i) => i).map((number) => (
<label
key={number}
className={cn(
selectedChoice === number
? "z-10 bg-slate-50 dark:bg-slate-500"
: "dark:bg-slate-700 dark:hover:bg-slate-500",
"relative h-10 flex-1 cursor-pointer border bg-white text-center text-sm leading-10 text-slate-900 first:rounded-l-md last:rounded-r-md hover:bg-gray-100 focus:outline-none dark:border-slate-600 dark:text-white "
)}>
<input
type="radio"
name="nps"
value={number}
className="absolute h-full w-full cursor-pointer opacity-0"
onChange={() => handleSelect(number)}
required={question.required}
/>
{number}
</label>
))}
</div>
<div className="flex justify-between px-1.5 text-xs leading-6 text-slate-500">
<p>{question.lowerLabel}</p>
<p>{question.upperLabel}</p>
</div>
</fieldset>
</div>
{!question.required && (
<div className="mt-4 flex w-full justify-between">
<div></div>
<button
type="submit"
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
)}
</form>
);
}

View File

@@ -1,27 +1,33 @@
import type { OpenTextQuestion as OpenTextQuestionType } from "./questionTypes";
import type { OpenTextQuestion } from "@formbricks/types/questions";
import { useState } from "react";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface OpenTextQuestionProps {
question: OpenTextQuestionType;
onSubmit: (id: string) => void;
question: OpenTextQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export const OpenTextQuestion: React.FC<OpenTextQuestionProps> = ({
export default function OpenTextQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}) => {
}: OpenTextQuestionProps) {
const [value, setValue] = useState<string>("");
return (
<form
onSubmit={(e) => {
e.preventDefault();
const data = "Pupsi";
const data = {
[question.id]: value,
};
setValue(""); // reset value
onSubmit(data);
// reset form
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
@@ -30,9 +36,11 @@ export const OpenTextQuestion: React.FC<OpenTextQuestionProps> = ({
rows={3}
name={question.id}
id={question.id}
value={value}
onChange={(e) => setValue(e.target.value)}
placeholder={question.placeholder}
required={question.required}
className="block w-full rounded-md border border-slate-100 bg-slate-50 p-2 shadow-sm focus:border-slate-500 focus:ring-0 dark:bg-slate-500 dark:text-white sm:text-sm"></textarea>
className="block w-full rounded-md border border-slate-100 bg-slate-50 p-2 shadow-sm focus:border-slate-500 focus:ring-0 dark:border-slate-500 dark:bg-slate-700 dark:text-white sm:text-sm"></textarea>
</div>
<div className="mt-4 flex w-full justify-between">
<div></div>
@@ -45,6 +53,4 @@ export const OpenTextQuestion: React.FC<OpenTextQuestionProps> = ({
</div>
</form>
);
};
export default OpenTextQuestion;
}

View File

@@ -1,78 +1,89 @@
import { useEffect, useState } from "react";
import { useState } from "react";
import Modal from "./Modal";
import type { Question } from "./questionTypes";
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
import OpenTextQuestion from "./OpenTextQuestion";
import QuestionConditional from "./QuestionConditional";
import type { Question } from "@formbricks/types/questions";
import { Survey } from "@formbricks/types/surveys";
import ThankYouCard from "./ThankYouCard";
interface PreviewSurveyProps {
localSurvey?: Survey;
setActiveQuestionId: (id: string | null) => void;
activeQuestionId?: string | null;
questions: Question[];
brandColor: string;
}
export const PreviewSurvey: React.FC<PreviewSurveyProps> = ({ activeQuestionId, questions, brandColor }) => {
export default function PreviewSurvey({
localSurvey,
setActiveQuestionId,
activeQuestionId,
questions,
brandColor,
}: PreviewSurveyProps) {
const [isModalOpen, setIsModalOpen] = useState(true);
const [currentQuestion, setCurrentQuestion] = useState<Question | null>(null);
useEffect(() => {
if (activeQuestionId) {
if (currentQuestion && currentQuestion.id === activeQuestionId) {
setCurrentQuestion(questions.find((q) => q.id === activeQuestionId) || null);
return;
}
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions.find((q) => q.id === activeQuestionId) || null);
setIsModalOpen(true);
}, 300);
} else {
if (questions && questions.length > 0) {
setCurrentQuestion(questions[0]);
}
}
}, [activeQuestionId, questions]);
const gotoNextQuestion = () => {
if (currentQuestion) {
const currentIndex = questions.findIndex((q) => q.id === currentQuestion.id);
if (currentIndex < questions.length - 1) {
setCurrentQuestion(questions[currentIndex + 1]);
const currentIndex = questions.findIndex((q) => q.id === activeQuestionId);
if (currentIndex < questions.length - 1) {
setActiveQuestionId(questions[currentIndex + 1].id);
} else {
if (localSurvey?.thankYouCard?.enabled) {
setActiveQuestionId("thank-you-card");
} else {
// start over
setIsModalOpen(false);
setTimeout(() => {
setCurrentQuestion(questions[0]);
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
if (localSurvey?.thankYouCard?.enabled) {
setActiveQuestionId("thank-you-card");
} else {
setIsModalOpen(false);
setTimeout(() => {
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
}
}
}
};
if (!currentQuestion) {
const resetPreview = () => {
setIsModalOpen(false);
setTimeout(() => {
setActiveQuestionId(questions[0].id);
setIsModalOpen(true);
}, 500);
};
if (!activeQuestionId) {
return null;
}
const lastQuestion = currentQuestion.id === questions[questions.length - 1].id;
return (
<Modal isOpen={isModalOpen}>
{currentQuestion.type === "openText" ? (
<OpenTextQuestion
question={currentQuestion}
onSubmit={() => gotoNextQuestion()}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : currentQuestion.type === "multipleChoiceSingle" ? (
<MultipleChoiceSingleQuestion
question={currentQuestion}
onSubmit={() => gotoNextQuestion()}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : null}
</Modal>
<>
<Modal isOpen={isModalOpen} reset={resetPreview}>
{activeQuestionId == "thank-you-card" ? (
<ThankYouCard
brandColor={brandColor}
headline={localSurvey?.thankYouCard?.headline || ""}
subheader={localSurvey?.thankYouCard?.subheader || ""}
/>
) : (
questions.map(
(question, idx) =>
activeQuestionId === question.id && (
<QuestionConditional
key={question.id}
question={question}
brandColor={brandColor}
lastQuestion={idx === questions.length - 1}
onSubmit={gotoNextQuestion}
/>
)
)
)}
</Modal>
</>
);
};
export default PreviewSurvey;
}

View File

@@ -0,0 +1,65 @@
import type { Question } from "@formbricks/types/questions";
import OpenTextQuestion from "./OpenTextQuestion";
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
import MultipleChoiceMultiQuestion from "./MultipleChoiceMultiQuestion";
import NPSQuestion from "./NPSQuestion";
import CTAQuestion from "./CTAQuestion";
import RatingQuestion from "./RatingQuestion";
interface QuestionConditionalProps {
question: Question;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function QuestionConditional({
question,
onSubmit,
lastQuestion,
brandColor,
}: QuestionConditionalProps) {
return question.type === "openText" ? (
<OpenTextQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "multipleChoiceSingle" ? (
<MultipleChoiceSingleQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "multipleChoiceMulti" ? (
<MultipleChoiceMultiQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "nps" ? (
<NPSQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "cta" ? (
<CTAQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : question.type === "rating" ? (
<RatingQuestion
question={question}
onSubmit={onSubmit}
lastQuestion={lastQuestion}
brandColor={brandColor}
/>
) : null;
}

View File

@@ -0,0 +1,91 @@
import type { RatingQuestion } from "@formbricks/types/questions";
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import Headline from "./Headline";
import Subheader from "./Subheader";
interface RatingQuestionProps {
question: RatingQuestion;
onSubmit: (data: { [x: string]: any }) => void;
lastQuestion: boolean;
brandColor: string;
}
export default function RatingQuestion({
question,
onSubmit,
lastQuestion,
brandColor,
}: RatingQuestionProps) {
const [selectedChoice, setSelectedChoice] = useState<number | null>(null);
const handleSelect = (number: number) => {
setSelectedChoice(number);
if (question.required) {
onSubmit({
[question.id]: number,
});
setSelectedChoice(null); // reset choice
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
const data = {
[question.id]: selectedChoice,
};
setSelectedChoice(null); // reset choice
onSubmit(data);
}}>
<Headline headline={question.headline} questionId={question.id} />
<Subheader subheader={question.subheader} questionId={question.id} />
<div className="my-4">
<fieldset>
<legend className="sr-only">Options</legend>
<div className="flex">
{Array.from({ length: question.range }, (_, i) => i + 1).map((number) => (
<label
key={number}
className={cn(
selectedChoice === number
? "z-10 border-slate-400 bg-slate-50"
: "bg-white hover:bg-gray-100 dark:bg-slate-700 dark:hover:bg-slate-500",
"relative h-10 flex-1 cursor-pointer border border-slate-100 text-center text-sm leading-10 text-slate-800 first:rounded-l-md last:rounded-r-md focus:outline-none dark:border-slate-500 dark:text-slate-200 "
)}>
<input
type="radio"
name="rating"
value={number}
className="absolute h-full w-full cursor-pointer opacity-0"
onChange={() => handleSelect(number)}
required={question.required}
/>
{number}
</label>
))}
</div>
<div className="flex justify-between px-1.5 text-xs leading-6 text-slate-500">
<p>{question.lowerLabel}</p>
<p>{question.upperLabel}</p>
</div>
</fieldset>
</div>
{!question.required && (
<div className="mt-4 flex w-full justify-between">
<div></div>
<button
type="submit"
className="flex items-center rounded-md border border-transparent px-3 py-3 text-base font-medium leading-4 text-white shadow-sm hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
style={{ backgroundColor: brandColor }}>
{question.buttonLabel || (lastQuestion ? "Finish" : "Next")}
</button>
</div>
)}
</form>
);
}

View File

@@ -1,218 +1,77 @@
import { OnboardingIcon } from "@formbricks/ui";
import { PlusCircleIcon } from "@heroicons/react/24/outline";
import { createId } from "@paralleldrive/cuid2";
import clsx from "clsx";
import { useState } from "react";
import PreviewSurvey from "./PreviewSurvey";
import type { Template } from "./templateTypes";
import type { Template } from "@formbricks/types/templates";
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { templates } from "./templates";
export const TemplateList: React.FC = () => {
const onboardingSegmentation: Template = {
name: "Onboarding Segmentation",
icon: OnboardingIcon,
category: "Product Management",
description: "Learn more about who signed up to your product and why.",
preset: {
name: "Onboarding Segmentation",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "What is your role?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Founder",
},
{
id: createId(),
label: "Executive",
},
{
id: createId(),
label: "Product Manager",
},
{
id: createId(),
label: "Product Owner",
},
{
id: createId(),
label: "Software Engineer",
},
],
},
{
id: createId(),
type: "multipleChoiceSingle",
headline: "What's your company size?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "only me",
},
{
id: createId(),
label: "1-5 employees",
},
{
id: createId(),
label: "6-10 employees",
},
{
id: createId(),
label: "11-100 employees",
},
{
id: createId(),
label: "over 100 employees",
},
],
},
{
id: createId(),
type: "multipleChoiceSingle",
headline: "How did you hear about us first?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Recommendation",
},
{
id: createId(),
label: "Social Media",
},
{
id: createId(),
label: "Ads",
},
{
id: createId(),
label: "Google Search",
},
{
id: createId(),
label: "in a Podcast",
},
],
},
],
},
};
const [activeTemplate, setActiveTemplate] = useState<Template | null>(onboardingSegmentation);
const categories = [
"All",
...(Array.from(new Set(templates.map((template) => template.category))) as string[]),
];
const [selectedFilter, setSelectedFilter] = useState(categories[0]);
const customSurvey: Template = {
name: "Custom Survey",
description: "Create your survey from scratch.",
icon: null,
preset: {
name: "New Survey",
questions: [
{
id: createId(),
type: "openText",
headline: "What's poppin?",
subheader: "This can help us improve your experience.",
placeholder: "Type your answer here...",
required: true,
},
],
},
};
return (
<div>
<div className="mt-6 hidden flex-col md:flex">
<div className="z-0 flex min-h-[90vh] overflow-hidden">
<main className="relative z-0 max-h-[90vh] flex-1 overflow-y-auto px-6 pb-6 focus:outline-none dark:bg-slate-700">
<div className="mb-6 flex space-x-2">
{categories.map((category) => (
<button
key={category}
type="button"
onClick={() => setSelectedFilter(category)}
className={clsx(
selectedFilter === category
? "text-brand-dark border-brand-dark font-semibold"
: "border-slate-300 text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:text-slate-400",
"rounded border bg-slate-50 px-3 py-1 text-xs transition-all duration-150 dark:bg-slate-800 "
)}>
{category}
</button>
))}
</div>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{templates
.filter((template) => selectedFilter === "All" || template.category === selectedFilter)
.map((template: Template) => (
<button
type="button"
onClick={() => setActiveTemplate(template)}
key={template.name}
className={clsx(
activeTemplate?.name === template.name && "ring-brand ring-2",
"duration-120 group relative rounded-lg bg-white p-6 shadow transition-all duration-150 hover:scale-105 dark:bg-slate-600"
)}>
<div className="absolute right-6 top-6 rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500 dark:border-slate-500 dark:bg-slate-700 dark:text-slate-300">
{template.category}
</div>
<template.icon className="h-8 w-8" />
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700 dark:text-slate-200">
{template.name}
</h3>
<p className="text-left text-xs text-slate-600 dark:text-slate-400">
{template.description}
</p>
</button>
))}
<button
type="button"
onClick={() => setActiveTemplate(customSurvey)}
className={clsx(
activeTemplate?.name === customSurvey.name && "ring-brand ring-2",
"duration-120 hover:border-brand-dark group relative rounded-lg border-2 border-dashed border-slate-300 bg-transparent p-8 transition-colors duration-150"
)}>
<PlusCircleIcon className="text-brand-dark h-8 w-8 transition-all duration-150 group-hover:scale-110" />
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700 dark:text-slate-200">
{customSurvey.name}
</h3>
<p className="text-left text-xs text-slate-600 dark:text-slate-400">
{customSurvey.description}
</p>
</button>
</div>
</main>
<aside className="group relative hidden max-h-[90vh] flex-1 flex-shrink-0 overflow-hidden rounded-r-lg border-l border-slate-200 bg-slate-200 shadow-inner dark:border-slate-700 dark:bg-slate-800 md:flex md:flex-col">
{activeTemplate && (
<PreviewSurvey
activeQuestionId={null}
questions={activeTemplate.preset.questions}
brandColor="#00C4B8"
/>
)}
</aside>
</div>
</div>
<div className="flex items-center justify-center pt-36 text-slate-600 md:hidden">
This demo is not yet optimized for smartphones.
</div>
</div>
);
type TemplateList = {
onTemplateClick: (template: Template) => void;
activeTemplate: Template | null;
};
export default TemplateList;
const ALL_CATEGORY_NAME = "All";
export default function TemplateList({ onTemplateClick, activeTemplate }: TemplateList) {
const [selectedFilter, setSelectedFilter] = useState(ALL_CATEGORY_NAME);
const [categories, setCategories] = useState<Array<string>>([]);
useEffect(() => {
const defaultCategories = [
/* ALL_CATEGORY_NAME, */
...(Array.from(new Set(templates.map((template) => template.category))) as string[]),
];
const fullCategories = [ALL_CATEGORY_NAME, ...defaultCategories];
setCategories(fullCategories);
const activeFilter = ALL_CATEGORY_NAME;
setSelectedFilter(activeFilter);
}, []);
return (
<main className="relative z-0 flex-1 overflow-y-auto rounded-l-lg bg-slate-100 px-8 py-6 focus:outline-none dark:bg-slate-800">
<div className="mb-6 flex flex-wrap space-x-1">
{categories.map((category) => (
<button
key={category}
type="button"
onClick={() => setSelectedFilter(category)}
className={cn(
selectedFilter === category
? "text-brand-dark border-brand-dark font-semibold"
: "border-slate-300 text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:text-slate-300",
"mt-2 rounded border bg-slate-50 px-3 py-1 text-xs transition-all duration-150 dark:bg-slate-600 dark:hover:bg-slate-500"
)}>
{category}
</button>
))}
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{templates
.filter((template) => selectedFilter === ALL_CATEGORY_NAME || template.category === selectedFilter)
.map((template: Template) => (
<button
type="button"
onClick={() => {
onTemplateClick(template); // Pass the 'template' object instead of 'activeTemplate'
}}
key={template.name}
className={cn(
activeTemplate?.name === template.name && "ring-brand ring-2",
"duration-120 group relative rounded-lg bg-white p-6 shadow transition-all duration-150 hover:scale-105 dark:bg-slate-700"
)}>
<div className="absolute right-6 top-6 rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500 dark:border-slate-400 dark:bg-slate-800 dark:text-slate-400">
{template.category}
</div>
<template.icon className="h-8 w-8" />
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700 dark:text-slate-300">
{template.name}
</h3>
<p className="text-left text-xs text-slate-600 dark:text-slate-400">{template.description}</p>
</button>
))}
</div>
</main>
);
}

View File

@@ -0,0 +1,52 @@
import Headline from "./Headline";
import Subheader from "./Subheader";
interface ThankYouCardProps {
headline: string;
subheader: string;
brandColor: string;
}
export default function ThankYouCard({ headline, subheader, brandColor }: ThankYouCardProps) {
return (
<div className="text-center">
<div className="flex items-center justify-center" style={{ color: brandColor }}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="h-24 w-24">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
</div>
<span className="mb-[10px] inline-block h-1 w-16 rounded-[100%] bg-slate-300"></span>
<div>
<Headline headline={headline} questionId="thankYouCard" />
<Subheader subheader={subheader} questionId="thankYouCard" />
</div>
{/* <span
className="mb-[10px] mt-[35px] inline-block h-[2px] w-4/5 rounded-full opacity-25"
style={{ backgroundColor: brandColor }}></span>
<div>
<p className="text-xs text-slate-500">
Powered by{" "}
<b>
<a href="https://formbricks.com" target="_blank" className="hover:text-slate-700">
Formbricks
</a>
</b>
</p>
</div> */}
</div>
);
}

View File

@@ -1,26 +0,0 @@
export type Question = OpenTextQuestion | MultipleChoiceSingleQuestion;
export interface OpenTextQuestion {
id: string;
type: "openText";
headline: string;
subheader?: string;
placeholder?: string;
buttonLabel?: string;
required: boolean;
}
export interface MultipleChoiceSingleQuestion {
id: string;
type: "multipleChoiceSingle";
headline: string;
subheader?: string;
required: boolean;
buttonLabel?: string;
choices: Choice[];
}
export interface Choice {
id: string;
label: string;
}

View File

@@ -1,12 +0,0 @@
import { Question } from "./questionTypes";
export interface Template {
name: string;
icon: any;
description: string;
category?: "All" | "Product Management" | "Growth Marketing" | "Increase Revenue";
preset: {
name: string;
questions: Question[];
};
}

View File

@@ -1,27 +1,144 @@
import {
AppPieChartIcon,
ArrowRightCircleIcon,
ArrowUpRightIcon,
BaseballIcon,
CancelSubscriptionIcon,
CashCalculatorIcon,
CheckMarkIcon,
CodeBookIcon,
DashboardIcon,
DogChaserIcon,
DoorIcon,
FeedbackIcon,
GaugeSpeedFastIcon,
HeartCommentIcon,
InterviewPromptIcon,
LoadingBarIcon,
OnboardingIcon,
PMFIcon,
TaskListSearchIcon,
UserSearchGlasIcon,
VideoTabletAdjustIcon,
} from "@formbricks/ui";
import { createId } from "@paralleldrive/cuid2";
import type { Template } from "./templateTypes";
import type { Template } from "@formbricks/types/templates";
const thankYouCardDefault = {
enabled: true,
headline: "Thank you!",
subheader: "We appreciate your feedback.",
};
export const customSurvey: Template = {
name: "Start from scratch",
description: "Create a survey without template.",
icon: null,
preset: {
name: "New Survey",
questions: [
{
id: createId(),
type: "openText",
headline: "Custom Survey",
subheader: "This is an example survey.",
placeholder: "Type your answer here...",
required: true,
},
],
thankYouCard: thankYouCardDefault,
},
};
export const templates: Template[] = [
{
name: "Product Market Fit (Superhuman)",
icon: PMFIcon,
category: "Product Experience",
description: "Measure PMF by assessing how disappointed users would be if your product disappeared.",
preset: {
name: "Product Market Fit (Superhuman)",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "How disappointed would you be if you could no longer use Formbricks?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Not at all disappointed",
},
{
id: createId(),
label: "Somewhat disappointed",
},
{
id: createId(),
label: "Very disappointed",
},
],
},
{
id: createId(),
type: "multipleChoiceSingle",
headline: "What is your role?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Founder",
},
{
id: createId(),
label: "Executive",
},
{
id: createId(),
label: "Product Manager",
},
{
id: createId(),
label: "Product Owner",
},
{
id: createId(),
label: "Software Engineer",
},
],
},
{
id: createId(),
type: "openText",
headline: "What type of people do you think would most benefit from Formbricks?",
required: true,
},
{
id: createId(),
type: "openText",
headline: "What is the main benefit your receive from Formbricks?",
required: true,
},
{
id: createId(),
type: "openText",
headline: "How can we improve our service for you?",
subheader: "Please be as specific as possible.",
required: true,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Onboarding Segmentation",
icon: OnboardingIcon,
category: "Product Management",
category: "Product Experience",
description: "Learn more about who signed up to your product and why.",
preset: {
name: "Onboarding Segmentation",
@@ -109,214 +226,18 @@ export const templates: Template[] = [
},
{
id: createId(),
label: "in a Podcast",
label: "In a Podcast",
},
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Product Market Fit Survey",
icon: PMFIcon,
category: "Product Management",
description: "Measure PMF by assessing how disappointed users would be if your product disappeared.",
preset: {
name: "Product Market Fit Survey",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "How disappointed would you be if you could no longer use Formbricks?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Not at all disappointed",
},
{
id: createId(),
label: "Somewhat disappointed",
},
{
id: createId(),
label: "Very disappointed",
},
],
},
{
id: createId(),
type: "multipleChoiceSingle",
headline: "What is your role?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Founder",
},
{
id: createId(),
label: "Executive",
},
{
id: createId(),
label: "Product Manager",
},
{
id: createId(),
label: "Product Owner",
},
{
id: createId(),
label: "Software Engineer",
},
],
},
{
id: createId(),
type: "openText",
headline: "How can we improve our service for you?",
subheader: "Please be as specific as possible.",
required: true,
},
],
},
},
{
name: "Pre-Churn Survey",
icon: CancelSubscriptionIcon,
category: "Product Management",
description: "Find out why people cancel you. These insights are pure gold!",
preset: {
name: "Churn Survey",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "Why do you cancel your subscription?",
subheader: "We're sorry to see you leave. Please help us do better:",
required: true,
choices: [
{
id: createId(),
label: "I don't get much value out of it",
},
{
id: createId(),
label: "It's too expensive",
},
{
id: createId(),
label: "I am missing a feature",
},
{
id: createId(),
label: "Poor customer service",
},
{
id: createId(),
label: "I just don't need you anymore",
},
],
},
{
id: createId(),
type: "openText",
headline: "Is there something we can do to win you back?",
subheader: "Feel free to speak your mind, we do too.",
required: false,
},
],
},
},
{
name: "Feature Chaser",
icon: DogChaserIcon,
category: "Product Management",
description: "Follow up with users who just used a specific feature.",
preset: {
name: "Feature Chaser",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "How easy was it to achieve your goal?",
required: true,
choices: [
{
id: createId(),
label: "Extremely difficult",
},
{
id: createId(),
label: "It took a while, but I got it",
},
{
id: createId(),
label: "It was alright",
},
{
id: createId(),
label: "Quite easy",
},
{
id: createId(),
label: "Very easy, love it!",
},
],
},
{
id: createId(),
type: "openText",
headline: "Wanna add something?",
subheader: "This really helps us do better!",
required: false,
},
],
},
},
{
name: "Feedback Box",
icon: FeedbackIcon,
category: "Product Management",
description: "Give your users the chance to seamlessly share what's on their minds.",
preset: {
name: "Feedback Box",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "What's on your mind, boss?",
subheader: "Thanks for sharing. We'll get back to you asap.",
required: true,
choices: [
{
id: createId(),
label: "Bug report 🐞",
},
{
id: createId(),
label: "Feature Request 💡",
},
],
},
{
id: createId(),
type: "openText",
headline: "Give us the juicy details:",
required: true,
},
],
},
},
{
name: "Uncover Strengths & Weaknesses",
icon: TaskListSearchIcon,
category: "Growth Marketing",
category: "Growth",
description: "Find out what users like and don't like about your product or offering.",
preset: {
name: "Uncover Strengths & Weaknesses",
@@ -379,12 +300,13 @@ export const templates: Template[] = [
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Marketing Attribution",
icon: AppPieChartIcon,
category: "Growth Marketing",
category: "Growth",
description: "How did you first hear about us?",
preset: {
name: "Marketing Attribution",
@@ -414,27 +336,75 @@ export const templates: Template[] = [
},
{
id: createId(),
label: "in a Podcast",
label: "In a Podcast",
},
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Missed Trial Conversion",
name: "Churn Survey",
icon: CancelSubscriptionIcon,
category: "Increase Revenue",
description: "Find out why people cancel their subscriptions. These insights are pure gold!",
preset: {
name: "Churn Survey",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "Why did you cancel your subscription?",
subheader: "We're sorry to see you leave. Please help us do better:",
required: true,
choices: [
{
id: createId(),
label: "I didn't get much value out of it",
},
{
id: createId(),
label: "It's too expensive",
},
{
id: createId(),
label: "I am missing a feature",
},
{
id: createId(),
label: "Poor customer service",
},
{
id: createId(),
label: "I just didn't need it anymore",
},
],
},
{
id: createId(),
type: "openText",
headline: "How can we win you back?",
subheader: "Feel free to speak your mind, we do too.",
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Improve Trial Conversion",
icon: BaseballIcon,
category: "Increase Revenue",
description: "Find out why people stopped their trial. These insights help you improve your funnel.",
preset: {
name: "Missed Trial Conversion",
name: "Improve Trial Conversion",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "Why did you stop your trial?",
subheader: "Help us understand you better. Choose one option:",
subheader: "Help us understand you better:",
required: true,
choices: [
{
@@ -462,10 +432,18 @@ export const templates: Template[] = [
{
id: createId(),
type: "openText",
headline: "Did you find a better alternative? Please name it:",
headline: "Any details to share?",
required: false,
},
{
id: createId(),
type: "openText",
headline: "How are you solving your problem instead?",
subheader: "Please name alternative tools:",
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
@@ -525,12 +503,13 @@ export const templates: Template[] = [
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Measure Task Accomplishment",
icon: CheckMarkIcon,
category: "Product Management",
category: "Product Experience",
description: "See if people get their 'Job To Be Done' done. Successful people are better customers.",
preset: {
name: "Measure Task Accomplishment",
@@ -555,6 +534,16 @@ export const templates: Template[] = [
},
],
},
{
id: createId(),
type: "rating",
headline: "How easy was it to achieve your goal?",
required: true,
lowerLabel: "Very difficult",
upperLabel: "Very easy",
range: 5,
scale: "number",
},
{
id: createId(),
type: "openText",
@@ -562,12 +551,13 @@ export const templates: Template[] = [
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Identify Customer Goals",
icon: ArrowRightCircleIcon,
category: "Product Management",
category: "Product Experience",
description:
"Better understand if your messaging creates the right expectations of the value your product provides.",
preset: {
@@ -598,45 +588,140 @@ export const templates: Template[] = [
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Feature Chaser",
icon: DogChaserIcon,
category: "Product Experience",
description: "Follow up with users who just used a specific feature.",
preset: {
name: "Feature Chaser",
questions: [
{
id: createId(),
type: "rating",
headline: "How easy was it to achieve your goal?",
required: true,
lowerLabel: "Very difficult",
upperLabel: "Very easy",
range: 5,
scale: "number",
},
{
id: createId(),
type: "openText",
headline: "Wanna add something?",
subheader: "This really helps us do better!",
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Fake Door Follow-Up",
icon: DoorIcon,
category: "Product Management",
category: "Exploration",
description: "Follow up with users who ran into one of your Fake Door experiments.",
preset: {
name: "Fake Door Follow-Up",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
type: "rating",
headline: "How important is this feature for you?",
required: true,
lowerLabel: "Not important",
upperLabel: "Very important",
range: 5,
scale: "number",
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Product Market Fit Survey (short)",
icon: PMFIcon,
category: "Product Experience",
description: "Measure PMF by assessing how disappointed users would be if your product disappeared.",
preset: {
name: "Product Market Fit Survey (short)",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "How disappointed would you be if you could no longer use Formbricks?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: createId(),
label: "Very important",
label: "Not at all disappointed",
},
{
id: createId(),
label: "Not so important",
label: "Somewhat disappointed",
},
{
id: createId(),
label: "I was just looking around",
label: "Very disappointed",
},
],
},
{
id: createId(),
type: "openText",
headline: "How can we improve our service for you?",
subheader: "Please be as specific as possible.",
required: true,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Feedback Box",
icon: FeedbackIcon,
category: "Product Experience",
description: "Give your users the chance to seamlessly share what's on their minds.",
preset: {
name: "Feedback Box",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "What's on your mind, boss?",
subheader: "Thanks for sharing. We'll get back to you asap.",
required: true,
choices: [
{
id: createId(),
label: "Bug report 🐞",
},
{
id: createId(),
label: "Feature Request 💡",
},
],
},
{
id: createId(),
type: "openText",
headline: "Give us the juicy details:",
required: true,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Integration usage survey",
icon: DashboardIcon,
category: "Product Management",
category: "Product Experience",
description: "Evaluate how easily users can add integrations to your product. Find blind spots.",
preset: {
name: "Integration Usage Survey",
@@ -677,24 +762,300 @@ export const templates: Template[] = [
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
/* {
name: "In-app Interview Prompt",
icon: OnboardingIcon,
description: "Invite a specific subset of your users to schedule an interview with your product team.",
{
name: "New integration survey",
icon: DashboardIcon,
category: "Exploration",
description: "Find out which integrations your users would like to see next.",
preset: {
name: "In-app Interview Prompt",
name: "New integration survey",
questions: [
{
id: createId(),
type: "prompt",
headline: "Wanna do a short 15m interview with Charly?",
subheader: "That would really help us",
buttonLabel: "Book slot",
buttonUrl: "https://cal.com/formbricks",
type: "multipleChoiceSingle",
headline: "Which other tools are you using?",
required: true,
choices: [
{
id: createId(),
label: "PostHog",
},
{
id: createId(),
label: "Segment",
},
{
id: createId(),
label: "Hubspot",
},
{
id: createId(),
label: "Twilio",
},
{
id: createId(),
label: "Other",
},
],
},
{
id: createId(),
type: "openText",
headline: "If you chose other, please clarify:",
required: false,
},
],
},s
}, */
thankYouCard: thankYouCardDefault,
},
},
{
name: "Docs Feedback",
icon: CodeBookIcon,
category: "Product Experience",
description: "Measure how clear each page of your developer documentation is.",
preset: {
name: "Formbricks Docs Feedback",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "Was this page helpful?",
required: true,
choices: [
{
id: createId(),
label: "Yes 👍",
},
{
id: createId(),
label: "No 👎",
},
],
},
{
id: createId(),
type: "openText",
headline: "Please elaborate:",
required: false,
},
{
id: createId(),
type: "openText",
headline: "Page URL",
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Interview Prompt",
icon: InterviewPromptIcon,
category: "Exploration",
description: "Invite a specific subset of your users to schedule an interview with your product team.",
preset: {
name: "Interview Prompt",
questions: [
{
id: createId(),
type: "cta",
headline: "Do you have 15 min to talk to us? 🙏",
html: "You're one of our power users. We would love to interview you briefly!",
buttonLabel: "Book interview",
buttonUrl: "https://cal.com/johannes/onboarding?duration=25",
buttonExternal: true,
required: false,
dismissButtonLabel: "Maybe later",
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Review Prompt",
icon: HeartCommentIcon,
category: "Growth",
description: "Invite users who love your product to review it publicly.",
preset: {
name: "Review Prompt",
questions: [
{
id: createId(),
type: "cta",
headline: "You're one of our most valued customers! Please write a review for us.",
buttonLabel: "Write review",
buttonUrl: "https://formbricks.com/github",
buttonExternal: true,
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Net Promoter Score (NPS)",
icon: GaugeSpeedFastIcon,
category: "Customer Success",
description: "Measure the Net Promoter Score of your product.",
preset: {
name: "Formbricks NPS",
questions: [
{
id: createId(),
type: "nps",
headline: "How likely are you to recommend Formbricks to a friend or colleague?",
required: false,
lowerLabel: "Not likely",
upperLabel: "Very likely",
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Identify upsell opportunities",
icon: ArrowUpRightIcon,
category: "Increase Revenue",
description: "Find out how much time your product saves your user. Use it to upsell.",
preset: {
name: "Identify upsell opportunities",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "How many hours does your team save per week by using Formbricks?",
required: true,
choices: [
{
id: createId(),
label: "Less than 1 hour",
},
{
id: createId(),
label: "1 to 2 hours",
},
{
id: createId(),
label: "3 to 5 hours",
},
{
id: createId(),
label: "5+ hours",
},
],
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Build Product Roadmap",
icon: LoadingBarIcon,
category: "Product Experience",
description: "Ask how users rate your product. Identify blind spots to build your roadmap.",
preset: {
name: "Build Product Roadmap",
questions: [
{
id: createId(),
type: "rating",
headline: "How satisfied are you with the features of Formbricks?",
required: true,
lowerLabel: "Not satisfied",
upperLabel: "Very satisfied",
scale: "number",
range: 5,
},
{
id: createId(),
type: "openText",
headline: "What's the #1 thing you'd like to change in Formbricks?",
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Gauge Feature Satisfaction",
icon: UserSearchGlasIcon,
category: "Product Experience",
description: "Evaluate the satisfaction of specific features of your product.",
preset: {
name: "Gauge Feature Satisfaction",
questions: [
{
id: createId(),
type: "rating",
headline: "How easy was it to achieve ... ?",
required: true,
lowerLabel: "Not easy",
upperLabel: "Very easy",
scale: "number",
range: 5,
},
{
id: createId(),
type: "openText",
headline: "What is one thing we could do better?",
required: false,
},
],
thankYouCard: thankYouCardDefault,
},
},
{
name: "Marketing Site Clarity",
icon: VideoTabletAdjustIcon,
category: "Growth",
description: "Identify users dropping off your marketing site. Improve your messaging.",
preset: {
name: "Marketing Site Clarity",
questions: [
{
id: createId(),
type: "multipleChoiceSingle",
headline: "Do you have all the info you need to give Formbricks a try?",
required: true,
choices: [
{
id: createId(),
label: "Yes, totally",
},
{
id: createId(),
label: "Kind of...",
},
{
id: createId(),
label: "No, not at all",
},
],
},
{
id: createId(),
type: "openText",
headline: "Whats missing or unclear to you about Formbricks?",
required: false,
},
{
id: createId(),
type: "cta",
headline: "Thanks for your answer! Get 25% off your first 6 months:",
required: false,
buttonLabel: "Get discount",
buttonUrl: "https://app.formbricks.com/auth/signup",
buttonExternal: true,
},
],
thankYouCard: thankYouCardDefault,
},
},
];
export const findTemplateByName = (name: string): Template | undefined => {
return templates.find((template) => template.name === name);
};

View File

@@ -38,63 +38,63 @@ export const Hero: React.FC = ({}) => {
</span>
</p>
<div className="mx-auto mt-5 max-w-3xl items-center space-x-8 sm:flex sm:justify-center md:mt-8">
<div className="mx-auto mt-5 max-w-3xl items-center px-4 sm:flex sm:justify-center md:mt-8 md:space-x-8 md:px-0">
<p className="hidden whitespace-nowrap pt-3 text-xs text-slate-400 dark:text-slate-500 md:block">
Trusted by
</p>
<div className="grid grid-cols-5 items-center gap-8 pt-2">
<div className="grid grid-cols-3 items-center gap-8 pt-2 md:grid-cols-5">
<Image
src={CalLogoLight}
alt="Cal Logo"
className="block rounded-lg opacity-50 hover:opacity-100 dark:hidden"
className="block rounded-lg hover:opacity-100 dark:hidden md:opacity-50"
width={170}
/>
<Image
src={CalLogoDark}
alt="Cal Logo"
className="hidden rounded-lg opacity-50 hover:opacity-100 dark:block"
className="hidden rounded-lg hover:opacity-100 dark:block md:opacity-50"
width={170}
/>
<Image
src={CrowdLogoLight}
alt="Crowd.dev Logo"
className="block rounded-lg pb-1 opacity-50 hover:opacity-100 dark:hidden"
className="block rounded-lg pb-1 hover:opacity-100 dark:hidden md:opacity-50"
width={200}
/>
<Image
src={CrowdLogoDark}
alt="Crowd.dev Logo"
className="hidden rounded-lg pb-1 opacity-50 hover:opacity-100 dark:block"
className="hidden rounded-lg pb-1 hover:opacity-100 dark:block md:opacity-50"
width={200}
/>
<Image
src={ClovyrLogo}
alt="Clovyr Logo"
className="rounded-lg pb-1 opacity-50 hover:opacity-100"
className="rounded-lg pb-1 hover:opacity-100 md:opacity-50"
width={200}
/>
<Image
src={NILogoDark}
alt="Neverinstall Logo"
className="block pb-1 opacity-50 hover:opacity-100 dark:hidden"
className="block pb-1 hover:opacity-100 dark:hidden md:opacity-50"
width={200}
/>
<Image
src={NILogoLight}
alt="Neverinstall Logo"
className="hidden pb-1 opacity-50 hover:opacity-100 dark:block"
className="hidden pb-1 hover:opacity-100 dark:block md:opacity-50"
width={200}
/>
<Image
src={StackOceanLogoLight}
alt="StackOcean Logo"
className="block pb-1 opacity-50 hover:opacity-100 dark:hidden"
className="block pb-1 hover:opacity-100 dark:hidden md:opacity-50"
width={200}
/>
<Image
src={StackOceanLogoDark}
alt="StakcOcean Logo"
className="hidden pb-1 opacity-50 hover:opacity-100 dark:block"
className="hidden pb-1 hover:opacity-100 dark:block md:opacity-50"
width={200}
/>
</div>
@@ -120,7 +120,7 @@ export const Hero: React.FC = ({}) => {
</Button>
</div>
</div>
<div className="relative">
<div className="relative px-2 md:px-0">
<HeroAnimation fallbackImage={AnimationFallback} />
</div>
</div>

View File

@@ -1,3 +1,4 @@
import DemoPreview from "@/components/dummyUI/DemoPreview";
import DashboardMockupDark from "@/images/dashboard-mockup-dark.png";
import DashboardMockup from "@/images/dashboard-mockup.png";
import { Button } from "@formbricks/ui";
@@ -6,71 +7,9 @@ import Image from "next/image";
import { useState } from "react";
import AddEventDummy from "../dummyUI/AddEventDummy";
import AddNoCodeEventModalDummy from "../dummyUI/AddNoCodeEventModalDummy";
import PreviewSurvey from "../dummyUI/PreviewSurvey";
import type { Question } from "../dummyUI/questionTypes";
import HeadingCentered from "../shared/HeadingCentered";
import SetupTabs from "./SetupTabs";
const questions: Question[] = [
{
id: "1",
type: "multipleChoiceSingle",
headline: "How disappointed would you be if you could no longer use Formbricks?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: "2",
label: "Not at all disappointed",
},
{
id: "3",
label: "Somewhat disappointed",
},
{
id: "4",
label: "Very disappointed",
},
],
},
{
id: "5",
type: "multipleChoiceSingle",
headline: "What is your role?",
subheader: "Please select one of the following options:",
required: true,
choices: [
{
id: "6",
label: "Founder",
},
{
id: "7",
label: "Executive",
},
{
id: "8",
label: "Product Manager",
},
{
id: "9",
label: "Product Owner",
},
{
id: "10",
label: "Software Engineer",
},
],
},
{
id: "11",
type: "openText",
headline: "How can we improve Formbricks for you?",
subheader: "Please be as specific as possible.",
required: true,
},
];
export const Steps: React.FC = () => {
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
@@ -108,7 +47,7 @@ export const Steps: React.FC = () => {
<div className="flex h-40 items-center justify-center">
<Button
variant="primary"
className="animate-bounce transition-all duration-150 hover:scale-105"
className=""
onClick={() => {
setAddEventModalOpen(true);
}}>
@@ -143,8 +82,8 @@ export const Steps: React.FC = () => {
adjust the look and feel of your survey.
</p>
</div>
<div className="relative w-full rounded-lg bg-slate-100 p-1 dark:bg-slate-800 sm:p-8">
<PreviewSurvey questions={questions} brandColor="#00C4B8" />
<div className="relative w-full rounded-lg p-1 dark:bg-slate-800 sm:p-8">
<DemoPreview template="Product Market Fit Survey (short)" />
</div>
</div>
</div>

View File

@@ -0,0 +1,114 @@
import {
CancelSubscriptionIcon,
DogChaserIcon,
FeedbackIcon,
InterviewPromptIcon,
OnboardingIcon,
PMFIcon,
BaseballIcon,
CodeBookIcon,
} from "@formbricks/ui";
import clsx from "clsx";
import Link from "next/link";
export default function BestPracticeNavigation() {
const BestPractices = [
{
name: "Interview Prompt",
href: "/interview-prompt",
status: true,
icon: InterviewPromptIcon,
description: "Ask only power users users to book a time in your calendar. Get those juicy details.",
category: "Understand Users",
},
{
name: "Product-Market Fit Survey",
href: "/measure-product-market-fit",
status: true,
icon: PMFIcon,
description: "Find out how disappointed people would be if they could not use your service any more.",
category: "Understand Users",
},
{
name: "Onboarding Segments",
href: "/onboarding-segmentation",
status: false,
icon: OnboardingIcon,
description:
"Get to know your users right from the start. Ask a few questions early, let us enrich the profile.",
category: "Understand Users",
},
{
name: "Learn from Churn",
href: "/learn-from-churn",
status: true,
icon: CancelSubscriptionIcon,
description: "Churn is hard, but insightful. Learn from users who changed their mind.",
category: "Increase Revenue",
},
{
name: "Improve Trial CR",
href: "/improve-trial-conversion",
status: true,
icon: BaseballIcon,
description: "Take guessing out, convert more trials to paid users with insights.",
category: "Increase Revenue",
},
{
name: "Docs Feedback",
href: "/docs-feedback",
status: true,
icon: CodeBookIcon,
description: "Clear docs lead to more adoption. Understand granularly what's confusing.",
category: "Boost Retention",
},
{
name: "Feature Chaser",
href: "/feature-chaser",
status: true,
icon: DogChaserIcon,
description: "Show a survey about a new feature shown only to people who used it.",
category: "Boost Retention",
},
{
name: "Feedback Box",
href: "/feedback-box",
status: true,
icon: FeedbackIcon,
description: "Give users the chance to share feedback in a single click.",
category: "Boost Retention",
},
];
return (
<div className=" mx-auto grid grid-cols-1 gap-6 px-2 sm:grid-cols-3">
{BestPractices.map((bestPractice) => (
<Link href={bestPractice.href} key={bestPractice.name}>
<div className="drop-shadow-card duration-120 relative rounded-lg bg-slate-100 p-8 transition-all ease-in-out hover:scale-105 hover:cursor-pointer dark:bg-slate-800">
<div
className={clsx(
// base styles independent what type of button it is
"absolute right-10 rounded-full px-3 py-1",
// different styles depending on type
bestPractice.category === "Boost Retention" &&
"bg-pink-100 text-pink-500 dark:bg-pink-800 dark:text-pink-200",
bestPractice.category === "Increase Revenue" &&
"bg-blue-100 text-blue-500 dark:bg-blue-800 dark:text-blue-200",
bestPractice.category === "Understand Users" &&
"bg-orange-100 text-orange-500 dark:bg-orange-800 dark:text-orange-200"
)}>
{bestPractice.category}
</div>
<div className="h-12 w-12">
<bestPractice.icon className="h-12 w-12 " />
</div>
<h3 className="mb-1 mt-3 text-xl font-bold text-slate-700 dark:text-slate-200">
{bestPractice.name}
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">{bestPractice.description}</p>
</div>
</Link>
))}
</div>
);
}

View File

@@ -1,71 +1,7 @@
import { Button } from "@formbricks/ui";
import {
AngryBirdRageIcon,
CancelSubscriptionIcon,
DogChaserIcon,
DoorIcon,
FeedbackIcon,
InterviewPromptIcon,
OnboardingIcon,
PMFIcon,
} from "@formbricks/ui";
import clsx from "clsx";
import { usePlausible } from "next-plausible";
import { useRouter } from "next/router";
const BestPractices = [
{
title: "Onboarding Segmentation",
description:
"Get to know your users right from the start. Ask a few questions early, let us enrich the profile.",
category: "Boost Retention",
icon: OnboardingIcon,
},
{
title: "Product-Market Fit Survey",
description: "Find out how disappointed people would be if they could not use your service any more.",
category: "Boost Retention",
icon: PMFIcon,
href: "/pmf",
},
{
title: "Feature Chaser",
description: "Show a survey about a new feature shown only to people who used it.",
category: "Boost Retention",
icon: DogChaserIcon,
},
{
title: "Cancel Subscription Flow",
description: "Request users going through a cancel subscription flow before cancelling.",
category: "Boost Retention",
icon: CancelSubscriptionIcon,
},
{
title: "Interview Prompt",
description: "Ask high-interest users to book a time in your calendar to get all the juicy details.",
category: "Exploration",
icon: InterviewPromptIcon,
},
{
title: "Fake Door Follow-Up",
description: "Running a fake door experiment? Catch users right when they are full of expectations.",
category: "Exploration",
icon: DoorIcon,
},
{
title: "Feedback Box",
description: "Give users the chance to share feedback in a single click.",
category: "Retain Users",
icon: FeedbackIcon,
},
{
title: "Rage Click Survey",
description: "Sometimes things dont work. Trigger this rage click survey to catch users in rage.",
category: "Retain Users",
icon: AngryBirdRageIcon,
},
];
import BestPracticeNavigation from "./BestPracticeNavigation";
export default function InsightOppos() {
const plausible = usePlausible();
@@ -83,40 +19,9 @@ export default function InsightOppos() {
Run battle-tested approaches for qualitative user research in minutes.
</p>
</div>
<div>
<div className=" mx-auto grid max-w-5xl grid-cols-1 gap-6 px-2 sm:grid-cols-2">
{BestPractices.map((bestPractice) => {
const IconComponent: React.ElementType = bestPractice.icon;
return (
<div
key={bestPractice.title}
className="drop-shadow-card duration-120 relative rounded-lg bg-slate-100 p-8 transition-all ease-in-out hover:scale-105 dark:bg-slate-800">
<div
className={clsx(
// base styles independent what type of button it is
"absolute right-10 rounded-full px-3 py-1",
// different styles depending on type
bestPractice.category === "Boost Retention" &&
"bg-pink-100 text-pink-500 dark:bg-pink-800 dark:text-pink-200",
bestPractice.category === "Exploration" &&
"bg-blue-100 text-blue-500 dark:bg-blue-800 dark:text-blue-200",
bestPractice.category === "Retain Users" &&
"bg-orange-100 text-orange-500 dark:bg-orange-800 dark:text-orange-200"
)}>
{bestPractice.category}
</div>
<div className="h-12 w-12">
<IconComponent className="h-12 w-12 " />
</div>
<h3 className="mb-1 mt-3 text-xl font-bold text-slate-700 dark:text-slate-200">
{bestPractice.title}
</h3>
<p className="text-sm text-slate-600 dark:text-slate-400">{bestPractice.description}</p>
</div>
);
})}
</div>
</div>
<BestPracticeNavigation />
<div className="mx-auto mt-4 w-fit px-4 py-2 text-center">
<Button
variant="highlight"

View File

@@ -4,7 +4,7 @@ import { Icon } from "@/components/shared/Icon";
const styles = {
note: {
container: "bg-slate-50 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10",
container: "bg-slate-100 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10",
title: "text-slate-900 dark:text-slate-400",
body: "text-slate-800 [--tw-prose-background:theme(colors.slate.50)] prose-a:text-slate-900 prose-code:text-slate-900 dark:text-slate-300 dark:prose-code:text-slate-300",
},

View File

@@ -15,7 +15,7 @@ export default function EarlyBirdDeal() {
</h2>
<h2 className="text-xl font-semibold tracking-tight text-slate-200 sm:text-lg">
Limited deal: Only{" "}
<span className="bg- rounded-sm bg-slate-200/40 px-2 py-0.5 text-slate-100">17</span> left.
<span className="bg- rounded-sm bg-slate-200/40 px-2 py-0.5 text-slate-100">14</span> left.
</h2>
<div className="mt-6">

View File

@@ -5,6 +5,7 @@ const navigation = {
other: [
{ name: "Community", href: "/community", status: true },
{ name: "Blog", href: "/blog", status: true },
{ name: "OSS Friends", href: "/oss-friends", status: true },
{ name: "GDPR FAQ", href: "/gdpr", status: true },
{ name: "GDPR Guide", href: "/gdpr-guide", status: true },
],
@@ -47,13 +48,15 @@ export default function Footer() {
<span className="sr-only">Formbricks</span>
<FooterLogo className="mx-auto h-8 w-auto sm:h-10" />
</Link>
<p className="text-base text-slate-500 dark:text-slate-400">Experience Management for B2B SaaS</p>
<p className="text-base text-slate-500 dark:text-slate-400">
Make customer-centric decisions based on data.
</p>
<div className="border-slate-500">
<p className="text-sm text-slate-400 dark:text-slate-500">
&copy; 2022. All rights reserved.
<br />
<Link href="/imprint">Imprint</Link> | <Link href="/privacy">Privacy Policy</Link> |{" "}
<Link href="/terms">Terms</Link>
<Link href="/terms">Terms</Link> | <Link href="/oss-friends">OSS Friends</Link>
</p>
</div>
<div className="flex justify-center space-x-6">

View File

@@ -1,17 +1,100 @@
import { Button } from "@formbricks/ui";
import {
BaseballIcon,
Button,
CancelSubscriptionIcon,
CodeBookIcon,
DogChaserIcon,
FeedbackIcon,
InterviewPromptIcon,
OnboardingIcon,
PMFIcon,
} from "@formbricks/ui";
import { Popover, Transition } from "@headlessui/react";
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
import { Bars3Icon, ChevronDownIcon, ChevronRightIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { StarIcon } from "@heroicons/react/24/solid";
import clsx from "clsx";
import { usePlausible } from "next-plausible";
import Link from "next/link";
import { useRouter } from "next/router";
import { Fragment } from "react";
import { Fragment, useState } from "react";
import { FooterLogo } from "./Logo";
import { ThemeSelector } from "./ThemeSelector";
function GitHubIcon(props: any) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
<path d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" />
</svg>
);
}
const UnderstandUsers = [
{
name: "Interview Prompt",
href: "/interview-prompt",
status: true,
icon: InterviewPromptIcon,
description: "Interview invites on auto-pilot",
},
{
name: "Measure PMF",
href: "/measure-product-market-fit",
status: true,
icon: PMFIcon,
description: "Improve Product-Market Fit",
},
{
name: "Onboarding Segments",
href: "/onboarding-segmentation",
status: true,
icon: OnboardingIcon,
description: "Get it right from the start",
},
];
const IncreaseRevenue = [
{
name: "Learn from Churn",
href: "/learn-from-churn",
status: true,
icon: CancelSubscriptionIcon,
description: "Churn is hard, but insightful",
},
{
name: "Improve Trial CR",
href: "/improve-trial-conversion",
status: true,
icon: BaseballIcon,
description: "Take guessing out, hit it right",
},
];
const BoostRetention = [
{
name: "Feedback Box",
href: "/feedback-box",
status: true,
icon: FeedbackIcon,
description: "Always keep an ear open",
},
{
name: "Docs Feedback",
href: "/docs-feedback",
status: true,
icon: CodeBookIcon,
description: "Clear docs, more adoption",
},
{
name: "Feature Chaser",
href: "/feature-chaser",
status: true,
icon: DogChaserIcon,
description: "Follow up, improve",
},
];
export default function Header() {
/*
const [videoModal, setVideoModal] = useState(false); */
const [mobileSubOpen, setMobileSubOpen] = useState(false);
const plausible = usePlausible();
const router = useRouter();
return (
@@ -30,12 +113,140 @@ export default function Header() {
</Popover.Button>
</div>
<Popover.Group as="nav" className="hidden space-x-10 md:flex">
<Link
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button
className={clsx(
open
? "text-slate-600 dark:text-slate-400 "
: "text-slate-400 hover:text-slate-900 dark:hover:text-slate-100",
"group inline-flex items-center rounded-md text-base font-medium hover:text-slate-300 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:ring-offset-2 dark:hover:text-slate-50"
)}>
<span>Best Practices</span>
<ChevronDownIcon
className={clsx(
open ? "text-slate-600" : "text-slate-400",
"ml-2 h-5 w-5 group-hover:text-slate-500"
)}
aria-hidden="true"
/>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1">
<Popover.Panel className="absolute z-10 -ml-4 mt-3 w-screen max-w-lg transform lg:left-1/2 lg:ml-0 lg:max-w-4xl lg:-translate-x-1/2">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
<div className="relative grid gap-6 bg-white px-5 py-6 dark:bg-slate-700 sm:gap-6 sm:p-8 lg:grid-cols-3">
<div>
<h4 className="mb-6 ml-16 text-sm text-slate-400 dark:text-slate-300">
Understand Users
</h4>
{UnderstandUsers.map((brick) => (
<Link
key={brick.name}
href={brick.href}
className={clsx(
brick.status
? "cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-600"
: "cursor-default",
"-m-3 flex items-start rounded-lg p-3 py-4"
)}>
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center text-teal-500 sm:h-12 sm:w-12">
<brick.icon className="h-6 w-6" aria-hidden="true" />
</div>
<div className="ml-4">
<p
className={clsx(
brick.status ? "text-slate-900 dark:text-slate-100" : "text-slate-400",
"font-semibold"
)}>
{brick.name}
</p>
<p className="mt-0.5 text-xs text-slate-400">{brick.description}</p>
</div>
</Link>
))}
</div>
<div>
<h4 className="mb-6 ml-16 text-sm text-slate-400 dark:text-slate-300">
Increase Revenue
</h4>
{IncreaseRevenue.map((brick) => (
<Link
key={brick.name}
href={brick.href}
className={clsx(
brick.status
? "cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-600"
: "cursor-default",
"-m-3 flex items-start rounded-lg p-3 py-4"
)}>
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md text-teal-500 sm:h-12 sm:w-12">
<brick.icon className="h-6 w-6" aria-hidden="true" />
</div>
<div className="ml-4">
<p
className={clsx(
brick.status ? "text-slate-900 dark:text-slate-100" : "text-slate-400",
" font-semibold"
)}>
{brick.name}
</p>
<p className="mt-0.5 text-xs text-slate-400">{brick.description}</p>
</div>
</Link>
))}
</div>
<div>
<h4 className="mb-6 ml-16 text-sm text-slate-400 dark:text-slate-300">
Boost Retention
</h4>
{BoostRetention.map((brick) => (
<Link
key={brick.name}
href={brick.href}
className={clsx(
brick.status
? "cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-600"
: "cursor-default",
"-m-3 flex items-start rounded-lg p-3 py-4"
)}>
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md text-teal-500 sm:h-12 sm:w-12">
<brick.icon className="h-6 w-6" aria-hidden="true" />
</div>
<div className="ml-4">
<p
className={clsx(
brick.status ? "text-slate-900 dark:text-slate-100" : "text-slate-400",
" font-semibold"
)}>
{brick.name}
</p>
<p className="mt-0.5 text-xs text-slate-400">{brick.description}</p>
</div>
</Link>
))}
</div>
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
{/* <Link
href="/community"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Community
</Link>
*/}
<Link
href="https://formbricks.com/#pricing"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
@@ -49,8 +260,20 @@ export default function Header() {
<Link
href="/blog"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Blog{/* <p className="bg-brand inline rounded-full px-2 text-xs text-white">1</p> */}
Blog {/* <p className="bg-brand inline rounded-full px-2 text-xs text-white">1</p> */}
</Link>
<Link
href="/careers"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Careers <p className="bg-brand inline rounded-full px-2 text-xs text-white">2</p>
</Link>
{/* <Link
href="/community"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Community
</Link>
*/}
</Popover.Group>
<div className="hidden flex-1 items-center justify-end md:flex">
<ThemeSelector className="relative z-10 mr-5" />
@@ -81,7 +304,7 @@ export default function Header() {
router.push("https://app.formbricks.com");
plausible("NavBar_CTA_Login");
}}>
Login
Go to app
</Button>
</div>
</div>
@@ -113,17 +336,46 @@ export default function Header() {
</div>
<div className="px-5 py-6">
<div className="flex flex-col space-y-5 text-center text-sm dark:text-slate-300">
<div>
{mobileSubOpen ? (
<ChevronDownIcon className="mr-2 inline h-4 w-4" />
) : (
<ChevronRightIcon className="mr-2 inline h-4 w-4" />
)}
<button onClick={() => setMobileSubOpen(!mobileSubOpen)}>Best Practices</button>
</div>
{mobileSubOpen && (
<div className="flex flex-col space-y-5 text-center text-sm dark:text-slate-300">
{UnderstandUsers.map((brick) => (
<Link href={brick.href} key={brick.name} className="font-semibold">
{brick.name}
</Link>
))}
{IncreaseRevenue.map((brick) => (
<Link href={brick.href} key={brick.name} className="font-semibold">
{brick.name}
</Link>
))}
{BoostRetention.map((brick) => (
<Link href={brick.href} key={brick.name} className="font-semibold">
{brick.name}
</Link>
))}
<hr className="mx-20 my-6 opacity-25" />
</div>
)}
<Link href="/community">Community</Link>
<Link href="#pricing">Pricing</Link>
<Link href="/docs">Docs</Link>
<Link href="/blog">Blog</Link>
{/* <Button
<Link href="/careers">Careers</Link>
<Button
variant="secondary"
EndIcon={GitHubIcon}
onClick={() => router.push("https://github.com/formbricks/formbricks")}
className="flex w-full justify-center fill-slate-800 dark:fill-slate-200">
View on Github
</Button> */}
</Button>
<Button
variant="primary"
onClick={() => router.push("https://app.formbricks.com/auth/signup")}

View File

@@ -10,7 +10,7 @@ export default function HeaderLight() {
const router = useRouter();
return (
<Popover className="relative" as="header">
<div className=" max-w-8xl mx-auto flex items-center justify-between py-6 sm:px-2 md:justify-start lg:px-8 xl:px-12 ">
<div className="mx-auto flex items-center justify-between py-6 sm:px-2 md:justify-start lg:px-8 xl:px-12 ">
<div className="flex w-0 flex-1 justify-start">
<Link href="/">
<span className="sr-only">Formbricks</span>
@@ -34,7 +34,7 @@ export default function HeaderLight() {
router.push("https://app.formbricks.com/auth/signup");
plausible("Demo_CTA_TryForFree");
}}>
Create surveys for free
Start for free
</Button>
</div>
</div>

View File

@@ -14,7 +14,7 @@ export default function Layout({ title, description, children }: LayoutProps) {
<MetaInformation title={title} description={description} />
<HeaderLight />
{
<main className="max-w-8xl relative mx-auto mb-auto flex w-full flex-col justify-center sm:px-2 lg:px-8 xl:px-12">
<main className="relative mx-auto mb-auto flex w-full flex-col justify-center sm:px-2 lg:px-8 xl:px-12">
{children}
</main>
}

View File

@@ -16,7 +16,7 @@ const tiers = [
highlight: false,
description: "Host Formbricks on your own server.",
features: [
"All Free feautres",
"All Free features",
"Easy self-hosting (Docker)",
"Unlimited surveys",
"Unlimited responses",
@@ -99,8 +99,8 @@ export default function Pricing() {
<ul className="mt-4 space-y-4">
{tier.features.map((feature, index) => (
<li key={index} className="flex items-start">
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:bg-green-800">
<CheckIcon className="h-5 w-5 p-0.5 text-green-500 dark:text-green-400" />
<div className="rounded-full border border-green-300 bg-green-100 p-0.5 dark:border-green-600 dark:bg-green-900">
<CheckIcon className="h-5 w-5 p-0.5 text-green-500 dark:text-green-300" />
</div>
<span className="ml-2 text-sm text-slate-500 dark:text-slate-400">{feature}</span>
</li>

View File

@@ -0,0 +1,29 @@
import { Button } from "@formbricks/ui";
import { useRouter } from "next/router";
interface UseCaseCTAProps {
href: string;
}
export default function UseCaseHeader({ href }: UseCaseCTAProps) {
/* const plausible = usePlausible(); */
const router = useRouter();
return (
<div className="my-8 flex space-x-2 whitespace-nowrap">
<Button variant="secondary" href={href}>
Step-by-step manual
</Button>
<div className="space-y-1 text-center">
<Button
variant="darkCTA"
onClick={() => {
router.push("https://app.formbricks.com/auth/signup");
/* plausible("BestPractice_SubPage_CTA_TryItNow"); */
}}>
Try it now
</Button>
<p className="text-xs text-slate-400">It&apos;s free</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,26 @@
interface UseCaseHeaderProps {
title: string;
difficulty: string;
setupMinutes: string;
}
export default function UseCaseHeader({ title, difficulty, setupMinutes }: UseCaseHeaderProps) {
return (
<div>
<div className="mb-4 flex-wrap space-y-2">
<h1 className="mb-2 inline whitespace-nowrap pr-4 text-3xl font-semibold text-slate-800 dark:text-slate-200 ">
{title}
</h1>
<div className="inline-flex items-center justify-center whitespace-nowrap ">
<div className="rounded-full bg-indigo-200 px-4 py-1 text-sm text-indigo-700 dark:bg-indigo-800 dark:text-indigo-200 ">
{difficulty}
</div>
<div className="ml-2 rounded-full bg-slate-300 px-4 py-1 text-sm text-slate-700 dark:bg-slate-700 dark:text-slate-200 ">
{setupMinutes} minutes
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,79 @@
/*!
* Sanitize an HTML string
* (c) 2021 Chris Ferdinandi, MIT License, https://gomakethings.com
* @param {String} str The HTML string to sanitize
* @return {String} The sanitized string
*/
export function cleanHtml(str: string): string {
/**
* Convert the string to an HTML document
* @return {Node} An HTML document
*/
function stringToHTML() {
let parser = new DOMParser();
let doc = parser.parseFromString(str, "text/html");
return doc.body || document.createElement("body");
}
/**
* Remove <script> elements
* @param {Node} html The HTML
*/
function removeScripts(html) {
let scripts = html.querySelectorAll("script");
for (let script of scripts) {
script.remove();
}
}
/**
* Check if the attribute is potentially dangerous
* @param {String} name The attribute name
* @param {String} value The attribute value
* @return {Boolean} If true, the attribute is potentially dangerous
*/
function isPossiblyDangerous(name, value) {
let val = value.replace(/\s+/g, "").toLowerCase();
if (["src", "href", "xlink:href"].includes(name)) {
if (val.includes("javascript:") || val.includes("data:")) return true;
}
if (name.startsWith("on")) return true;
}
/**
* Remove potentially dangerous attributes from an element
* @param {Node} elem The element
*/
function removeAttributes(elem) {
// Loop through each attribute
// If it's dangerous, remove it
let atts = elem.attributes;
for (let { name, value } of atts) {
if (!isPossiblyDangerous(name, value)) continue;
elem.removeAttribute(name);
}
}
/**
* Remove dangerous stuff from the HTML document's nodes
* @param {Node} html The HTML document
*/
function clean(html) {
let nodes = html.children;
for (let node of nodes) {
removeAttributes(node);
clean(node);
}
}
// Convert the string to HTML
let html = stringToHTML();
// Sanitize it
removeScripts(html);
clean(html);
// If the user wants HTML nodes back, return them
// Otherwise, pass a sanitized string back
return html.innerHTML;
}

View File

@@ -0,0 +1,6 @@
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -15,14 +15,6 @@ const navigation = [
{ title: "Setup with Vue.js", href: "/docs/getting-started/vuejs" },
],
},
{
title: "Best Practices",
links: [
/* { title: "Feedback Box", href: "/docs/best-practices/feedback-box" }, */
{ title: "Docs Feedback", href: "/docs/best-practices/docs-feedback" },
/* { title: "In-app Interview Prompt", href: "/docs/best-practices/interview-prompt" }, */
],
},
{
title: "Attributes",
links: [
@@ -39,6 +31,18 @@ const navigation = [
{ title: "Code Actions", href: "/docs/actions/code" },
],
},
{
title: "Best Practices",
links: [
{ title: "Learn from Churn", href: "/docs/best-practices/cancel-subscription" },
{ title: "Interview Prompt", href: "/docs/best-practices/interview-prompt" },
{ title: "Product-Market Fit", href: "/docs/best-practices/pmf-survey" },
{ title: "Trial Conversion", href: "/docs/best-practices/improve-trial-cr" },
{ title: "Feature Chaser", href: "/docs/best-practices/feature-chaser" },
{ title: "Feedback Box", href: "/docs/best-practices/feedback-box" },
{ title: "Docs Feedback", href: "/docs/best-practices/docs-feedback" },
],
},
{
title: "API",
links: [

View File

@@ -8,7 +8,7 @@ import rehypePrism from "@mapbox/rehype-prism";
const nextConfig = {
reactStrictMode: true,
pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"],
transpilePackages: ["@formbricks/ui"],
transpilePackages: ["@formbricks/ui", "@formbricks/lib"],
async redirects() {
return [
{

View File

@@ -13,6 +13,8 @@
"dependencies": {
"@docsearch/react": "^3.3.3",
"@formbricks/ui": "workspace:*",
"@formbricks/types": "workspace:*",
"@formbricks/lib": "workspace:*",
"@headlessui/react": "^1.7.14",
"@heroicons/react": "^2.0.17",
"@mapbox/rehype-prism": "^0.8.0",

View File

@@ -0,0 +1,46 @@
import Layout from "@/components/shared/Layout";
import HeroTitle from "@/components/shared/HeroTitle";
import Link from "next/link";
const Roles = [
{
name: "Full-Stack Engineer",
description: "Join early and be a part of our journey from start to IPO 🚀",
location: "Worldwide",
workplace: "Remote",
},
{
name: "Junior Full-Stack Engineer",
description: "All you want is write code and learn? You're exactly right!",
location: "Worldwide",
workplace: "Remote",
},
];
export default function CareersPage() {
return (
<Layout
title="Careers"
description="Work with us on helping teams make customer-centric decisions - all privacy-focused.">
<HeroTitle
headingPt1="Help teams make"
headingTeal="customer-centric"
headingPt2="decisions."
subheading="We are hiring! Please see all open roles below:"
/>
<div className="mx-auto w-3/4">
{Roles.map((role) => (
<Link
href="https://formbricks.notion.site/Work-at-Formbricks-6c3ad218b2c7461ca2714ce2101730e4?pvs=4"
target="_blank"
key="role.name">
<div className="mb-6 rounded-lg border border-slate-300 bg-slate-100 p-6 shadow-sm hover:bg-slate-50">
<h4 className="text-xl font-bold text-slate-700">{role.name}</h4>
<p className="text-lg text-slate-500">{role.description}</p>
</div>
</Link>
))}
</div>
</Layout>
);
}

View File

@@ -1,12 +1,12 @@
import LayoutWaitlist from "@/components/shared/LayoutLight";
import TemplateList from "@/components/dummyUI/TemplateList";
import DemoView from "@/components/dummyUI/DemoView";
export default function DemoPage() {
return (
<LayoutWaitlist
title="Formbricks Demo"
description="Leverage 30+ templates to kick-start your experience management.">
<TemplateList />
<DemoView />
</LayoutWaitlist>
);
}

View File

@@ -0,0 +1,44 @@
import Layout from "@/components/shared/Layout";
import UseCaseHeader from "@/components/shared/UseCaseHeader";
import UseCaseCTA from "@/components/shared/UseCaseCTA";
import DocsFeedback from "@/components/docs/DocsFeedback";
import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
export default function DocsFeedbackPage() {
return (
<Layout
title="Feedback Box"
description="The better your docs, the higher your user adoption. Measure granularly how clear your documentation is.">
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
<div className="p-6 md:p-0">
<UseCaseHeader title="Docs Feedback" difficulty="Intermediate" setupMinutes="60" />
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
Why is it useful?
</h3>
<p className="text-slate-600 dark:text-slate-400">
You want to know if your Developer Docs are clear and concise. When engineers dont understand
your technology or find the answer to their question, they are unlikely to use it. Docs Feedback
opens a window into how clear your Docs are. Have a look!
</p>
<h3 className="text-md mb-1.5 mt-6 font-semibold text-slate-800 dark:text-slate-200">
How to get started:
</h3>
<p className="text-slate-600 dark:text-slate-400">
As of now, Docs Feedback uses custom UI in the frontend and Formbricks in the backend. A partial
submission is sent when the user answers YES / NO and is enriched when the open text field is
filled out and submitted.
</p>
<UseCaseCTA href="/docs/best-practices/docs-feedback" />
</div>
<div className="mx-6 my-6 flex flex-col items-center justify-center rounded-xl border-2 border-slate-300 bg-slate-200 p-4 pb-36 transition-transform duration-150 dark:border-slate-500 dark:bg-slate-700 md:mx-0">
<p className="my-3 text-sm text-slate-500">Preview</p>
<DocsFeedback />
</div>
</div>
<h2 className="mb-6 ml-4 mt-12 text-2xl font-semibold text-slate-700 dark:text-slate-400 md:mt-0">
Other Best Practices
</h2>
<BestPracticeNavigation />
</Layout>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -0,0 +1,125 @@
import { Layout } from "@/components/docs/Layout";
import { Fence } from "@/components/shared/Fence";
import { Callout } from "@/components/shared/Callout";
import Image from "next/image";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import CreateChurnFlow from "./create-cancel-flow.png";
import ChangeText from "./change-text.png";
import TriggerInnerText from "./trigger-inner-text.png";
import TriggerCSS from "./trigger-css-selector.png";
import TriggerPageUrl from "./trigger-page-url.png";
import RecontactOptions from "./recontact-options.png";
import PublishSurvey from "./publish-survey.png";
import SelectAction from "./select-action.png";
export const meta = {
title: "Learn from Churn",
description: "To know how to decrease churn, you have to understand it. Use a micro-survey.",
};
Churn is hard, but can teach you a lot. Whenever a user decides that your product isnt worth it anymore, you have a unique opportunity to get deep insights. These insights are pure gold to reduce churn.
## Purpose
The Churn Survey is among the most effective ways to identify weaknesses in you offering. People were willing to pay but now are not anymore: What changed? Lets find out!
## Preview
<DemoPreview template="Churn Survey" />
## Formbricks Approach
- Ask at exactly the right point in time
- Follow-up to prevent bad reviews
- Coming soon: Make survey mandatory
## Overview
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/)
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!
<Callout title="Formbricks Widget running?" type="note">
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)
</Callout>
### 1. Create new Churn Survey
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
Click on "Create Survey" and choose the template “Churn Survey”:
<Image src={CreateChurnFlow} alt="Create churn survey by template" quality="100" className="rounded-lg" />
### 2. Update questions (if you like)
Youre free to update the question and answer options. However, based on our experience, we suggest giving the provided template a go 😊
<Image src={ChangeText} alt="Change text content" quality="100" className="rounded-lg" />
_Want to change the button color? You can do so in the product settings._
Save, and move over to the “Audience” tab.
### 3. Pre-segment your audience
In this case, you dont really need to pre-segment your audience. You likely want to ask everyone who hits the “Cancel subscription” button.
### 4. Set up a trigger
To create the trigger for your Churn Survey, you have two options to choose from:
1. **Trigger by innerText:** You likely have a “Cancel Subscription” button in your app. You can setup a user Action with the according `innerText` to trigger the survey, like so:
<Image src={TriggerInnerText} alt="Set the trigger by inner Text" quality="100" className="rounded-lg" />
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:
<Image src={TriggerCSS} alt="Set the trigger by CSS Selector" quality="100" className="rounded-lg" />
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:
<Image src={TriggerPageUrl} alt="Set the trigger by page URL" quality="100" className="rounded-lg" />
Whenever a user visits this page, matches the filter conditions above and the recontact options (below) the survey will be displayed ✅
Here is our complete [Actions manual](/docs/actions/why) covering [Code](/docs/actions/code) and [No-Code](/docs/actions/no-code) Actions.
<Callout title="Pre-churn flow coming soon" type="note">
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 🤷
</Callout>
### 5. Select Action in the “When to ask” card
<Image src={SelectAction} alt="Select feedback button action" quality="100" className="rounded-lg" />
### 6. Last step: Set Recontact Options correctly
Lastly, scroll down to “Recontact Options”. Here you have to choose the correct settings to make sure you milk these super valuable insights. You want to make sure that this survey is always displayed, no matter if the user has already seen a survey in the past days:
<Image src={RecontactOptions} alt="Set recontact options" quality="100" className="rounded-lg" />
These settings make sure the survey is always displayed, when a user wants to Cancel their subscription.
### 7. Congrats! Youre ready to publish your survey 💃
<Image src={PublishSurvey} alt="Publish survey" quality="100" className="rounded-lg" />
<Callout title="Formbricks Widget running?" type="warning">
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) to install the widget.
</Callout>
###
# Get those insights! 🎉
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -15,6 +15,7 @@ import CopyIds from "./copy-ids.png";
export const meta = {
title: "Docs Feedback",
description: "Docs Feedback allows you to measure how clear your documentation is.",
};
Docs Feedback allows you to measure how clear your documentation is.
@@ -42,15 +43,33 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
2. In the Menu (top right) you see that you can switch between a “Development” and a “Production” environment. These are two separate environments so your test data doesnt mess up the insights from prod. Switch to “Development”:
<Image src={SwitchToDev} alt="switch to dev environment" quality="100" className="rounded" />
<Image
src={SwitchToDev}
alt="switch to dev environment"
quality="100"
className="rounded-lg"
className="rounded"
/>
3. Then, create a survey using the template “Docs Feedback”:
<Image src={DocsTemplate} alt="select docs template" quality="100" className="rounded" />
<Image
src={DocsTemplate}
alt="select docs template"
quality="100"
className="rounded-lg"
className="rounded"
/>
4. Change the Internal Question ID of the first question to **“isHelpful”** to make your life easier 😉
<Image src={ChangeId} alt="switch to dev environment" quality="100" className="rounded" />
<Image
src={ChangeId}
alt="switch to dev environment"
quality="100"
className="rounded-lg"
className="rounded"
/>
5. In the same way, you can change the Internal Question ID of the _Please elaborate_ question to **“additionalFeedback”** and the one of the _Page URL_ question to **“pageUrl”**.
@@ -59,17 +78,23 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
to be identical to the frontend we're building in the next step.
</Callout>
6. Click on “Continue to Audience” or select the audience tab manually. Scroll down to “When to ask” and create a new Action:
6. Click on “Continue to Settings or select the audience tab manually. Scroll down to “When to ask” and create a new Action:
<Image src={WhenToAsk} alt="set up when to ask card" quality="100" className="rounded" />
<Image
src={WhenToAsk}
alt="set up when to ask card"
quality="100"
className="rounded-lg"
className="rounded"
/>
7. Our goal is to create an event that never fires. This is a bit nonsensical because it is a workaround. Stick with me 😃 Fill the action out like on the screenshot:
<Image src={AddAction} alt="add action" quality="100" className="rounded" />
<Image src={AddAction} alt="add action" quality="100" className="rounded-lg" className="rounded" />
8. Select the Non-Event in the dropdown. Now you see that the “Publish survey” button is active. Publish your survey 🤝
<Image src={SelectNonevent} alt="select nonevent" quality="100" className="rounded" />
<Image src={SelectNonevent} alt="select nonevent" quality="100" className="rounded-lg" className="rounded" />
**Youre all setup in Formbricks Cloud for now 👍**
@@ -82,7 +107,7 @@ To get this running, you'll need a bit of time. Here are the steps we're going t
Before we start, lets talk about the widget. It works like this:
- Once the user selects yes/no, a partial response is created in Formbricks. It includes the feedback and the current page url.
- Once the user selects yes/no, a partial response is sent to the Formbricks API. It includes the feedback and the current page url.
- Then the user is presented with an additional open text field to further explain their choice. Once it's submitted, the previous response is updated with the additional feedback.
This allows us to capture and analyze partial feedback where the user is not willing to provide additional information.
@@ -93,7 +118,7 @@ This allows us to capture and analyze partial feedback where the user is not wil
2. Likely, you have a template file or similar which renders the navigation at the bottom of the page:
<Image src={DocsNavi} alt="doc navigation" quality="100" className="rounded" />
<Image src={DocsNavi} alt="doc navigation" quality="100" className="rounded-lg" className="rounded" />
Locate that file. We are using the [Tailwind Template “Syntax”](https://tailwindui.com/templates/syntax) for our docs. Here is our [Layout.tsx](https://github.com/formbricks/formbricks/blob/main/apps/formbricks-com/components/docs/Layout.tsx) file.
@@ -348,7 +373,7 @@ Before you roll it out in production, you want to test it. To do so, you need tw
When you are on the survey detail page, youll find both of them in the URL:
<Image src={CopyIds} alt="copy IDs" quality="100" className="rounded" />
<Image src={CopyIds} alt="copy IDs" quality="100" className="rounded-lg" className="rounded" />
Now, you have to replace the IDs and the API host accordingly in your `handleFeedbackSubmit`:

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1,106 @@
import { Layout } from "@/components/docs/Layout";
import { Fence } from "@/components/shared/Fence";
import { Callout } from "@/components/shared/Callout";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import Image from "next/image";
import ActionCSS from "./action-css.png";
import ActionText from "./action-text.png";
import ChangeText from "./change-text.png";
import CreateSurvey from "./create-survey.png";
import Publish from "./publish.png";
import RecontactOptions from "./recontact-options.png";
import SelectAction from "./select-action.png";
export const meta = {
title: "Feature Chaser",
description: "Follow up with users who used a specific feature. Gather feedback and improve your product.",
};
Following up on specific features only makes sense with very targeted surveys. Formbricks is built for that.
## Purpose
Product analytics never tell you why a feature is used - and why not. Following up on specfic features with highly relevant questions is a great way to gather feedback and improve your product.
## Preview
<DemoPreview template="Feature Chaser" />
## Formbricks Approach
- Trigger survey at exactly the right point in the user journey
- Never ask twice, keep your data clean
- Prevent survey fatigue with global waiting period
## Overview
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/)
2. Setup a user action to display survey at the right point in time
<Callout title="Formbricks Widget running?" type="note">
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)
</Callout>
### 1. Create new Feature Chaser
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
Click on "Create Survey" and choose the template “Feature Chaser”:
<Image src={CreateSurvey} alt="Create survey by template" quality="100" className="rounded-lg" />
### 2. Update questions
The questions you want to ask are dependent on your feature and can be very specific. In the template, we suggest a high-level check on how easy it was for the user to achieve their goal. We also add an opportunity to provide context:
<Image src={ChangeText} alt="Change text content" quality="100" className="rounded-lg" />
Save, and move over to where the magic happens: The “Audience” tab.
### 3. Set up a trigger for the Feature Chaser survey:
Before setting the right trigger, you need to identify a user action in your app which signals, that they have just used the feature you want to understand better. In most cases, it is clicking a specific button in your product.
You can create [Code Actions](/docs/actions/code) and [No Code Actions](/docs/actions/no-code) to follow users through your app. In this example, we will create a No Code Action.
There are two ways to track a button:
1. **Trigger by innerText:** You might have a button with a unique text at the end of your feature e.g. "Export Report". You can setup a user Action with the according `innerText` to trigger the survey, like so:
<Image src={ActionText} alt="Set the trigger by inner Text" quality="100" className="rounded-lg" />
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:
<Image src={ActionCSS} alt="Set the trigger by CSS Selector" quality="100" className="rounded-lg" />
Please follow our [Actions manual](/docs/actions/why) for an in-depth description of how Actions work.
### 4. Select Action in the “When to ask” card
<Image src={SelectAction} alt="Select PMF trigger button action" quality="100" className="rounded-lg" />
### 5. Last step: Set Recontact Options correctly
Lastly, scroll down to “Recontact Options”. Here you have full freedom to decide who you want to ask. Generally, you only want to ask every user once and prevent survey fatigue. It's up to you to decide if you want to ask again, when the user did not yet reply:
<Image src={RecontactOptions} alt="Set recontact options" quality="100" className="rounded-lg" />
### 7. Congrats! Youre ready to publish your survey 💃
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg" />
<Callout title="Formbricks Widget running?" type="warning">
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) to install the widget.
</Callout>
###
# Get those insights! 🎉
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -1,209 +1,105 @@
import { Layout } from "@/components/docs/Layout";
import { Fence } from "@/components/shared/Fence";
import Link from "next/link";
import NewFB from "@/images/docs/create-feedback-box.png";
import FBID from "@/images/docs/fb-id.png";
import Image from "next/image";
import { Callout } from "@/components/shared/Callout";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import AddAction from "./add-action.png";
import AddCSSAction from "./add-css-action.png";
import AddHTMLAction from "./add-html-action.png";
import ChangeTextContent from "./change-text-content.png";
import CreateFeedbackBox from "./create-feedback-box-by-template.png";
import PublishSurvey from "./publish-survey.png";
import SelectAction from "./select-feedback-button-action.png";
import RecontactOptions from "./set-recontact-options.png";
export const meta = {
title: "Feedback Box",
description: "The Feedback Box gives your users a direct channel to share their feedback and feel heard.",
};
The Feedback Box gives your user a direct channel to share their feedback and feel heard.
The Feedback Box gives your users a direct channel to share their feedback and feel heard.
## Purpose
Allow users to share feedback with 2 clicks. A low friction way to gather feedback helps catching even the smallest points of annoyance / frustration in user experiences.
A low friction way to gather feedback helps catching even the smallest points of frustration in user experiences. Use automations to react rapidly and make users feel heard.
## Preview
<DemoPreview template="Feedback Box" />
## Formbricks Approach
- Make it **easy**: 2 clicks to share feedback
- **Pre-sort** feedback:
- Bug → Pipe into Bug channel for devs
- Feedback → Pipe into Feedback channel for PMs
## Preview
- Make it easy: 2 clicks to share feedback
- Pipe insights where team can see them and react quickly
## Installation
To add the Feedback Box to your app, you need to perform these steps:
1. Create new Feedback Box at app.formbricks.com
2. Setup Feedback Box with template
3. Use NPM to install or embed JS snippet in `<head>`
4. Test
2. Add user action to trigger Feedback Box
3. Update recontact settings to display correctly
### 1. Create new Feedback Box
Create an account at [app.formbricks.com](https://app.formbricks.com/auth/signup)
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
Then, create a new Feedback Box:
Then, create a new survey and look for the "Feedback Box" template:
<Image src={NewFB} alt="new feedback box" quality="100" />
<Image src={CreateFeedbackBox} alt="Create feedback box by template" quality="100" className="rounded-lg" />
Go to the "Setup instructions" tab and locate your Feedback Box ID. You'll need it in a minute:
### 2. Update question content
<Image src={FBID} alt="copy feedback box id" quality="100" />
Change the questions and answer options according to your preference:
### 2. Embed Formbricks snippet in `<head>`
<Image src={ChangeTextContent} alt="Change text content" quality="100" className="rounded-lg" />
Embed the following Feedback Box script in your HTML `<head>` tag.
### 3. Create user action to trigger Feedback Box:
```tsx
<script src="https://cdn.jsdelivr.net/npm/@formbricks/feedback@0.3" defer></script>
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:
<script>
window.formbricks = {
...window.formbricks,
config: {
formbricksUrl: "https://app.formbricks.com",
formId: "YOUR FEEDBACK BOX ID HERE", // copy from Formbricks dashboard
contact: {
name: "Matti",
position: "Co-Founder",
imgUrl: "https://avatars.githubusercontent.com/u/675065?s=128&v=4",
},
},
}
</script>
```
<Image src={AddAction} alt="Add action" quality="100" className="rounded-lg" />
Afterwards you need to embed the feedback box into your app. The standard ways are either a <Link href="/docs/wrappers/pop-over">pop-over on button click</Link> or <Link href="/docs/wrappers/inline">inline inserted into a div</Link>.
<Callout title="You can also add actions in your code" type="note">
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.
</Callout>
For example for a pop-over on button click you need to add the following code to your app:
We have two options to track the Feedback Button in your application: innerText and CSS-Selector:
```html
<button data-formbricks-button>Feedback</button>
```
1. **innerText:** This means that whenever a user clicks any HTML item in your app which has an `innerText` of `Feedback` the Feedback Box will be displayed.
2. **CSS-Selector:** This means that when an element with a specific CSS-Selector like `#feedback-button` is clicked, your Feedback Box is triggered.
### 3. Configure Feedback Box
<div className="grid grid-cols-2 space-x-2">
<Image src={AddHTMLAction} alt="Add HTML action" quality="100" className="rounded-lg" />
<Image src={AddCSSAction} alt="Add CSS action" quality="100" className="rounded-lg" />
</div>
You can change the content behaviour of the Feedback Box with the config object.
### 4. Select action in the “When to ask” card
**Basic config**
<Image src={SelectAction} alt="Select feedback button action" quality="100" className="rounded-lg" />
Add your Formbricks form ID and the formbricks server address to the config object.
### 5. Set Recontact Options correctly
```html
<script>
window.formbricks = {
...window.formbricks,
config: {
formbricksUrl: "https://app.formbricks.com",
formId: "cldipnvz80002le0ha2a3zhgl",
},
};
</script>
```
Scroll down to “Recontact Options”. Here you have to choose the right settings so that the Feedback Box pops up every time the user action is performed. (Our default is that every user sees every survey only once):
**Personalizing**
<Image src={RecontactOptions} alt="Set recontact options" quality="100" className="rounded-lg" />
Add your name, position and image link to give users the impression that you care about their feedback :)
### 7. Youre ready publish your survey!
```javascript
window.formbricks = {
...window.formbricks,
config: {
// ...
contact: {
name: "Matti",
position: "Co-Founder",
imgUrl: "https://avatars.githubusercontent.com/u/675065?s=128&v=4",
},
},
};
```
<Image src={PublishSurvey} alt="Publish survey" quality="100" className="rounded-lg" />
<div id="add-user-email">**Sending user data with feedback**</div>
## Setting up the Widget
The feedback box is built for in-app experiences. We assume that you already have user properties stored in a session object.
<Callout title="Formbricks Widget running?" type="warning">
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) to install the widget.
</Callout>
Here is an example of how to pass it to Formbricks. However, it might differ in your specific case.
### &nbsp;
```javascript
window.formbricks = {
...window.formbricks,
config: {
// ...
customer: {
email: "", // fill dynamically
name: "", // fill dynamically
},
};
},
```
Note: the `email` field must be present in the customer object
**Styling**
You can style the Feedback Box to match your UI. We recommend to at least replace the brand color with your main color.
```javascript
window.formbricks = {
...window.formbricks,
config: {
// ...
style: {
brandColor: "#00c4b8",
},
};
},
```
Here are all variables you can set with the current defaults:
```javascript
style: {
brandColor: "#00c4b8",
borderRadius: "0.4rem",
headerBGColor: "#1e293b",
headerTitleColor: "#111111",
boxBGColor: "#cbd5e1",
textColor: "#0f172a",
buttonHoverColor: "#e2e8f0",
},
```
### Example config
Here is an example of a full config object:
```javascript
window.formbricks = {
...window.formbricks,
config: {
formbricksUrl: "https://app.formbricks.com",
formId: "cldipcgat0000mn0g31a8pdse",
containerId: "formbricks-feedback-box", // only needed for modal & inline
contact: {
name: "Johannes",
position: "Co-Founder",
imgUrl: "https://avatars.githubusercontent.com/u/72809645?v=4",
},
customer: {
id: "", // fill dynamically
name: "", // fill dynamically
email: "", // fill dynamically
},
style: {
brandColor: "#0E1420",
headerBGColor: "#E5E7EB",
headerTitleColor: "#4B5563",
boxBGColor: "#F9FAFB",
textColor: "#374151",
buttonHoverColor: "#F3F4F6",
},
},
};
```
### 4. Render survey in your app
To add the Feedback Box to your UI, you can use different wrappers. Please follow the instructions linked below:
1. <Link href="/docs/wrappers/pop-over">In-app Pop-over</Link>
2. <Link href="/docs/wrappers/modal">Modal</Link>
3. <Link href="/docs/wrappers/inline">Inline</Link>
# Thats it! 🎉
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -0,0 +1,114 @@
import { Layout } from "@/components/docs/Layout";
import { Fence } from "@/components/shared/Fence";
import { Callout } from "@/components/shared/Callout";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import Image from "next/image";
import ActionText from "./action-innertext.png";
import ActionPageurl from "./action-pageurl.png";
import ChangeText from "./change-text.png";
import CreateSurvey from "./create-survey.png";
import Publish from "./publish.png";
import RecontactOptions from "./recontact-options.png";
import SelectAction from "./select-action.png";
export const meta = {
title: "Improve Trial Conversion",
description: "Understand how to improve the trial conversions to get more paying customers.",
};
When a user doesn't convert, you want to know why. A micro-survey displayed at exactly the right time gives you a window into understanding the most relevant question: To pay or not to pay?
## Purpose
The better you understand why free users dont convert to paid users, the higher your revenue. You can make an informed decision about what to change in your offering to make more people pay for your service.
## Preview
<DemoPreview template="Improve Trial Conversion" />
## Formbricks Approach
- Ask at exactly the right point in time
- Ask to understand the problem, dont ask for solutions
## Installation
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/)
2. Set up the user action to display survey at right point in time
3. Print that 💸
<Callout title="Formbricks Widget running?" type="note">
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)
</Callout>
### 1. Create new Trial Conversion Survey
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
Click on "Create Survey" and choose the template “Improve Trial Conversion”:
<Image src={CreateSurvey} alt="Create survey by template" quality="100" className="rounded-lg" />
### 2. Update questions (if you like)
Youre free to update the questions and answer options. However, based on our experience, we suggest giving the provided template a go 😊
<Image src={ChangeText} alt="Change text content" quality="100" className="rounded-lg" />
_Want to change the button color? You can do so in the product settings!_
Save, and move over to the “Audience” tab.
### 3. Pre-segment your audience (coming soon)
<Callout title="Filter by attribute coming soon" type="note">
We're working on pre-segmenting users by attributes. We will update this manual in the next days.
</Callout>
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.
### 4. Set up a trigger for the Trial Conversion Survey:
How you trigger your survey depends on your product. There are two options:
1. **Trigger by pageURL:** Lets say you have a page under “/trial-cancelled” where you forward users once they cancelled the trial subscription. You can then create an user Action with the type `pageURL` with the following settings:
<Image src={ActionPageurl} alt="Change text content" quality="100" className="rounded-lg" />
Whenever a user visits this page, the survey will be displayed ✅
2. **Trigger by Button Click:** In a different case, you have a “Cancel Trial button in your app. You can setup a user Action with the according `innerText` like so:
<Image src={ActionText} alt="Change text content" quality="100" className="rounded-lg" />
Please have a look at our complete [Actions manual](/docs/actions/why) if you have questions.
### 5. Select Action in the “When to ask” card
<Image src={SelectAction} alt="Select feedback button action" quality="100" className="rounded-lg" />
### 6. Last step: Set Recontact Options correctly
Lastly, scroll down to “Recontact Options”. Here you have to choose the correct settings to make sure you gather as many insights as possible. You want to make sure that this survey is always displayed, no matter if the user has already seen a survey in the past days:
<Image src={RecontactOptions} alt="Set recontact options" quality="100" className="rounded-lg" />
### 7. Congrats! Youre ready to publish your survey 💃
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg" />
<Callout title="Formbricks Widget running?" type="warning">
You need to have the Formbricks Widget installed to display the Improve Trial Conversion Survey in your app.
Please follow [this tutorial (Step 4 onwards)](/docs/getting-started/quickstart) to install the widget.
</Callout>
###
# Go get 'em 🎉
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -1,14 +1,133 @@
import { Layout } from "@/components/docs/Layout";
import { Fence } from "@/components/shared/Fence";
import { Callout } from "@/components/shared/Callout";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import Image from "next/image";
import ActionCSS from "./action-css.png";
import ActionInner from "./action-innertext.png";
import ActionPageurl from "./action-pageurl.png";
import AddAction from "./add-action.png";
import ChangeText from "./change-text.png";
import CreatePrompt from "./create-prompt.png";
import InterviewExample from "./interview-example.png";
import Publish from "./publish-survey.png";
import RecontactOptions from "./recontact-options.png";
import SelectAction from "./select-action.png";
export const meta = {
title: "In-app Interview Prompt",
description: "Invite only power users to schedule an interview with your product team.",
};
<Callout title="Closed Beta" type="note">
In-app interview prompts are only available in closed beta. Want to become a design partner? Reach out at
hola@formbricks.com
The Interview Prompt allows you to pick a specific user segment (e.g. Power Users) and invite them to a user interview. Bye, bye spammy email invites, benefit from up to 6x more respondents.
## Purpose
Product analytics and in-app surveys are incomplete without user interviews. Set the scheduling on autopilot for a continuous stream of interviews.
## Preview
<DemoPreview template="Interview Prompt" />
## Formbricks Approach
- Pre-segment users with custom attributes. Only invite highly relevant users.
- In-app prompts have a 6x higher conversion rate than email invites.
- Set scheduling user interviews on auto pilot.
- Soon: Integrate directly with your [Cal.com](http://Cal.com) account.
## Installation
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/)
2. Adjust content and settings
3. Thats it! 🎉
<Callout title="Formbricks Widget running?" type="note">
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)
</Callout>
### 1. Create new Interview Prompt
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
Click on "Create Survey" and choose the template “Interview Prompt”:
<Image src={CreatePrompt} alt="Create interview prompt by template" quality="100" className="rounded-lg" />
### 2. Update prompt and CTA
Update the prompt, description and button text to match your products tonality. You can also update the button color in the Product Settings.
<Image src={ChangeText} alt="Change text content" quality="100" className="rounded-lg" />
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!
<Image src={InterviewExample} alt="Add CSS action" quality="100" className="rounded-lg" />
Save, and move over to the “Audience” tab.
### 3. Pre-segment your audience (coming soon)
<Callout title="Filter by attribute coming soon" type="note">
We're working on pre-segmenting users by attributes. We will update this manual in the next few days.
</Callout>
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.
In our case, we want to select users who we have assigned the attribute “Power User”. To learn how to assign attributes to your users, please [follow this guide](/docs/attributes/why).
Great, now only the “Power User” segment will see our Interview Prompt. But when will they see it?
### 4. Set up a trigger for the Interview Prompt:
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" />
<Callout title="You can also add actions in your code" type="note">
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.
</Callout>
Generally, we have two types of user actions: Page views and clicks. The Interview Prompt, youll likely want to display on a page visit since you already filter who sees the prompt by attributes.
1. **pageURL:** Whenever a user visits a page the survey will be displayed, as long as the other conditions match. Other conditions are pre-segmentation, if this user has seen a survey in the past 2 weeks, etc.
<Image src={ActionPageurl} alt="Add page URL action" quality="100" className="rounded-lg" />
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="grid grid-cols-2 space-x-2">
<Image src={ActionCSS} alt="Add CSS action" quality="100" className="rounded-lg" />
<Image src={ActionInner} alt="Add inner text action" quality="100" className="rounded-lg" />
</div>
### 5. Select action in the “When to ask” card
<Image src={SelectAction} alt="Select feedback button action" quality="100" className="rounded-lg" />
### 6. Set Recontact Options correctly
Scroll down to “Recontact Options”. Here you have to choose the correct settings to strike the right balance between asking for user feedback and preventing survey fatigue. Your settings also depend on the size of your user base or segment. If you e.g. have thousands of “Power Users” you can easily afford to only display the prompt once. If you have a smaller user base you might want to ask twice to get a sufficient amount of bookings:
<Image src={RecontactOptions} alt="Set recontact options" quality="100" className="rounded-lg" />
### 7. Congrats! Youre ready to publish your survey 💃 🤸
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg" />
<Callout title="Formbricks widget running?" type="note">
You need to have the Formbricks Widget installed to display the Interview Prompt in your app. Please follow
[this tutorial (Step 4 onwards)](/docs/getting-started/quickstart) to install the widget.
</Callout>
###
# Learn about them users! 🎉
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@@ -1,175 +1,116 @@
import { Layout } from "@/components/docs/Layout";
import { Fence } from "@/components/shared/Fence";
import Link from "next/link";
import { Callout } from "@/components/shared/Callout";
import DemoPreview from "@/components/dummyUI/DemoPreview";
import Image from "next/image";
import NewPMF from "@/images/docs/new-pmf.png";
import ID from "@/images/docs/copy-id.png";
import ActionCSS from "./action-css.png";
import ActionPageurl from "./action-pageurl.png";
import ChangeText from "./change-text.png";
import CreateSurvey from "./create-survey.png";
import Publish from "./publish.png";
import RecontactOptions from "./recontact-options.png";
import SelectAction from "./select-action.png";
export const meta = {
title: "Product-Market Fit Survey",
description: "The Product-Market Fit survey helps you measure, well, Product-Market Fit (PMF).",
};
The Product-Market Fit survey (or Sean Ellis Test) is a method to measure Product-Market Fit.
## Purpose
By assessing how disappointed users would be if they could no longer use your service you get a good idea of how well your current product fits your target market.
Measuring it allows you to optimize it.
## Formbricks Approach
- Higher conversion: In-app surveys **convert significantly better** than email surveys
- **Pre-segment** user base: Only ask users who experienced the value of your product
- **Specific** dashboard: Understand your data right, separate signal from noise by design
- Targeted approach: **Personally address** users with their name (if you have it)
- Measure continuously: Feel the pulse of your user base **consistently**
- No UI clutter: **Natively embed** the survey for best possible UX
- **Never** ask twice: Assure to not survey users twice
Measuring and understanding your PMF is essential to build a large, successful business. It helps you understand what users like, what theyre missing and what to build next. This survey is perfectly suited to measure PMF like [Superhuman](https://review.firstround.com/how-superhuman-built-an-engine-to-find-product-market-fit).
## Preview
<div className="max-w-md"></div>
<DemoPreview template="Product Market Fit (Superhuman)" />
## Installation
## Formbricks Approach
To add the Product-Market Fit Survey to your app, you need to perform these steps:
- Pre-segment users to only survey users who have experienced your products value
- Never ask twice, keep your data clean
- Run on autopilot: Set up once, keep surveying users continuously
1. Create new survey at app.formbricks.com
2. Embed JS snippet in `<head>`
3. Configure survey
4. Render in-app
## Overview
### 1. Create new survey
To display the Product-Market Fit survey in your app you want to proceed as follows:
Create an account at [app.formbricks.com](https://app.formbricks.com/auth/signup)
1. Create new Product-Market Fit survey at [app.formbricks.com](http://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
Then, create a new Product-Market Fit Survey:
<Callout title="Formbricks Widget running?" type="note">
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)
</Callout>
<Image src={NewPMF} alt="create pmf survey" quality="100" />
### 1. Create new PMF survey
Go to the "Setup instructions" tab and locate your survey ID. You'll need it in a minute:
If you don't have an account yet, create one at [app.formbricks.com](https://app.formbricks.com/auth/signup)
<Image src={ID} alt="copy survey id" quality="100" />
Click on "Create Survey" and choose one of the PMF survey templates. The first one is rather short, the latter builds on the ["Product-Market Fit Engine"](https://review.firstround.com/how-superhuman-built-an-engine-to-find-product-market-fit) developed by Superhuman:
### 2. Embed Formbricks snippet in `<head>`
<Image src={CreateSurvey} alt="Create survey by template" quality="100" className="rounded-lg" />
Embed the following Product-Market Fit Survey script in your HTML `<head>` tag.
### 2. Update questions (if you like)
Replace the `formId` with survey Id from the Formbricks dashboard:
Youre free to update the question and answer options. However, based on our experience, we suggest giving the provided template a go 😊 Here is a very [detailed description](https://coda.io/@rahulvohra/superhuman-product-market-fit-engine) of what to do with the data youre collecting.
```tsx
<script src="https://cdn.jsdelivr.net/npm/@formbricks/pmf@0.1" defer></script>
<Image src={ChangeText} alt="Change text content" quality="100" className="rounded-lg" />
<script>
window.formbricksPmf = {
...window.formbricksPmf,
config: {
formbricksUrl: "https://app.formbricks.com",
formId: "SURVEY ID HERE", // paste your survey ID here
containerId: "formbricks-pmf", // required to render survey in your page
},
};
</script>
```
_Want to change the button color? You can do so in the product settings!_
All you have to do now is assigning the `containerId` to the div where you want to render your survey (detailed instructions linked at the bottom):
Save, and move over to where the magic happens: The “Audience” tab.
```html
<div id="formbricks-pmf"></div>
```
### 3. Pre-segment your audience (coming soon)
### 3. Configure survey
<Callout title="Filter by attribute coming soon" type="note">
We're working on pre-segmenting users by attributes. We will update this manual in the next days.
</Callout>
**Sending user metadata with submission**
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:
The Product-Market Fit Survey is built for in-app experiences. We assume that you already have user properties stored in a session object. It makes sense to send them to Formbricks to enrich the user profile in the user view. Later on, you will be able to create cohorts to survey based on user properties.
**Filter by attribute**: You can keep the logic to decide if a user has (or has not) experienced value in your application. This makes most sense if you want to use historic usage data to decide if a user qualifies or not. Create your logic and if it applies, send an attribute to Formbricks by e.g. `formbricks.setAttribute("Loyalty", "Experienced Value");` Here is the full manual on how to [set attributes](/docs/attributes/custom-attributes).
Here is an example of how to take metadata from the next.js Session Object and pass it to Formbricks:
**Filter by actions (coming soon)**: Later, you can also segment users based on events tracked with Formbricks. However, this makes it impossible to use historic usage data (pre Formbricks usage). Here we will have a few options to achieve that:
```javascript
window.formbricksPmf = {
...window.formbricksPmf,
config: {
// ...
customer: {
email: "", // fill dynamically
name: "", // fill dynamically
},
};
},
```
- Check the time passed since sign-up (e.g. signed up 4 weeks ago)
- User has performed a specific action a certain number of times or (e.g. created 5 reports)
- User has performed a combination of actions (e.g. created a report **and** invited a team member)
Note: the `email` field must be present in the customer object
This way you make sure that you separate potentially misleading opinions from valuable insights.
**Styling**
### 4. Set up a trigger for the Product-Market Fit survey:
You can style the Product-Market Fit Survey to match your UI. We recommend to at least replace the brand color with your main color.
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:
```javascript
window.formbricksPmf = {
...window.formbricksPmf,
config: {
// ...
style: {
brandColor: "#00c4b8",
},
};
},
```
<div className="grid grid-cols-2 space-x-2">
<Image src={ActionCSS} alt="Add CSS action" quality="100" className="rounded-lg" />
<Image src={ActionPageurl} alt="Add inner text action" quality="100" className="rounded-lg" />
</div>
Here are all variables you can set with the current defaults:
### 5. Select Action in the “When to ask” card
```javascript
style: {
brandColor: "#00c4b8",
borderRadius: "0.4rem",
containerBgColor: "#f8fafc",
textColor: "#0f172a",
buttonTextColor: "#ffffff",
textareaBorderColor: "#e2e8f0",
},
```
<Image src={SelectAction} alt="Select PMF trigger button action" quality="100" className="rounded-lg" />
### Example config
### 6. Last step: Set Recontact Options correctly
Here is an example of a full config object:
Lastly, scroll down to “Recontact Options”. Here you have to choose the correct settings to make sure your data remains of high quality. You want to make sure that this survey is only responded to once per user. It is up to you to decide if you want to display it several times until the user responds:
```javascript
window.formbricksPmf = {
...window.formbricksPmf,
config: {
formbricksUrl: "https://app.formbricks.com",
formId: "cldetkpre0000nr0hku986hio",
containerId: "formbricks-pmf-survey", // always needed
customer: {
id: "", // fill dynamically
name: "", // fill dynamically
email: "", // fill dynamically
},
style: {
brandColor: "#00c4b8",
borderRadius: "0.4rem",
containerBgColor: "#f8fafc",
textColor: "#0f172a",
buttonTextColor: "#ffffff",
textareaBorderColor: "#e2e8f0",
},
},
};
```
<Image src={RecontactOptions} alt="Set recontact options" quality="100" className="rounded-lg" />
### 4. Render survey in your app
### 7. Congrats! Youre ready to publish your survey 💃
To add the Product-Market Fit Survey to your UI, you can use different wrappers. Please follow the instructions linked below:
<Image src={Publish} alt="Publish survey" quality="100" className="rounded-lg" />
1. <Link href="/docs/wrappers/modal">Modal</Link>
2. <Link href="/docs/wrappers/inline">Inline</Link>
<Callout title="Formbricks Widget running?" type="warning">
You need to have the Formbricks Widget installed to display the PMF Survey in your app. Please follow [this
tutorial (Step 4 onwards)](/docs/getting-started/quickstart) to install the widget.
</Callout>
###
# Get those insights!
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

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