Compare commits
119 Commits
v2.5.0
...
ai-fix-tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1d4ddb203 | ||
|
|
7337e94538 | ||
|
|
f55df95f8c | ||
|
|
dc894bae2a | ||
|
|
ea4941fe98 | ||
|
|
126a4c8989 | ||
|
|
add567ffdc | ||
|
|
1c10679b71 | ||
|
|
5e17b7919f | ||
|
|
4f5ac2b27f | ||
|
|
0b9d76e243 | ||
|
|
4d225818a6 | ||
|
|
f60ae69ed1 | ||
|
|
897c2de656 | ||
|
|
5c802c2fe8 | ||
|
|
e583aa38ba | ||
|
|
57927f6a3e | ||
|
|
4ffbbad9fe | ||
|
|
d47944ed21 | ||
|
|
a8e1fce3b7 | ||
|
|
7221a12964 | ||
|
|
27fc47144e | ||
|
|
706fa8a04e | ||
|
|
11748cbeba | ||
|
|
0f50c9690c | ||
|
|
316e5f15b0 | ||
|
|
18719746ed | ||
|
|
41fb76e4ff | ||
|
|
24d1f23421 | ||
|
|
6a691e2b68 | ||
|
|
354ec1b887 | ||
|
|
87c584add8 | ||
|
|
5035e3db9d | ||
|
|
b7f4097508 | ||
|
|
26591d9b9f | ||
|
|
655b67c3ad | ||
|
|
2dbd7111a9 | ||
|
|
06ddee42a9 | ||
|
|
47aa84bf8a | ||
|
|
2f7a59817a | ||
|
|
e7a0228bfa | ||
|
|
2367313ff2 | ||
|
|
68e52954e2 | ||
|
|
59ebde49cf | ||
|
|
6ab2560432 | ||
|
|
861d399025 | ||
|
|
fc3886fafa | ||
|
|
ddf7ad8475 | ||
|
|
ad6d5d6c00 | ||
|
|
315aaac395 | ||
|
|
01ceaa13ec | ||
|
|
25774f6f08 | ||
|
|
9a98772210 | ||
|
|
59a29dd3d6 | ||
|
|
e4fceb2e5e | ||
|
|
0b553447e0 | ||
|
|
6b64367d99 | ||
|
|
fe9746ba67 | ||
|
|
e4009d5951 | ||
|
|
b1ed61c247 | ||
|
|
10255aa102 | ||
|
|
774c6f19a5 | ||
|
|
ebf35ea582 | ||
|
|
f13efc954e | ||
|
|
9ee052a229 | ||
|
|
152fbede90 | ||
|
|
c9b8ffa9ef | ||
|
|
04e16d44a1 | ||
|
|
29131f93c2 | ||
|
|
1e2fe7b066 | ||
|
|
426a0a3847 | ||
|
|
80ef504bef | ||
|
|
6ab83e25d3 | ||
|
|
688dc25990 | ||
|
|
c53b58c64f | ||
|
|
61d18edb5d | ||
|
|
07b5dfe28a | ||
|
|
f55cad0121 | ||
|
|
494299cd89 | ||
|
|
7eca969496 | ||
|
|
5cc071e5a8 | ||
|
|
0532f2744b | ||
|
|
43ea26a33a | ||
|
|
ec54e40a8b | ||
|
|
96292130a8 | ||
|
|
4b508f02e3 | ||
|
|
eec7e1b62a | ||
|
|
39e87eb8d3 | ||
|
|
780115ffb8 | ||
|
|
c7c4ba6e49 | ||
|
|
b9a7edf1f5 | ||
|
|
86bf2accc9 | ||
|
|
954c435404 | ||
|
|
538c1bd809 | ||
|
|
e0767881f2 | ||
|
|
7d0cbad326 | ||
|
|
0ce7703ab8 | ||
|
|
4362fdf35a | ||
|
|
4bcca2daf4 | ||
|
|
8891000c64 | ||
|
|
b23088bd2f | ||
|
|
470151d79b | ||
|
|
df054537ee | ||
|
|
12f721982f | ||
|
|
9c6aaf5365 | ||
|
|
519f7838c6 | ||
|
|
32d870b063 | ||
|
|
81738b77f5 | ||
|
|
5e9df605e4 | ||
|
|
590f9305b8 | ||
|
|
a69df3baf0 | ||
|
|
da124bbbbe | ||
|
|
539e0e2fd3 | ||
|
|
9ffd6d4121 | ||
|
|
cc9ea82e5c | ||
|
|
14e3bb07ec | ||
|
|
ab1fe677d9 | ||
|
|
703260b906 | ||
|
|
7a24badff1 |
@@ -180,3 +180,9 @@ UNSPLASH_ACCESS_KEY=
|
||||
|
||||
# Disable custom cache handler if necessary (e.g. if deployed on Vercel)
|
||||
# CUSTOM_CACHE_DISABLED=1
|
||||
|
||||
# Azure AI settings
|
||||
# AI_AZURE_RESSOURCE_NAME=
|
||||
# AI_AZURE_API_KEY=
|
||||
# AI_AZURE_EMBEDDINGS_DEPLOYMENT_ID=
|
||||
# AI_AZURE_LLM_DEPLOYMENT_ID=
|
||||
33
.github/ISSUE_TEMPLATE/oss-gg-hack-submission.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: oss.gg hack submission 🕹️
|
||||
description: "Submit your contribution for the for the oss.gg hackathon"
|
||||
title: "[🕹️]"
|
||||
labels: 🕹️ oss.gg, player submission, hacktoberfest
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: contribution-name
|
||||
attributes:
|
||||
label: What side quest or challenge are you solving?
|
||||
description: Add the name of the side quest or challenge.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: points
|
||||
attributes:
|
||||
label: Points
|
||||
description: How many points are assigned to this contribution?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: What's the task your performed?
|
||||
validations:
|
||||
- type: textarea
|
||||
id: proof
|
||||
attributes:
|
||||
label: Provide proof that you've completed the task
|
||||
description: Screenshots, loom recordings, links to the content you shared or interacted with.
|
||||
validations:
|
||||
required: true
|
||||
14
.github/workflows/semantic-pull-requests.yml
vendored
@@ -20,6 +20,20 @@ jobs:
|
||||
id: lint_pr_title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
types: |
|
||||
fix
|
||||
feat
|
||||
chore
|
||||
docs
|
||||
style
|
||||
refactor
|
||||
perf
|
||||
test
|
||||
build
|
||||
ci
|
||||
revert
|
||||
ossgg
|
||||
|
||||
- uses: marocchino/sticky-pull-request-comment@v2
|
||||
# When the previous steps fails, the workflow would stop. By adding this
|
||||
|
||||
4
LICENSE
@@ -1,9 +1,9 @@
|
||||
Copyright (c) 2023 Formbricks GmbH
|
||||
Copyright (c) 2024 Formbricks GmbH
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- All content that resides under the "packages/ee/" directory of this repository, if that directory exists, is licensed under the license defined in "packages/ee/LICENSE".
|
||||
- All content that resides under the "packages/js/", "packages/errors/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
|
||||
- All content that resides under the "packages/js/", "packages/react-native/" and "packages/api/" directories of this repository, if that directories exist, is licensed under the "MIT" license as defined in the "LICENSE" files of these packages.
|
||||
- All third party components incorporated into the Formbricks Software are licensed under the original license provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function App(): JSX.Element {
|
||||
const config = {
|
||||
environmentId: process.env.EXPO_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
|
||||
apiHost: process.env.EXPO_PUBLIC_API_HOST,
|
||||
userId: "random user id",
|
||||
userId: "random-user-id",
|
||||
attributes: {
|
||||
language: "en",
|
||||
testAttr: "attr-test",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AppProps } from "next/app";
|
||||
import Head from "next/head";
|
||||
import "../styles/globals.css";
|
||||
import "@formbricks/ui/globals.css";
|
||||
|
||||
const App = ({ Component, pageProps }: AppProps) => {
|
||||
return (
|
||||
|
||||
@@ -60,7 +60,7 @@ const AppPage = ({}) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
|
||||
<div className="min-h-screen bg-white px-12 py-6 dark:bg-slate-800">
|
||||
<div className="flex flex-col justify-between md:flex-row">
|
||||
<div className="flex flex-col items-center gap-2 sm:flex-row">
|
||||
<SurveySwitch value="app" formbricks={formbricks} />
|
||||
@@ -117,7 +117,7 @@ const AppPage = ({}) => {
|
||||
</div>
|
||||
|
||||
<div className="md:grid md:grid-cols-3">
|
||||
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-800">
|
||||
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
|
||||
<h3 className="text-lg font-semibold dark:text-white">
|
||||
Reset person / pull data from Formbricks app
|
||||
</h3>
|
||||
|
||||
@@ -56,8 +56,8 @@ const AppPage = ({}) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
|
||||
<div className="flex flex-col items-center justify-between md:flex-row">
|
||||
<div className="min-h-screen bg-white px-12 py-6 dark:bg-slate-800">
|
||||
<div className="flex flex-col justify-between md:flex-row">
|
||||
<div className="flex flex-col items-center gap-2 sm:flex-row">
|
||||
<SurveySwitch value="website" formbricks={formbricks} />
|
||||
<div>
|
||||
@@ -113,7 +113,7 @@ const AppPage = ({}) => {
|
||||
</div>
|
||||
|
||||
<div className="md:grid md:grid-cols-3">
|
||||
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-800">
|
||||
<div className="col-span-3 self-start rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
|
||||
<h3 className="text-lg font-semibold dark:text-white">
|
||||
Reset person / pull data from Formbricks app
|
||||
</h3>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Example on overriding packages/js colors */
|
||||
.dark {
|
||||
--fb-brand-color: red;
|
||||
--fb-brand-text-color: white;
|
||||
--fb-border-color: green;
|
||||
--fb-border-color-highlight: var(--slate-500);
|
||||
--fb-focus-color: red;
|
||||
--fb-heading-color: yellow;
|
||||
--fb-subheading-color: green;
|
||||
--fb-info-text-color: orange;
|
||||
--fb-signature-text-color: blue;
|
||||
--fb-survey-background-color: black;
|
||||
--fb-accent-background-color: rgb(13, 13, 12);
|
||||
--fb-accent-background-color-selected: red;
|
||||
--fb-placeholder-color: white;
|
||||
--fb-shadow-color: yellow;
|
||||
--fb-rating-fill: var(--yellow-300);
|
||||
--fb-rating-hover: var(--yellow-500);
|
||||
--fb-back-btn-border: currentColor;
|
||||
--fb-submit-btn-border: transparent;
|
||||
--fb-rating-selected: black;
|
||||
}
|
||||
@@ -4,25 +4,20 @@ import I1 from "./images/I1.webp";
|
||||
import I2 from "./images/I2.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Using Actions in Formbricks | Fine-tuning User Moments",
|
||||
title: "Using Actions in Formbricks",
|
||||
description:
|
||||
"Dive deep into how actions in Formbricks help products and organizations to engage users at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine user targeting and generate richer, more detailed user insights.",
|
||||
};
|
||||
|
||||
#### App Surveys
|
||||
|
||||
# Actions & Targeting
|
||||
# Actions
|
||||
|
||||
Understanding user thoughts and feelings at critical moments in their journey is pivotal. To achieve this, Formbricks uses user-centric actions that trigger surveys at precisely the right time. Actions are essentially notifications sent from your application to Formbricks when predefined user activities occur, making it possible to gather insights during key interactions.
|
||||
|
||||
<Note>
|
||||
Ensure that you’ve **initialized Formbricks with a userId** to fully utilize this feature along with other
|
||||
app survey capabilities.
|
||||
</Note>
|
||||
Actions are predefined events within your app that prompt Formbricks to display a survey when triggered. These are detected by the Formbricks widget, which then presents the appropriate survey based on your predefined settings.
|
||||
|
||||
## **How Do Actions Work?**
|
||||
|
||||
Actions in Formbricks App Surveys are deeply integrated with user activities within your app. When a user performs a specified action, the Formbricks widget detects this activity and can present a survey to that specific user if the trigger conditions match of that survey, while also recording the event. This capability ensures that surveys are not only triggered at the right time but are also tailored to the user’s recent interactions within the app. You can set up these actions through a user-friendly No-Code interface within the Formbricks dashboard.
|
||||
Actions in Formbricks App Surveys are deeply integrated with user activities within your app. When a user performs a specified action, the Formbricks widget detects this activity and can present a survey to that specific user if the trigger conditions match for that survey. This capability ensures that surveys are triggered at the right time. You can set up these actions through a user-friendly No-Code interface within the Formbricks dashboard.
|
||||
|
||||
## **Why Are Actions Useful?**
|
||||
|
||||
@@ -30,8 +25,7 @@ Actions are invaluable for enhancing survey relevance and effectiveness:
|
||||
|
||||
- **Personalized Engagement**: Surveys triggered by user actions ensure content is highly relevant and engaging, matching each user’s current context.
|
||||
- **User Attributes**: By tying surveys to specific user attributes, such as activity levels or feature usage, you can customize the survey experience to reflect individual user profiles.
|
||||
- **User Segments**: Analyze action data to create detailed user segments, targeting specific groups with surveys that are pertinent to their behaviors or interactions within the app.
|
||||
- **User Targeting**: Precise targeting based on user actions and attributes ensures that surveys are shown only to users who meet certain criteria, enhancing the relevance and effectiveness of each survey.
|
||||
- **User Targeting**: Precise targeting based on user attributes ensures that surveys are shown only to users who meet certain criteria, enhancing the relevance and effectiveness of each survey.
|
||||
|
||||
## **Setting Up No-Code Actions**
|
||||
|
||||
@@ -127,5 +121,3 @@ return <button onClick={handleClick}>Click Me</button>;
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
This documentation frames actions around user interactions, emphasizing the connection between the user's activities and the survey experience. By leveraging user-centric actions, you can create highly targeted and timely surveys that resonate with users and yield valuable insights.
|
||||
|
||||
@@ -1,40 +1,18 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import { ResponsiveVideo } from "@/components/ResponsiveVideo";
|
||||
|
||||
import GermansGpt from "./germans-gpt.webp";
|
||||
import Hni from "./hni.webp";
|
||||
import PowerUsers from "./power-users.webp";
|
||||
import RideHailing from "./ride-hailing.webp";
|
||||
import UpsellMiro from "./upsell-miro.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Advanced Targeting in Surveys | Formbricks",
|
||||
title: "Advanced Targeting for App Surveys | Formbricks",
|
||||
description:
|
||||
"Advanced Targeting allows you to show surveys to just the right group of people. You can target surveys based on user attributes, user events, metadata , literally anything! This helps you get more relevant feedback and make data-driven decisions. All of this without writing a single line of code.",
|
||||
"Advanced Targeting allows you to show surveys to just the right group of people. You can target surveys based on user attributes, metadata, and other segments. This helps you get more relevant feedback and make data-driven decisions.",
|
||||
};
|
||||
|
||||
#### App Surveys
|
||||
|
||||
# Advanced Targeting
|
||||
|
||||
<Note>
|
||||
Targeting based on actions is deprecated in Advanced Targeting and will be removed soon. We recommend using
|
||||
filters on user attributes to target the survey only to specific groups of users.
|
||||
</Note>
|
||||
|
||||
Advanced Targeting allows you to show surveys to the right group of people. You can target surveys based on user attributes, device type, and more instead of spraying and praying. This helps you get more relevant feedback and make data-driven decisions. All of this without writing a single line of code.
|
||||
|
||||
<ResponsiveVideo
|
||||
title="Formbricks Multi-language Surveys"
|
||||
src="https://www.youtube-nocookie.com/embed/0BQp6N4cXzU?si=KeBM7G7Ch1xtrsOm&controls=0"
|
||||
/>
|
||||
# How to setup Advanced Targeting
|
||||
|
||||
## How to setup Advanced Targeting
|
||||
|
||||
<Note>
|
||||
Advanced Targeting is available on the Pro plan! Don't worry, you just need to enter your credit card
|
||||
details to start the freemium plan.
|
||||
</Note>
|
||||
<Note>Advanced Targeting is only available on the Pro plan!</Note>
|
||||
|
||||
1. On the Formbricks dashboard, click on **People** tab from the top navigation bar.
|
||||
|
||||
@@ -42,7 +20,7 @@ Advanced Targeting allows you to show surveys to the right group of people. You
|
||||
|
||||
3. Give your segment a title & a description to help you remember what this segment is about.
|
||||
|
||||
4. Now click on the **Add Filter** button to add a filter. You can filter based on actions, user attributes, other segments, devices, and more.
|
||||
4. Now click on the **Add Filter** button to add a filter. You can filter based on user attributes, other segments, devices, and more.
|
||||
|
||||
5. To group a set of filters together, click on the Three Dots icon on the right side of the filter and click on **Create Group**.
|
||||
|
||||
@@ -51,50 +29,3 @@ Advanced Targeting allows you to show surveys to the right group of people. You
|
||||
7. Once you are happy with the segment, click on **Save Segment**.
|
||||
|
||||
8. Now, when you create a survey, you can select this segment to target your survey to.
|
||||
|
||||
## Examples:
|
||||
|
||||
1. Let's say you want to upsell to: Miro, Loom, Figma, Slack and Asana.
|
||||
|
||||
<MdxImage
|
||||
src={UpsellMiro}
|
||||
alt="Upselling Opportunity"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. Post-experience surveying for a ride hailing app where users who have taken more than 1 ride are shown a survey.
|
||||
|
||||
<MdxImage
|
||||
src={RideHailing}
|
||||
alt="Ride Hailing Targeting"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. Target High Value users who have $100k+ in their bank account, own 20+ stocks, and have are an active user.
|
||||
|
||||
<MdxImage
|
||||
src={Hni}
|
||||
alt="Target Active High Net Worth Individuals"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
4. Target Germans on mobile phones who have regenerated chatGPT answers frequently in the last quarter and did so today.
|
||||
|
||||
<MdxImage
|
||||
src={GermansGpt}
|
||||
alt="Target Germans on Mobile Phones who have regenerated chatGPT answers frequently in the last quarter and did so today"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
5. Sneak Peak: How we at Formbricks automate inviting power users to chat with us
|
||||
|
||||
<MdxImage
|
||||
src={PowerUsers}
|
||||
alt="Automate inviting power users to chat with us at Formbricks"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
@@ -129,10 +129,8 @@ Locate that file. We are using the [Tailwind Template “Syntax”](https://tail
|
||||
```tsx
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui/Popover";
|
||||
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui/components/Popover";
|
||||
import { handleFeedbackSubmit, updateFeedback } from "../../lib/handleFeedbackSubmit";
|
||||
|
||||
export const DocsFeedback = () => {
|
||||
|
||||
@@ -37,8 +37,9 @@ To display the Product-Market Fit survey in your app you want to proceed as foll
|
||||
3. Setup the user action to display survey at good point in time
|
||||
|
||||
<Note>
|
||||
## Formbricks Widget running?
|
||||
We assume that you have already installed the Formbricks Widget in your web app. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide (15mins).](/app-surveys/quickstart)
|
||||
## Formbricks Widget running? We assume that you have already installed the Formbricks Widget in your web
|
||||
app. It’s required to display messages and surveys in your app. If not, please follow the [Quick Start Guide
|
||||
(15mins).](/app-surveys/quickstart)
|
||||
</Note>
|
||||
|
||||
### 1. Create new PMF survey
|
||||
@@ -69,25 +70,12 @@ _Want to change the button color? You can do so in the product settings!_
|
||||
|
||||
Save, and move over to where the magic happens: The “Audience” tab.
|
||||
|
||||
### 3. Pre-segment your audience (coming soon)
|
||||
|
||||
<Note>
|
||||
## Filter by attribute coming soon We're working on pre-segmenting users by attributes. We will update this
|
||||
manual in the next days.
|
||||
</Note>
|
||||
### 3. Pre-segment your audience
|
||||
|
||||
To run this survey properly, you should pre-segment your user base. As touched upon earlier: if you ask every user you’ll get lots of opinions which are often misleading. You only want to gather feedback from people who invested the time to get to know and use your product:
|
||||
|
||||
**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](/app-surveys/user-identification).
|
||||
|
||||
**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:
|
||||
|
||||
- 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 organization member)
|
||||
|
||||
This way you make sure that you separate potentially misleading opinions from valuable insights.
|
||||
|
||||
### 4. Set up a trigger for the Product-Market Fit survey:
|
||||
|
||||
You need a trigger to display the survey but in this case, the filtering does all the work. It’s up to you to decide to display the survey after the user viewed a specific subpage (pageURL) or after clicking an element. Have a look at the [Actions manual](/app-surveys/actions/) if you are not sure how to set them up:
|
||||
|
||||
@@ -79,28 +79,6 @@ Promise<{ id: string }, NetworkError | Error>
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
- Update Display
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Update Display">
|
||||
|
||||
```javascript {{ title: 'Update Display Method Call'}}
|
||||
await api.client.display.update(
|
||||
displayId: "<your-display-id>",
|
||||
{
|
||||
userId: "<your-user-id>", // optional
|
||||
responseId: "<your-response-id>", // optional
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
```javascript {{ title: 'Update Display Method Return Type' }}
|
||||
Promise<{ }, NetworkError | Error]>
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
## Responses
|
||||
|
||||
- Create Response
|
||||
@@ -173,29 +151,6 @@ Promise<{ }, NetworkError | Error]>
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
## Action
|
||||
|
||||
- Create Action:
|
||||
|
||||
<Note> An environment cannot have 2 actions with the same name. </Note>
|
||||
|
||||
<Col>
|
||||
<CodeGroup title="Create Action">
|
||||
|
||||
```javascript {{ title: 'Create Action Method Call'}}
|
||||
await api.client.action.create({
|
||||
name: "<your-action-name>", // required
|
||||
userId: "<your-user-id>", // required
|
||||
});
|
||||
```
|
||||
|
||||
```javascript {{ title: 'Create Action Method Return Type' }}
|
||||
Promise<{ }, NetworkError | Error]>
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
|
||||
## Attribute
|
||||
|
||||
- Update Attribute
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@formbricks/ui/Accordion";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@formbricks/ui/components/Accordion";
|
||||
import { FaqJsonLdComponent } from "./FAQPageJsonLd";
|
||||
|
||||
const FAQ_DATA = [
|
||||
|
||||
@@ -14,10 +14,6 @@ export const metadata = {
|
||||
|
||||
The Formbricks React Native SDK can be used for seamlessly integrating App Surveys into your React Native Apps. Here, w'll explore how to leverage the SDK for in app surveys. The SDK is [available on npm.](https://www.npmjs.com/package/@formbricks/react-native)
|
||||
|
||||
<Note>
|
||||
Our React Native SDK is now in public beta. We're happy to **offer you a free 3-month trial for our Scale plan** if you're providing feedback on your developer experience. Reach out on Discord and shoot Johannes a message to get the extended trial started.
|
||||
</Note>
|
||||
|
||||
### Install
|
||||
|
||||
<Col>
|
||||
|
||||
@@ -6,7 +6,7 @@ import ApiKeySecret from "./images/api-key-secret.webp";
|
||||
export const metadata = {
|
||||
title: "Formbricks API Overview: Public Client & Management API Breakdown",
|
||||
description:
|
||||
"Formbricks provides a powerful API to manage your surveys, responses, users, displays, actions, attributes & webhooks programmatically. Get a detailed understanding of Formbricks' dual API offerings: the unauthenticated Public Client API optimized for client-side tasks and the secured Management API for advanced account operations. Choose the perfect fit for your integration needs and ensure robust data handling",
|
||||
"Formbricks provides a powerful API to manage your surveys, responses, users, displays, attributes & webhooks programmatically. Get a detailed understanding of Formbricks' dual API offerings: the unauthenticated Public Client API optimized for client-side tasks and the secured Management API for advanced account operations. Choose the perfect fit for your integration needs and ensure robust data handling",
|
||||
};
|
||||
|
||||
#### API
|
||||
@@ -23,7 +23,6 @@ The [Public Client API](https://documenter.getpostman.com/view/11026000/2sA3Bq5X
|
||||
|
||||
We currently have the following Client API methods exposed and below is their documentation attached in Postman:
|
||||
|
||||
- [Actions API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#b8f3a10e-1642-4d82-a629-fef0a8c6c86c) - Create actions for a Person
|
||||
- [Displays API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#949272bf-daec-4d72-9b52-47af3d74a62c) - Mark Survey as Displayed or Update an existing Display by linking it with a Response for a Person
|
||||
- [People API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#ee3d2188-4253-4bca-9238-6b76455805a9) - Create & Update a Person (e.g. attributes, email, userId, etc)
|
||||
- [Responses API](https://documenter.getpostman.com/view/11026000/2sA3Bq5XEh#8c773032-536c-483c-a237-c7697347946e) - Create & Update a Response for a Survey
|
||||
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/docs/app/global/logic-editor/images/action-calculate.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
apps/docs/app/global/logic-editor/images/action-jump.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
apps/docs/app/global/logic-editor/images/action-options.webp
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
apps/docs/app/global/logic-editor/images/action-require.webp
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/docs/app/global/logic-editor/images/add-logic.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/docs/app/global/logic-editor/images/condition-chaining.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/docs/app/global/logic-editor/images/condition-options.webp
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
apps/docs/app/global/logic-editor/images/condition-value.webp
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/docs/app/global/logic-editor/images/conditions.webp
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
apps/docs/app/global/logic-editor/images/editor.webp
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
apps/docs/app/global/logic-editor/images/question-logic.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
171
apps/docs/app/global/logic-editor/page.mdx
Normal file
@@ -0,0 +1,171 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
import ActionCalculateOperators from "./images/action-calculate-operators.webp";
|
||||
import ActionCalculateValue from "./images/action-calculate-value.webp";
|
||||
import ActionCalculateVariables from "./images/action-calculate-variables.webp";
|
||||
import ActionCalculate from "./images/action-calculate.webp";
|
||||
import ActionJump from "./images/action-jump.webp";
|
||||
import ActionOptions from "./images/action-options.webp";
|
||||
import ActionRequire from "./images/action-require.webp";
|
||||
import AddLogic from "./images/add-logic.webp";
|
||||
import ConditionChaining from "./images/condition-chaining.webp";
|
||||
import ConditionOperators from "./images/condition-operators.webp";
|
||||
import ConditionOptions from "./images/condition-options.webp";
|
||||
import ConditionValue from "./images/condition-value.webp";
|
||||
import Conditions from "./images/conditions.webp";
|
||||
import Editor from "./images/editor.webp";
|
||||
import QuestionLogic from "./images/question-logic.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Logic Editor",
|
||||
description:
|
||||
"Create complex survey logic with the Logic Editor. Use conditions, actions, and variables to create a personalized survey experience.",
|
||||
};
|
||||
|
||||
# Logic Editor
|
||||
|
||||
Create complex survey logic with the Logic Editor. Use conditions, actions, and variables to create a personalized survey experience.
|
||||
|
||||
<MdxImage src={Editor} alt="Logic Editor" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
|
||||
|
||||
## Terminology
|
||||
|
||||
- **Condition**: A rule that determines when an action should be executed.
|
||||
- **Action**: A task that is executed when a condition is met.
|
||||
|
||||
## **Creating Logic**
|
||||
|
||||
1. **Add a Logic Block**: Click the `Add logic +` button to add a new logic block.
|
||||
|
||||
<MdxImage src={AddLogic} alt="Add Logic" quality="100" className="max-w-full rounded-lg sm:max-w-3xl" />
|
||||
<Note>
|
||||
You can add multiple logic blocks to a survey. Logic blocks are executed in the order they are added. You
|
||||
can rearrange the order of logic blocks.
|
||||
</Note>
|
||||
|
||||
2. **Add Conditions**: Add conditions to the logic block. Conditions are rules that determine when an action should be executed.
|
||||
|
||||
<MdxImage
|
||||
src={Conditions}
|
||||
alt="Add Conditions"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Conditons can be based on:
|
||||
|
||||
- **Question**: The answer to a question.
|
||||
- **Variable**: A variable value.
|
||||
- **Hidden Field**: The value of a hidden field.
|
||||
|
||||
2.a **Condition Options**: Choose from a list of available conditions.
|
||||
|
||||
<MdxImage
|
||||
src={ConditionOptions}
|
||||
alt="Condition Options"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2.b **Condition Operators**: Choose an operator to compare the condition value.
|
||||
|
||||
<MdxImage
|
||||
src={ConditionOperators}
|
||||
alt="Condition Operators"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2.c **Condition Value**: Enter a value to compare the condition against.
|
||||
Comparisons can be made against a fixed value or a dynamic value.
|
||||
Dynamic values can be based on a question, variable, or hidden field.
|
||||
|
||||
<MdxImage
|
||||
src={ConditionValue}
|
||||
alt="Condition Value"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
<Note>
|
||||
- Conditions can be grouped. - Conditions can be combined using AND or OR operators. You can add multiple
|
||||
conditions to a logic block. Conditions are evaluated in the order they are added.
|
||||
</Note>
|
||||
|
||||
<MdxImage
|
||||
src={ConditionChaining}
|
||||
alt="Condition Chaining"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. **Add Actions**: Add actions to the logic block. Actions are tasks that are executed when a condition is met.
|
||||
|
||||
<Note>You can add multiple actions to a logic block. Actions are executed in the order they are added.</Note>
|
||||
|
||||
- 3.a **Action Options**: Choose from a list of available actions.
|
||||
|
||||
<MdxImage
|
||||
src={ActionOptions}
|
||||
alt="Add Actions"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Action is of the following types:
|
||||
|
||||
- **Calculate**: Perform a calculation. These variables are then available for use in other questions.
|
||||
|
||||
- Calculations can be performed on variables.
|
||||
- Calculations can be based on fixed values or dynamic values.
|
||||
<MdxImage
|
||||
src={ActionCalculateVariables}
|
||||
alt="Action Calculate Variables"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
<MdxImage
|
||||
src={ActionCalculateOperators}
|
||||
alt="Action Calculate Variables"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
<MdxImage
|
||||
src={ActionCalculateValue}
|
||||
alt="Action Calculate Variables"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
<MdxImage
|
||||
src={ActionCalculate}
|
||||
alt="Action Calculate"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
- **Require Answer**: Make a question required. Only the optional questions can be marked as required while filling the survey.
|
||||
<MdxImage
|
||||
src={ActionRequire}
|
||||
alt="Action Require"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
- **Jump to Question**: Skip to a specific question. The user will be redirected to the specified question based on the condition.
|
||||
<MdxImage
|
||||
src={ActionJump}
|
||||
alt="Action Jump"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
4. **Save Logic**: Click the `Save` button to save the logic block.
|
||||
|
||||
# Question Logic
|
||||
|
||||
This logic is executed when the user answers the question. Logic can be as simple as showing a follow-up question based on the answer or as complex as calculating a score based on multiple answers.
|
||||
|
||||
<MdxImage
|
||||
src={QuestionLogic}
|
||||
alt="Question Logic"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
75
apps/docs/app/global/shareable-dashboards/page.mdx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import StepOne from "./images/1-publish-to-web.webp";
|
||||
import StepTwo from "./images/2-warning-publish.webp";
|
||||
import StepThree from "./images/3-share-link.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Shareable Dashboards",
|
||||
description:
|
||||
"Create shareable links to dashboards of specific surveys.",
|
||||
};
|
||||
|
||||
# Shareable Dashboards
|
||||
|
||||
Formbricks allows you to create public, shareable versions of your survey results dashboards. This feature enables you to easily share survey results with stakeholders, team members, or the public without granting access to your Formbricks account.
|
||||
|
||||
## How To Publish Survey Results
|
||||
|
||||
1. **Go to survey summary**: Choose the survey for which you want to create a shareable dashboard and go to its summary page.
|
||||
|
||||
2. **Share results**: Click the "Share results" and then "Publish to web".
|
||||
|
||||
<MdxImage
|
||||
src={StepOne}
|
||||
alt="Go to survey summary"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
3. **Confirm**: Click "Publish to public web" (it's public).
|
||||
|
||||
<MdxImage
|
||||
src={StepTwo}
|
||||
alt="Go to survey summary"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
4. **Share link**: Formbricks has generated a unique URL for your public dashboard. Share it around.
|
||||
|
||||
<MdxImage
|
||||
src={StepThree}
|
||||
alt="Go to survey summary"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
<Note>Whoever has access to the link can access the survey results.</Note>
|
||||
|
||||
## How To Unpublish Survey Results
|
||||
|
||||
Unpublish is very simple: Go to "Share results" -> "Unpublish from web" -> "Unpublish".
|
||||
|
||||
<MdxImage
|
||||
src={StepThree}
|
||||
alt="Go to survey summary"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Read-only access**: Viewers can see survey results but cannot modify data or settings.
|
||||
- **Real-time updates**: The shared dashboard reflects current survey data in real-time.
|
||||
- **Filters included**: Visitors can access all filters to dissect the data.
|
||||
- **Revocable access**: You can disable the shared link at any time to restrict access.
|
||||
|
||||
## Use Cases
|
||||
|
||||
- Share results with clients or stakeholders
|
||||
- Publish survey findings to your website or blog
|
||||
- Collaborate with team members without sharing account credentials
|
||||
- Create transparency by making certain survey results public
|
||||
|
||||
Shareable dashboards provide a simple yet powerful way to disseminate survey insights while maintaining control over your Formbricks account and data.
|
||||
|
Before Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
@@ -1,15 +1,15 @@
|
||||
import { MdxImage } from "@/components/MdxImage";
|
||||
|
||||
import EntraIDAppReg01 from "./images/entra_app_reg_01.png";
|
||||
import EntraIDAppReg02 from "./images/entra_app_reg_02.png";
|
||||
import EntraIDAppReg03 from "./images/entra_app_reg_03.png";
|
||||
import EntraIDAppReg04 from "./images/entra_app_reg_04.png";
|
||||
import EntraIDAppReg05 from "./images/entra_app_reg_05.png";
|
||||
import EntraIDAppReg06 from "./images/entra_app_reg_06.png";
|
||||
import EntraIDAppReg07 from "./images/entra_app_reg_07.png";
|
||||
import EntraIDAppReg08 from "./images/entra_app_reg_08.png";
|
||||
import EntraIDAppReg09 from "./images/entra_app_reg_09.png";
|
||||
import EntraIDAppReg10 from "./images/entra_app_reg_10.png";
|
||||
import EntraIDAppReg01 from "./images/entra_app_reg_01.webp";
|
||||
import EntraIDAppReg02 from "./images/entra_app_reg_02.webp";
|
||||
import EntraIDAppReg03 from "./images/entra_app_reg_03.webp";
|
||||
import EntraIDAppReg04 from "./images/entra_app_reg_04.webp";
|
||||
import EntraIDAppReg05 from "./images/entra_app_reg_05.webp";
|
||||
import EntraIDAppReg06 from "./images/entra_app_reg_06.webp";
|
||||
import EntraIDAppReg07 from "./images/entra_app_reg_07.webp";
|
||||
import EntraIDAppReg08 from "./images/entra_app_reg_08.webp";
|
||||
import EntraIDAppReg09 from "./images/entra_app_reg_09.webp";
|
||||
import EntraIDAppReg10 from "./images/entra_app_reg_10.webp";
|
||||
|
||||
export const metadata = {
|
||||
title: "Configure Formbricks with External auth providers",
|
||||
@@ -38,7 +38,7 @@ These variables are present inside your machine’s docker-compose file. Restart
|
||||
| S3_SECRET_KEY | Secret key for S3. | optional | (resolved by the AWS SDK) |
|
||||
| S3_REGION | Region for S3. | optional | (resolved by the AWS SDK) |
|
||||
| S3_BUCKET_NAME | S3 bucket name for data storage. Formbricks enables S3 storage when this is set. | optional (required if S3 is enabled) | |
|
||||
| S3_ENDPOINT | Endpoint for S3. | optional | (resolved by the AWS SDK) |
|
||||
| S3_ENDPOINT_URL | Endpoint for S3. | optional | (resolved by the AWS SDK) |
|
||||
| PRIVACY_URL | URL for privacy policy. | optional | |
|
||||
| TERMS_URL | URL for terms of service. | optional | |
|
||||
| IMPRINT_URL | URL for imprint. | optional | |
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
@@ -11,11 +11,9 @@ export const metadata = {
|
||||
|
||||
#### Website Surveys
|
||||
|
||||
# Actions & Targeting
|
||||
# Actions
|
||||
|
||||
For public-facing websites, landing pages, and pages without user authentication walls, actions serve as effective triggers for displaying surveys. This method is particularly suitable for engaging general audiences, where **individual user tracking is not required or feasible**.
|
||||
|
||||
Actions in this context are straightforward triggers based on interactions with your website, allowing you to capture feedback precisely when it's most relevant.
|
||||
Actions are triggers based on interactions with your website, allowing you to capture feedback precisely when it's most relevant.
|
||||
|
||||
<Note>
|
||||
These actions operate **independently** as website surveys do not involve user identification. If you have
|
||||
@@ -46,7 +44,7 @@ Formbricks provides an intuitive No-Code interface for configuring actions, enab
|
||||
src={StepOne}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
2. Now click on “Add Action”
|
||||
@@ -55,7 +53,7 @@ Formbricks provides an intuitive No-Code interface for configuring actions, enab
|
||||
src={StepTwo}
|
||||
alt="Choose a link survey template"
|
||||
quality="100"
|
||||
className="max-w-full rounded-lg sm:max-w-3xl "
|
||||
className="max-w-full rounded-lg sm:max-w-3xl"
|
||||
/>
|
||||
|
||||
Here are four types of No-Code actions you can set up:
|
||||
@@ -1,15 +1,17 @@
|
||||
// ResponsiveVideo.js
|
||||
// ResponsiveVideo.tsx
|
||||
export const ResponsiveVideo = ({ src, title }) => {
|
||||
return (
|
||||
<div className="relative w-full overflow-hidden pt-[56.25%]">
|
||||
<iframe
|
||||
src={src}
|
||||
title={title}
|
||||
frameBorder="0"
|
||||
className="absolute left-0 top-0 h-full w-full"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen></iframe>
|
||||
<div className="max-w-[1280px]">
|
||||
<div className="relative w-full overflow-hidden pt-[56.25%]">
|
||||
<iframe
|
||||
src={src}
|
||||
title={title}
|
||||
frameBorder="0"
|
||||
className="absolute left-0 top-0 h-full w-full"
|
||||
referrerPolicy="strict-origin-when-cross-origin"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen></iframe>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,6 +44,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
|
||||
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
||||
{ title: "Partial Submissions", href: "/global/partial-submissions" }, // global
|
||||
{ title: "Shareable Dashboards", href: "/global/shareable-dashboards" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -56,7 +57,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{
|
||||
title: "Features",
|
||||
children: [
|
||||
{ title: "Actions & Targeting", href: "/website-surveys/actions-and-targeting" },
|
||||
{ title: "Actions", href: "/website-surveys/actions" },
|
||||
{ title: "Show Survey to % of users", href: "/global/show-survey-to-percent-of-users" }, // app and website
|
||||
{ title: "Recontact Options", href: "/app-surveys/recontact" },
|
||||
{ title: "Hidden Fields", href: "/global/hidden-fields" }, // global
|
||||
@@ -68,6 +69,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
|
||||
{ title: "Recall Functionality", href: "/global/recall" }, // global
|
||||
{ title: "Partial Submissions", href: "/global/partial-submissions" }, // global
|
||||
{ title: "Shareable Dashboards", href: "/global/shareable-dashboards" },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -91,6 +93,7 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "User Metadata", href: "/global/metadata" },
|
||||
{ title: "Custom Styling", href: "/global/overwrite-styling" }, // global
|
||||
{ title: "Conditional Logic", href: "/global/conditional-logic" },
|
||||
{ title: "Shareable Dashboards", href: "/global/shareable-dashboards" },
|
||||
{ title: "Start & End Dates", href: "/global/custom-start-end-conditions" },
|
||||
{ title: "Limit submissions", href: "/global/limit-submissions" }, // global
|
||||
{ title: "Recall Functionality", href: "/global/recall" },
|
||||
@@ -106,6 +109,7 @@ export const navigation: Array<NavGroup> = [
|
||||
links: [
|
||||
{ title: "Access Roles", href: "/global/access-roles" },
|
||||
{ title: "Styling Theme", href: "/global/styling-theme" },
|
||||
{ title: "Logic Editor", href: "/global/logic-editor" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -139,9 +143,9 @@ export const navigation: Array<NavGroup> = [
|
||||
{ title: "Zapier", href: "/developer-docs/integrations/zapier" },
|
||||
],
|
||||
},
|
||||
{ title: "SDK: React Native", href: "/developer-docs/react-native-in-app-surveys" },
|
||||
{ title: "SDK: Web Apps", href: "/developer-docs/app-survey-sdk" },
|
||||
{ title: "SDK: Public Websites", href: "/developer-docs/website-survey-sdk" },
|
||||
{ title: "SDK: React Native", href: "/developer-docs/react-native-in-app-surveys" },
|
||||
{ title: "SDK: Formbricks API", href: "/developer-docs/api-sdk" },
|
||||
{ title: "REST API", href: "/developer-docs/rest-api" },
|
||||
{ title: "Webhooks", href: "/developer-docs/webhooks" },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
import base from "../../packages/config-tailwind/tailwind.config";
|
||||
import base from "../../packages/ui/tailwind.config";
|
||||
|
||||
export default {
|
||||
...base,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import Dance from "@/images/onboarding-dance.gif";
|
||||
import Lost from "@/images/onboarding-lost.gif";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { OnboardingSetupInstructions } from "./OnboardingSetupInstructions";
|
||||
|
||||
interface ConnectWithFormbricksProps {
|
||||
@@ -62,20 +59,21 @@ export const ConnectWithFormbricks = ({
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-[30rem] w-1/2 flex-col items-center justify-center rounded-lg border bg-slate-200 text-center shadow",
|
||||
widgetSetupCompleted ? "border-green-500 bg-green-100" : ""
|
||||
"flex h-[30rem] w-1/2 flex-col items-center justify-center rounded-lg border text-center",
|
||||
widgetSetupCompleted ? "border-green-500 bg-green-100" : "border-slate-300 bg-slate-200"
|
||||
)}>
|
||||
{widgetSetupCompleted ? (
|
||||
<div>
|
||||
<Image src={Dance} alt="lost" height={250} />
|
||||
<p className="mt-6 text-xl font-bold">Connection successful ✅</p>
|
||||
<p className="text-3xl">Congrats!</p>
|
||||
<p className="pt-4 text-sm font-medium text-slate-600">Well done! We're connected.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<Image src={Lost} alt="lost" height={250} />
|
||||
<p className="animate-pulse pt-4 text-sm font-semibold text-slate-700">
|
||||
Waiting for your signal...
|
||||
</p>
|
||||
<div className="flex animate-pulse flex-col items-center space-y-4">
|
||||
<span className="relative flex h-10 w-10">
|
||||
<span className="animate-ping-slow absolute inline-flex h-full w-full rounded-full bg-slate-400 opacity-75"></span>
|
||||
<span className="relative inline-flex h-10 w-10 rounded-full bg-slate-500"></span>
|
||||
</span>
|
||||
<p className="pt-4 text-sm font-medium text-slate-600">Waiting for your signal...</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,9 @@ import { FormProvider, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
import { TOrganization } from "@formbricks/types/organizations";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@formbricks/ui/components/Form";
|
||||
import { Input } from "@formbricks/ui/components/Input";
|
||||
|
||||
interface InviteOrganizationMemberProps {
|
||||
organization: TOrganization;
|
||||
@@ -102,7 +102,7 @@ export const InviteOrganizationMember = ({ organization, environmentId }: Invite
|
||||
<div className="flex w-full justify-end space-x-2">
|
||||
<Button
|
||||
id="onboarding-inapp-invite-have-a-look-first"
|
||||
className="font-normal text-slate-400"
|
||||
className="text-slate-400"
|
||||
variant="minimal"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -4,10 +4,10 @@ import "prismjs/themes/prism.css";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { CodeBlock } from "@formbricks/ui/CodeBlock";
|
||||
import { TabBar } from "@formbricks/ui/TabBar";
|
||||
import { Html5Icon, NpmIcon } from "@formbricks/ui/icons";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { CodeBlock } from "@formbricks/ui/components/CodeBlock";
|
||||
import { TabBar } from "@formbricks/ui/components/TabBar";
|
||||
import { Html5Icon, NpmIcon } from "@formbricks/ui/components/icons";
|
||||
|
||||
const tabs = [
|
||||
{ id: "html", label: "HTML", icon: <Html5Icon /> },
|
||||
|
||||
@@ -5,8 +5,8 @@ import { notFound, redirect } from "next/navigation";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Header } from "@formbricks/ui/Header";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Header } from "@formbricks/ui/components/Header";
|
||||
|
||||
interface InvitePageProps {
|
||||
params: {
|
||||
@@ -33,7 +33,7 @@ const Page = async ({ params }: InvitePageProps) => {
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center">
|
||||
<Header
|
||||
title="Who is your favorite engineer?"
|
||||
subtitle="Invite your tech-savvy co-worker to help with the setup 🤓"
|
||||
subtitle="Invite your tech-savvy co-worker to help with the setup."
|
||||
/>
|
||||
<div className="space-y-4 text-center">
|
||||
<p className="text-4xl font-medium text-slate-800"></p>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { ConnectWithFormbricks } from "@/app/(app)/(onboarding)/environments/[environmentId]/connect/components/ConnectWithFormbricks";
|
||||
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Header } from "@formbricks/ui/Header";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Header } from "@formbricks/ui/components/Header";
|
||||
|
||||
interface ConnectPageProps {
|
||||
params: {
|
||||
@@ -26,20 +24,11 @@ const Page = async ({ params }: ConnectPageProps) => {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const channel = product.config.channel;
|
||||
const industry = product.config.industry;
|
||||
|
||||
if (!channel || !industry) {
|
||||
return notFound();
|
||||
}
|
||||
const customHeadline = getCustomHeadline(channel, industry);
|
||||
const channel = product.config.channel || null;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full flex-col items-center justify-center py-10">
|
||||
<Header
|
||||
title={`Let's connect your ${customHeadline} with Formbricks`}
|
||||
subtitle="It takes less than 4 minutes, pinky promise!"
|
||||
/>
|
||||
<Header title={`Let's connect your product with Formbricks`} subtitle="It takes less than 4 minutes." />
|
||||
<div className="space-y-4 text-center">
|
||||
<p className="text-4xl font-medium text-slate-800"></p>
|
||||
<p className="text-sm text-slate-500"></p>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { replacePresetPlaceholders } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/utils";
|
||||
import { XMTemplates } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/lib/xm-templates";
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { ActivityIcon, ShoppingCartIcon, SmileIcon, StarIcon, ThumbsUpIcon, UsersIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TSurveyCreateInput } from "@formbricks/types/surveys/types";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { createSurveyAction } from "@formbricks/ui/components/TemplateList/actions";
|
||||
|
||||
interface XMTemplateListProps {
|
||||
product: TProduct;
|
||||
user: TUser;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export const XMTemplateList = ({ product, user, environmentId }: XMTemplateListProps) => {
|
||||
const [activeTemplateId, setActiveTemplateId] = useState<number | null>(null);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const createSurvey = async (activeTemplate: TXMTemplate) => {
|
||||
const augmentedTemplate: TSurveyCreateInput = {
|
||||
...activeTemplate,
|
||||
type: "link",
|
||||
createdBy: user.id,
|
||||
};
|
||||
const createSurveyResponse = await createSurveyAction({
|
||||
environmentId: environmentId,
|
||||
surveyBody: augmentedTemplate,
|
||||
});
|
||||
|
||||
if (createSurveyResponse?.data) {
|
||||
router.push(`/environments/${environmentId}/surveys/${createSurveyResponse.data.id}/edit?mode=cx`);
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createSurveyResponse);
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTemplateClick = (templateIdx) => {
|
||||
setActiveTemplateId(templateIdx);
|
||||
const template = XMTemplates[templateIdx];
|
||||
const newTemplate = replacePresetPlaceholders(template, product);
|
||||
createSurvey(newTemplate);
|
||||
};
|
||||
|
||||
const XMTemplateOptions = [
|
||||
{
|
||||
title: "NPS",
|
||||
description: "Implement proven best practices to understand WHY people buy.",
|
||||
icon: ShoppingCartIcon,
|
||||
onClick: () => handleTemplateClick(0),
|
||||
isLoading: activeTemplateId === 0,
|
||||
},
|
||||
{
|
||||
title: "5-Star Rating",
|
||||
description: "Universal feedback solution to gauge overall satisfaction.",
|
||||
icon: StarIcon,
|
||||
onClick: () => handleTemplateClick(1),
|
||||
isLoading: activeTemplateId === 1,
|
||||
},
|
||||
{
|
||||
title: "CSAT",
|
||||
description: "Implement best practices to measure customer satisfaction.",
|
||||
icon: ThumbsUpIcon,
|
||||
onClick: () => handleTemplateClick(2),
|
||||
isLoading: activeTemplateId === 2,
|
||||
},
|
||||
{
|
||||
title: "CES",
|
||||
description: "Leverage every touchpoint to understand ease of customer interaction.",
|
||||
icon: ActivityIcon,
|
||||
onClick: () => handleTemplateClick(3),
|
||||
isLoading: activeTemplateId === 3,
|
||||
},
|
||||
{
|
||||
title: "Smileys",
|
||||
description: "Use visual indicators to capture feedback across customer touchpoints.",
|
||||
icon: SmileIcon,
|
||||
onClick: () => handleTemplateClick(4),
|
||||
isLoading: activeTemplateId === 4,
|
||||
},
|
||||
{
|
||||
title: "eNPS",
|
||||
description: "Universal feedback to understand employee engagement and satisfaction.",
|
||||
icon: UsersIcon,
|
||||
onClick: () => handleTemplateClick(5),
|
||||
isLoading: activeTemplateId === 5,
|
||||
},
|
||||
];
|
||||
|
||||
return <OnboardingOptionsContainer options={XMTemplateOptions} />;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { replaceQuestionPresetPlaceholders } from "@formbricks/lib/utils/templates";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
|
||||
// replace all occurences of productName with the actual product name in the current template
|
||||
export const replacePresetPlaceholders = (template: TXMTemplate, product: TProduct) => {
|
||||
const survey = structuredClone(template);
|
||||
survey.name = survey.name.replace("{{productName}}", product.name);
|
||||
survey.questions = survey.questions.map((question) => {
|
||||
return replaceQuestionPresetPlaceholders(question, product);
|
||||
});
|
||||
return { ...template, ...survey };
|
||||
};
|
||||
@@ -0,0 +1,406 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { getDefaultEndingCard } from "@formbricks/lib/templates";
|
||||
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
|
||||
import { TXMTemplate } from "@formbricks/types/templates";
|
||||
|
||||
export const XMSurveyDefault: TXMTemplate = {
|
||||
name: "",
|
||||
endings: [getDefaultEndingCard([])],
|
||||
questions: [],
|
||||
styling: {
|
||||
overwriteThemeStyling: true,
|
||||
},
|
||||
};
|
||||
|
||||
const NPSSurvey = (): TXMTemplate => {
|
||||
return {
|
||||
...XMSurveyDefault,
|
||||
name: "NPS Survey",
|
||||
questions: [
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.NPS,
|
||||
headline: { default: "How likely are you to recommend {{productName}} to a friend or colleague?" },
|
||||
required: true,
|
||||
lowerLabel: { default: "Not at all likely" },
|
||||
upperLabel: { default: "Extremely likely" },
|
||||
isColorCodingEnabled: true,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "To help us improve, can you describe the reason(s) for your rating?" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Any other comments, feedback, or concerns?" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const StarRatingSurvey = (): TXMTemplate => {
|
||||
const reusableQuestionIds = [createId(), createId(), createId()];
|
||||
|
||||
return {
|
||||
...XMSurveyDefault,
|
||||
name: "{{productName}}'s Rating Survey",
|
||||
questions: [
|
||||
{
|
||||
id: reusableQuestionIds[0],
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
logic: [
|
||||
{
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: reusableQuestionIds[0],
|
||||
type: "question",
|
||||
},
|
||||
operator: "isLessThanOrEqual",
|
||||
rightOperand: {
|
||||
type: "static",
|
||||
value: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: reusableQuestionIds[2],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
range: 5,
|
||||
scale: "number",
|
||||
headline: { default: "How do you like {{productName}}?" },
|
||||
required: true,
|
||||
lowerLabel: { default: "Extremely dissatisfied" },
|
||||
upperLabel: { default: "Extremely satisfied" },
|
||||
isColorCodingEnabled: false,
|
||||
},
|
||||
{
|
||||
id: reusableQuestionIds[1],
|
||||
html: { default: '<p class="fb-editor-paragraph" dir="ltr"><span>This helps us a lot.</span></p>' },
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
logic: [
|
||||
{
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: reusableQuestionIds[1],
|
||||
type: "question",
|
||||
},
|
||||
operator: "isClicked",
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: XMSurveyDefault.endings[0].id,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
headline: { default: "Happy to hear 🙏 Please write a review for us!" },
|
||||
required: true,
|
||||
buttonUrl: "https://formbricks.com/github",
|
||||
buttonLabel: { default: "Write review" },
|
||||
buttonExternal: true,
|
||||
},
|
||||
{
|
||||
id: reusableQuestionIds[2],
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Sorry to hear! What is ONE thing we can do better?" },
|
||||
required: true,
|
||||
subheader: { default: "Help us improve your experience." },
|
||||
buttonLabel: { default: "Send" },
|
||||
placeholder: { default: "Type your answer here..." },
|
||||
inputType: "text",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const CSATSurvey = (): TXMTemplate => {
|
||||
const reusableQuestionIds = [createId(), createId(), createId()];
|
||||
|
||||
return {
|
||||
...XMSurveyDefault,
|
||||
name: "{{productName}} CSAT",
|
||||
questions: [
|
||||
{
|
||||
id: reusableQuestionIds[0],
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
logic: [
|
||||
{
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: reusableQuestionIds[0],
|
||||
type: "question",
|
||||
},
|
||||
operator: "isLessThanOrEqual",
|
||||
rightOperand: {
|
||||
type: "static",
|
||||
value: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: reusableQuestionIds[2],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
range: 5,
|
||||
scale: "smiley",
|
||||
headline: { default: "How satisfied are you with your {{productName}} experience?" },
|
||||
required: true,
|
||||
lowerLabel: { default: "Extremely dissatisfied" },
|
||||
upperLabel: { default: "Extremely satisfied" },
|
||||
isColorCodingEnabled: false,
|
||||
},
|
||||
{
|
||||
id: reusableQuestionIds[1],
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
logic: [
|
||||
{
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: reusableQuestionIds[1],
|
||||
type: "question",
|
||||
},
|
||||
operator: "isSubmitted",
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: XMSurveyDefault.endings[0].id,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
headline: { default: "Lovely! Is there anything we can do to improve your experience?" },
|
||||
required: false,
|
||||
placeholder: { default: "Type your answer here..." },
|
||||
inputType: "text",
|
||||
},
|
||||
{
|
||||
id: reusableQuestionIds[2],
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Ugh, sorry! Is there anything we can do to improve your experience?" },
|
||||
required: false,
|
||||
placeholder: { default: "Type your answer here..." },
|
||||
inputType: "text",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const CESSurvey = (): TXMTemplate => {
|
||||
return {
|
||||
...XMSurveyDefault,
|
||||
name: "CES Survey",
|
||||
questions: [
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
range: 5,
|
||||
scale: "number",
|
||||
headline: { default: "{{productName}} makes it easy for me to [ADD GOAL]" },
|
||||
required: true,
|
||||
lowerLabel: { default: "Disagree strongly" },
|
||||
upperLabel: { default: "Agree strongly" },
|
||||
isColorCodingEnabled: false,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Thanks! How could we make it easier for you to [ADD GOAL]?" },
|
||||
required: true,
|
||||
placeholder: { default: "Type your answer here..." },
|
||||
inputType: "text",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const SmileysRatingSurvey = (): TXMTemplate => {
|
||||
const reusableQuestionIds = [createId(), createId(), createId()];
|
||||
|
||||
return {
|
||||
...XMSurveyDefault,
|
||||
name: "Smileys Survey",
|
||||
questions: [
|
||||
{
|
||||
id: reusableQuestionIds[0],
|
||||
type: TSurveyQuestionTypeEnum.Rating,
|
||||
logic: [
|
||||
{
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: reusableQuestionIds[0],
|
||||
type: "question",
|
||||
},
|
||||
operator: "isLessThanOrEqual",
|
||||
rightOperand: {
|
||||
type: "static",
|
||||
value: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: reusableQuestionIds[2],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
range: 5,
|
||||
scale: "smiley",
|
||||
headline: { default: "How do you like {{productName}}?" },
|
||||
required: true,
|
||||
lowerLabel: { default: "Not good" },
|
||||
upperLabel: { default: "Very satisfied" },
|
||||
isColorCodingEnabled: false,
|
||||
},
|
||||
{
|
||||
id: reusableQuestionIds[1],
|
||||
html: { default: '<p class="fb-editor-paragraph" dir="ltr"><span>This helps us a lot.</span></p>' },
|
||||
type: TSurveyQuestionTypeEnum.CTA,
|
||||
logic: [
|
||||
{
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: reusableQuestionIds[1],
|
||||
type: "question",
|
||||
},
|
||||
operator: "isClicked",
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: XMSurveyDefault.endings[0].id,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
headline: { default: "Happy to hear 🙏 Please write a review for us!" },
|
||||
required: true,
|
||||
buttonUrl: "https://formbricks.com/github",
|
||||
buttonLabel: { default: "Write review" },
|
||||
buttonExternal: true,
|
||||
},
|
||||
{
|
||||
id: reusableQuestionIds[2],
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Sorry to hear! What is ONE thing we can do better?" },
|
||||
required: true,
|
||||
subheader: { default: "Help us improve your experience." },
|
||||
buttonLabel: { default: "Send" },
|
||||
placeholder: { default: "Type your answer here..." },
|
||||
inputType: "text",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const eNPSSurvey = (): TXMTemplate => {
|
||||
return {
|
||||
...XMSurveyDefault,
|
||||
name: "eNPS Survey",
|
||||
questions: [
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.NPS,
|
||||
headline: {
|
||||
default: "How likely are you to recommend working at this company to a friend or colleague?",
|
||||
},
|
||||
required: false,
|
||||
lowerLabel: { default: "Not at all likely" },
|
||||
upperLabel: { default: "Extremely likely" },
|
||||
isColorCodingEnabled: true,
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "To help us improve, can you describe the reason(s) for your rating?" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
},
|
||||
{
|
||||
id: createId(),
|
||||
type: TSurveyQuestionTypeEnum.OpenText,
|
||||
headline: { default: "Any other comments, feedback, or concerns?" },
|
||||
required: false,
|
||||
inputType: "text",
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
export const XMTemplates: TXMTemplate[] = [
|
||||
NPSSurvey(),
|
||||
StarRatingSurvey(),
|
||||
CSATSurvey(),
|
||||
CESSurvey(),
|
||||
SmileysRatingSurvey(),
|
||||
eNPSSurvey(),
|
||||
];
|
||||
@@ -0,0 +1,60 @@
|
||||
import { XMTemplateList } from "@/app/(app)/(onboarding)/environments/[environmentId]/xm-templates/components/XMTemplateList";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils";
|
||||
import { getProductByEnvironmentId, getProducts } from "@formbricks/lib/product/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Header } from "@formbricks/ui/components/Header";
|
||||
|
||||
interface XMTemplatePageProps {
|
||||
params: {
|
||||
environmentId: string;
|
||||
};
|
||||
}
|
||||
|
||||
const Page = async ({ params }: XMTemplatePageProps) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
const environment = await getEnvironment(params.environmentId);
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||
const user = await getUser(session.user.id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment not found");
|
||||
}
|
||||
|
||||
const organizationId = await getOrganizationIdFromEnvironmentId(environment.id);
|
||||
|
||||
const product = await getProductByEnvironmentId(environment.id);
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const products = await getProducts(organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header title="What kind of feedback would you like to get?" />
|
||||
<XMTemplateList product={product} user={user} environmentId={environment.id} />
|
||||
{products.length >= 2 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="minimal"
|
||||
href={`/environments/${environment.id}/surveys`}>
|
||||
<XIcon className="h-7 w-7" strokeWidth={1.5} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,13 +1,12 @@
|
||||
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
|
||||
export const getCustomHeadline = (channel: TProductConfigChannel, industry: TProductConfigIndustry) => {
|
||||
const combinations = {
|
||||
"website+eCommerce": "web shop",
|
||||
"website+saas": "landing page",
|
||||
"website+other": "website",
|
||||
"app+eCommerce": "shopping app",
|
||||
"app+saas": "SaaS app",
|
||||
"app+other": "app",
|
||||
};
|
||||
return combinations[`${channel}+${industry}`] || "product";
|
||||
export const getCustomHeadline = (channel?: TProductConfigChannel) => {
|
||||
switch (channel) {
|
||||
case "website":
|
||||
return "Let's get the most out of your website traffic!";
|
||||
case "app":
|
||||
return "Let's research what your users need!";
|
||||
default:
|
||||
return "You maintain a product, how exciting!";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { canUserAccessOrganization } from "@formbricks/lib/organization/auth";
|
||||
import { getOrganization } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { ToasterClient } from "@formbricks/ui/ToasterClient";
|
||||
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
|
||||
|
||||
const ProductOnboardingLayout = async ({ children, params }) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { GlobeIcon, GlobeLockIcon, LinkIcon, XIcon } from "lucide-react";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Header } from "@formbricks/ui/Header";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Header } from "@formbricks/ui/components/Header";
|
||||
|
||||
interface ChannelPageProps {
|
||||
params: {
|
||||
@@ -17,14 +17,14 @@ const Page = async ({ params }: ChannelPageProps) => {
|
||||
description: "Run well-timed pop-up surveys.",
|
||||
icon: GlobeIcon,
|
||||
iconText: "Built for scale",
|
||||
href: `/organizations/${params.organizationId}/products/new/industry?channel=website`,
|
||||
href: `/organizations/${params.organizationId}/products/new/settings?channel=website`,
|
||||
},
|
||||
{
|
||||
title: "App with sign up",
|
||||
description: "Run highly-targeted micro-surveys.",
|
||||
icon: GlobeLockIcon,
|
||||
iconText: "Enrich user profiles",
|
||||
href: `/organizations/${params.organizationId}/products/new/industry?channel=app`,
|
||||
href: `/organizations/${params.organizationId}/products/new/settings?channel=app`,
|
||||
},
|
||||
{
|
||||
channel: "link",
|
||||
@@ -32,7 +32,7 @@ const Page = async ({ params }: ChannelPageProps) => {
|
||||
description: "Reach people anywhere online.",
|
||||
icon: LinkIcon,
|
||||
iconText: "Anywhere online",
|
||||
href: `/organizations/${params.organizationId}/products/new/industry?channel=link`,
|
||||
href: `/organizations/${params.organizationId}/products/new/settings?channel=link`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import { HeartIcon, MonitorIcon, ShoppingCart, XIcon } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { TProductConfigChannel } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Header } from "@formbricks/ui/Header";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Header } from "@formbricks/ui/components/Header";
|
||||
|
||||
interface IndustryPageProps {
|
||||
params: {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { OnboardingOptionsContainer } from "@/app/(app)/(onboarding)/organizations/components/OnboardingOptionsContainer";
|
||||
import { HeartIcon, ListTodoIcon, XIcon } from "lucide-react";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Header } from "@formbricks/ui/components/Header";
|
||||
|
||||
interface ModePageProps {
|
||||
params: {
|
||||
organizationId: string;
|
||||
};
|
||||
}
|
||||
|
||||
const Page = async ({ params }: ModePageProps) => {
|
||||
const channelOptions = [
|
||||
{
|
||||
title: "Formbricks Surveys",
|
||||
description: "Multi-purpose survey platform for web, app and email surveys.",
|
||||
icon: ListTodoIcon,
|
||||
href: `/organizations/${params.organizationId}/products/new/channel`,
|
||||
},
|
||||
{
|
||||
title: "Formbricks CX",
|
||||
description: "Surveys and reports to understand what your customers need.",
|
||||
icon: HeartIcon,
|
||||
href: `/organizations/${params.organizationId}/products/new/settings?mode=cx`,
|
||||
},
|
||||
];
|
||||
|
||||
const products = await getProducts(params.organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
<Header title="What are you here for?" />
|
||||
<OnboardingOptionsContainer options={channelOptions} />
|
||||
{products.length >= 1 && (
|
||||
<Button
|
||||
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
|
||||
variant="minimal"
|
||||
href={"/"}>
|
||||
<XIcon className="h-7 w-7" strokeWidth={1.5} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
|
||||
import { createProductAction } from "@/app/(app)/environments/[environmentId]/actions";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Image from "next/image";
|
||||
@@ -8,16 +7,17 @@ import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper";
|
||||
import { FORMBRICKS_PRODUCT_ID_LS, FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
|
||||
import { FORMBRICKS_SURVEYS_FILTERS_KEY_LS } from "@formbricks/lib/localStorage";
|
||||
import { PREVIEW_SURVEY } from "@formbricks/lib/styling/constants";
|
||||
import {
|
||||
TProductConfigChannel,
|
||||
TProductConfigIndustry,
|
||||
TProductMode,
|
||||
TProductUpdateInput,
|
||||
ZProductUpdateInput,
|
||||
} from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { ColorPicker } from "@formbricks/ui/ColorPicker";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { ColorPicker } from "@formbricks/ui/components/ColorPicker";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
@@ -26,12 +26,13 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormProvider,
|
||||
} from "@formbricks/ui/Form";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { SurveyInline } from "@formbricks/ui/Survey";
|
||||
} from "@formbricks/ui/components/Form";
|
||||
import { Input } from "@formbricks/ui/components/Input";
|
||||
import { SurveyInline } from "@formbricks/ui/components/Survey";
|
||||
|
||||
interface ProductSettingsProps {
|
||||
organizationId: string;
|
||||
productMode: TProductMode;
|
||||
channel: TProductConfigChannel;
|
||||
industry: TProductConfigIndustry;
|
||||
defaultBrandColor: string;
|
||||
@@ -39,6 +40,7 @@ interface ProductSettingsProps {
|
||||
|
||||
export const ProductSettings = ({
|
||||
organizationId,
|
||||
productMode,
|
||||
channel,
|
||||
industry,
|
||||
defaultBrandColor,
|
||||
@@ -62,16 +64,16 @@ export const ProductSettings = ({
|
||||
);
|
||||
if (productionEnvironment) {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(FORMBRICKS_PRODUCT_ID_LS, productionEnvironment.productId);
|
||||
|
||||
// Rmove filters when creating a new product
|
||||
localStorage.removeItem(FORMBRICKS_SURVEYS_FILTERS_KEY_LS);
|
||||
}
|
||||
}
|
||||
if (channel !== "link") {
|
||||
if (channel === "app" || channel === "website") {
|
||||
router.push(`/environments/${productionEnvironment?.id}/connect`);
|
||||
} else {
|
||||
} else if (channel === "link") {
|
||||
router.push(`/environments/${productionEnvironment?.id}/surveys`);
|
||||
} else if (productMode === "cx") {
|
||||
router.push(`/environments/${productionEnvironment?.id}/xm-templates`);
|
||||
}
|
||||
} else {
|
||||
const errorMessage = getFormattedErrorMessage(createProductResponse);
|
||||
@@ -128,9 +130,7 @@ export const ProductSettings = ({
|
||||
<FormItem className="w-full space-y-4">
|
||||
<div>
|
||||
<FormLabel>Product name</FormLabel>
|
||||
<FormDescription>
|
||||
What is your {getCustomHeadline(channel, industry)} called?
|
||||
</FormDescription>
|
||||
<FormDescription>What is your product called?</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<div>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { getCustomHeadline } from "@/app/(app)/(onboarding)/lib/utils";
|
||||
import { ProductSettings } from "@/app/(app)/(onboarding)/organizations/[organizationId]/products/new/settings/components/ProductSettings";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants";
|
||||
import { getProducts } from "@formbricks/lib/product/service";
|
||||
import { startsWithVowel } from "@formbricks/lib/utils/strings";
|
||||
import { TProductConfigChannel, TProductConfigIndustry } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Header } from "@formbricks/ui/Header";
|
||||
import { TProductConfigChannel, TProductConfigIndustry, TProductMode } from "@formbricks/types/product";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Header } from "@formbricks/ui/components/Header";
|
||||
|
||||
interface ProductSettingsPageProps {
|
||||
params: {
|
||||
@@ -16,31 +14,34 @@ interface ProductSettingsPageProps {
|
||||
searchParams: {
|
||||
channel?: TProductConfigChannel;
|
||||
industry?: TProductConfigIndustry;
|
||||
mode?: TProductMode;
|
||||
};
|
||||
}
|
||||
|
||||
const Page = async ({ params, searchParams }: ProductSettingsPageProps) => {
|
||||
const channel = searchParams.channel;
|
||||
const industry = searchParams.industry;
|
||||
if (!channel || !industry) return notFound();
|
||||
const customHeadline = getCustomHeadline(channel, industry);
|
||||
const channel = searchParams.channel || null;
|
||||
const industry = searchParams.industry || null;
|
||||
const mode = searchParams.mode || "surveys";
|
||||
|
||||
const customHeadline = getCustomHeadline(channel);
|
||||
const products = await getProducts(params.organizationId);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-full flex-col items-center justify-center space-y-12">
|
||||
{channel === "link" ? (
|
||||
{channel === "link" || mode === "cx" ? (
|
||||
<Header
|
||||
title="Match your brand, get 2x more responses."
|
||||
subtitle="When people recognize your brand, they are much more likely to start and complete responses."
|
||||
/>
|
||||
) : (
|
||||
<Header
|
||||
title={`You maintain ${startsWithVowel(customHeadline) ? "an " + customHeadline : "a " + customHeadline}, how exciting!`}
|
||||
title={customHeadline}
|
||||
subtitle="Get 2x more responses matching surveys with your brand and UI"
|
||||
/>
|
||||
)}
|
||||
<ProductSettings
|
||||
organizationId={params.organizationId}
|
||||
productMode={mode}
|
||||
channel={channel}
|
||||
industry={industry}
|
||||
defaultBrandColor={DEFAULT_BRAND_COLOR}
|
||||
|
||||
@@ -1,41 +1,59 @@
|
||||
import { LucideProps } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ForwardRefExoticComponent, RefAttributes } from "react";
|
||||
import { OptionCard } from "@formbricks/ui/OptionCard";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { OptionCard } from "@formbricks/ui/components/OptionCard";
|
||||
|
||||
interface OnboardingOptionsContainerProps {
|
||||
options: {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>;
|
||||
iconText: string;
|
||||
href: string;
|
||||
iconText?: string;
|
||||
href?: string;
|
||||
onClick?: () => void;
|
||||
isLoading?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const OnboardingOptionsContainer = ({ options }: OnboardingOptionsContainerProps) => {
|
||||
const getOptionCard = (option) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<OptionCard
|
||||
size="md"
|
||||
key={option.title}
|
||||
title={option.title}
|
||||
onSelect={option.onClick}
|
||||
description={option.description}
|
||||
loading={option.isLoading || false}>
|
||||
<div className="flex flex-col items-center">
|
||||
<Icon className="h-16 w-16 text-slate-600" strokeWidth={0.5} />
|
||||
{option.iconText && (
|
||||
<p className="mt-4 w-fit rounded-xl bg-slate-200 px-4 text-sm text-slate-700">
|
||||
{option.iconText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</OptionCard>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid w-5/6 grid-cols-3 gap-8 text-center lg:w-2/3">
|
||||
{options.map((option, index) => {
|
||||
const Icon = option.icon;
|
||||
return (
|
||||
<Link href={option.href}>
|
||||
<OptionCard
|
||||
size="md"
|
||||
key={index}
|
||||
title={option.title}
|
||||
description={option.description}
|
||||
loading={false}>
|
||||
<div className="flex flex-col items-center">
|
||||
<Icon className="h-16 w-16 text-slate-600" strokeWidth={0.5} />
|
||||
<p className="mt-4 w-fit rounded-xl bg-slate-200 px-4 text-sm text-slate-700">
|
||||
{option.iconText}
|
||||
</p>
|
||||
</div>
|
||||
</OptionCard>
|
||||
<div
|
||||
className={cn({
|
||||
"grid w-5/6 grid-cols-3 gap-8 text-center lg:w-2/3": options.length >= 3,
|
||||
"flex justify-center gap-8": options.length < 3,
|
||||
})}>
|
||||
{options.map((option) =>
|
||||
option.href ? (
|
||||
<Link key={option.title} href={option.href}>
|
||||
{getOptionCard(option)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
) : (
|
||||
getOptionCard(option)
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,8 +9,8 @@ import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { DevEnvironmentBanner } from "@formbricks/ui/DevEnvironmentBanner";
|
||||
import { ToasterClient } from "@formbricks/ui/ToasterClient";
|
||||
import { DevEnvironmentBanner } from "@formbricks/ui/components/DevEnvironmentBanner";
|
||||
import { ToasterClient } from "@formbricks/ui/components/ToasterClient";
|
||||
|
||||
const EnvLayout = async ({ children, params }) => {
|
||||
const session = await getServerSession(authOptions);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from "zod";
|
||||
import { createActionClass } from "@formbricks/lib/actionClass/service";
|
||||
import { actionClient, authenticatedActionClient } from "@formbricks/lib/actionClient";
|
||||
import { checkAuthorization } from "@formbricks/lib/actionClient/utils";
|
||||
import { UNSPLASH_ACCESS_KEY } from "@formbricks/lib/constants";
|
||||
import { UNSPLASH_ACCESS_KEY, UNSPLASH_ALLOWED_DOMAINS } from "@formbricks/lib/constants";
|
||||
import {
|
||||
getOrganizationIdFromEnvironmentId,
|
||||
getOrganizationIdFromProductId,
|
||||
@@ -227,13 +227,26 @@ export const getImagesFromUnsplashAction = actionClient
|
||||
});
|
||||
});
|
||||
|
||||
const isValidUnsplashUrl = (url: string): boolean => {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.protocol === "https:" && UNSPLASH_ALLOWED_DOMAINS.includes(parsedUrl.hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const ZTriggerDownloadUnsplashImageAction = z.object({
|
||||
downloadUrl: z.string(),
|
||||
downloadUrl: z.string().url(),
|
||||
});
|
||||
|
||||
export const triggerDownloadUnsplashImageAction = actionClient
|
||||
.schema(ZTriggerDownloadUnsplashImageAction)
|
||||
.action(async ({ parsedInput }) => {
|
||||
if (!isValidUnsplashUrl(parsedInput.downloadUrl)) {
|
||||
throw new Error("Invalid Unsplash URL");
|
||||
}
|
||||
|
||||
const response = await fetch(`${parsedInput.downloadUrl}/?client_id=${UNSPLASH_ACCESS_KEY}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { TActionClass } from "@formbricks/types/action-classes";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { ModalWithTabs } from "@formbricks/ui/ModalWithTabs";
|
||||
import { ModalWithTabs } from "@formbricks/ui/components/ModalWithTabs";
|
||||
import { CreateNewActionTab } from "./CreateNewActionTab";
|
||||
import { SavedActionsTab } from "./SavedActionsTab";
|
||||
|
||||
@@ -56,6 +56,7 @@ export const AddActionModal = ({
|
||||
return (
|
||||
<ModalWithTabs
|
||||
label="Add action"
|
||||
description="Capture a new action to trigger a survey on."
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
tabs={tabs}
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { getQuestionDefaults, questionTypes, universalQuestionPresets } from "@/app/lib/questions";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import {
|
||||
CXQuestionTypes,
|
||||
getQuestionDefaults,
|
||||
questionTypes,
|
||||
universalQuestionPresets,
|
||||
} from "@formbricks/lib/utils/questions";
|
||||
import { TProduct } from "@formbricks/types/product";
|
||||
|
||||
interface AddQuestionButtonProps {
|
||||
addQuestion: (question: any) => void;
|
||||
product: TProduct;
|
||||
isCxMode: boolean;
|
||||
}
|
||||
|
||||
export const AddQuestionButton = ({ addQuestion, product }: AddQuestionButtonProps) => {
|
||||
export const AddQuestionButton = ({ addQuestion, product, isCxMode }: AddQuestionButtonProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const availableQuestionTypes = isCxMode ? CXQuestionTypes : questionTypes;
|
||||
|
||||
return (
|
||||
<Collapsible.Root
|
||||
open={open}
|
||||
@@ -37,7 +45,7 @@ export const AddQuestionButton = ({ addQuestion, product }: AddQuestionButtonPro
|
||||
</Collapsible.CollapsibleTrigger>
|
||||
<Collapsible.CollapsibleContent className="justify-left flex flex-col">
|
||||
{/* <hr className="py-1 text-slate-600" /> */}
|
||||
{questionTypes.map((questionType) => (
|
||||
{availableQuestionTypes.map((questionType) => (
|
||||
<button
|
||||
type="button"
|
||||
key={questionType.id}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TSurvey, TSurveyAddressQuestion } from "@formbricks/types/surveys/types";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
|
||||
import { QuestionToggleTable } from "@formbricks/ui/components/QuestionToggleTable";
|
||||
|
||||
interface AddressQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -32,6 +33,64 @@ export const AddressQuestionForm = ({
|
||||
}: AddressQuestionFormProps): JSX.Element => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
|
||||
|
||||
const fields = [
|
||||
{
|
||||
id: "addressLine1",
|
||||
label: "Address Line 1",
|
||||
...question.addressLine1,
|
||||
},
|
||||
{
|
||||
id: "addressLine2",
|
||||
label: "Address Line 2",
|
||||
...question.addressLine2,
|
||||
},
|
||||
{
|
||||
id: "city",
|
||||
label: "City",
|
||||
...question.city,
|
||||
},
|
||||
{
|
||||
id: "state",
|
||||
label: "State",
|
||||
...question.state,
|
||||
},
|
||||
{
|
||||
id: "zip",
|
||||
label: "Zip",
|
||||
...question.zip,
|
||||
},
|
||||
{
|
||||
id: "country",
|
||||
label: "Country",
|
||||
...question.country,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const allFieldsAreOptional = [
|
||||
question.addressLine1,
|
||||
question.addressLine2,
|
||||
question.city,
|
||||
question.state,
|
||||
question.zip,
|
||||
question.country,
|
||||
]
|
||||
.filter((field) => field.show)
|
||||
.every((field) => !field.required);
|
||||
|
||||
if (allFieldsAreOptional) {
|
||||
updateQuestion(questionIdx, { required: false });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
question.addressLine1,
|
||||
question.addressLine2,
|
||||
question.city,
|
||||
question.state,
|
||||
question.zip,
|
||||
question.country,
|
||||
]);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
@@ -81,73 +140,30 @@ export const AddressQuestionForm = ({
|
||||
Add Description
|
||||
</Button>
|
||||
)}
|
||||
<div className="mt-2 font-medium">Settings</div>
|
||||
<AdvancedOptionToggle
|
||||
isChecked={question.isAddressLine1Required}
|
||||
onToggle={() =>
|
||||
|
||||
<QuestionToggleTable
|
||||
type="address"
|
||||
fields={fields}
|
||||
onShowToggle={(field, show) => {
|
||||
updateQuestion(questionIdx, {
|
||||
isAddressLine1Required: !question.isAddressLine1Required,
|
||||
required: true,
|
||||
})
|
||||
}
|
||||
htmlId="isAddressRequired"
|
||||
title="Required: Address Line 1"
|
||||
description=""
|
||||
childBorder
|
||||
customContainerClass="p-0 mt-4"></AdvancedOptionToggle>
|
||||
<AdvancedOptionToggle
|
||||
isChecked={question.isAddressLine2Required}
|
||||
onToggle={() =>
|
||||
[field.id]: {
|
||||
show,
|
||||
required: field.required,
|
||||
},
|
||||
// when show changes, and the field is required, the question should be required
|
||||
...(show && field.required && { required: true }),
|
||||
});
|
||||
}}
|
||||
onRequiredToggle={(field, required) => {
|
||||
updateQuestion(questionIdx, {
|
||||
isAddressLine2Required: !question.isAddressLine2Required,
|
||||
[field.id]: {
|
||||
show: field.show,
|
||||
required,
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
}
|
||||
htmlId="isAddressLine2Required"
|
||||
title="Required: Address Line 2"
|
||||
description=""
|
||||
childBorder
|
||||
customContainerClass="p-0 mt-4"></AdvancedOptionToggle>
|
||||
<AdvancedOptionToggle
|
||||
isChecked={question.isCityRequired}
|
||||
onToggle={() =>
|
||||
updateQuestion(questionIdx, { isCityRequired: !question.isCityRequired, required: true })
|
||||
}
|
||||
htmlId="isCityRequired"
|
||||
title="Required: City / Town"
|
||||
description=""
|
||||
childBorder
|
||||
customContainerClass="p-0 mt-4"></AdvancedOptionToggle>
|
||||
<AdvancedOptionToggle
|
||||
isChecked={question.isStateRequired}
|
||||
onToggle={() =>
|
||||
updateQuestion(questionIdx, { isStateRequired: !question.isStateRequired, required: true })
|
||||
}
|
||||
htmlId="isStateRequired"
|
||||
title="Required: State / Region"
|
||||
description=""
|
||||
childBorder
|
||||
customContainerClass="p-0 mt-4"></AdvancedOptionToggle>
|
||||
<AdvancedOptionToggle
|
||||
isChecked={question.isZipRequired}
|
||||
onToggle={() =>
|
||||
updateQuestion(questionIdx, { isZipRequired: !question.isZipRequired, required: true })
|
||||
}
|
||||
htmlId="isZipRequired"
|
||||
title="Required: ZIP / Post Code"
|
||||
description=""
|
||||
childBorder
|
||||
customContainerClass="p-0 mt-4"></AdvancedOptionToggle>
|
||||
<AdvancedOptionToggle
|
||||
isChecked={question.isCountryRequired}
|
||||
onToggle={() =>
|
||||
updateQuestion(questionIdx, { isCountryRequired: !question.isCountryRequired, required: true })
|
||||
}
|
||||
htmlId="iscountryRequired"
|
||||
title="Required: Country"
|
||||
description=""
|
||||
childBorder
|
||||
customContainerClass="p-0 mt-4"></AdvancedOptionToggle>
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ConditionalLogic } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/ConditionalLogic";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { LogicEditor } from "./LogicEditor";
|
||||
import { UpdateQuestionId } from "./UpdateQuestionId";
|
||||
|
||||
interface AdvancedSettingsProps {
|
||||
@@ -19,16 +19,14 @@ export const AdvancedSettings = ({
|
||||
attributeClasses,
|
||||
}: AdvancedSettingsProps) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<LogicEditor
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<ConditionalLogic
|
||||
question={question}
|
||||
updateQuestion={updateQuestion}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<UpdateQuestionId
|
||||
question={question}
|
||||
|
||||
@@ -6,9 +6,9 @@ import { UseFormReturn } from "react-hook-form";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TProductStyling } from "@formbricks/types/product";
|
||||
import { TSurveyStyling } from "@formbricks/types/surveys/types";
|
||||
import { Badge } from "@formbricks/ui/Badge";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
|
||||
import { Slider } from "@formbricks/ui/Slider";
|
||||
import { Badge } from "@formbricks/ui/components/Badge";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/components/Form";
|
||||
import { Slider } from "@formbricks/ui/components/Slider";
|
||||
import { SurveyBgSelectorTab } from "./SurveyBgSelectorTab";
|
||||
|
||||
interface BackgroundStylingCardProps {
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useState } from "react";
|
||||
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TSurvey, TSurveyCTAQuestion } from "@formbricks/types/surveys/types";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { OptionsSwitch } from "@formbricks/ui/OptionsSwitch";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Input } from "@formbricks/ui/components/Input";
|
||||
import { Label } from "@formbricks/ui/components/Label";
|
||||
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
|
||||
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
|
||||
|
||||
const options = [
|
||||
{
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useEffect, useState } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TSurvey, TSurveyCalQuestion } from "@formbricks/types/surveys/types";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/AdvancedOptionToggle";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { AdvancedOptionToggle } from "@formbricks/ui/components/AdvancedOptionToggle";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Input } from "@formbricks/ui/components/Input";
|
||||
import { Label } from "@formbricks/ui/components/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
|
||||
|
||||
interface CalQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
|
||||
@@ -8,12 +8,12 @@ import { cn } from "@formbricks/lib/cn";
|
||||
import { COLOR_DEFAULTS } from "@formbricks/lib/styling/constants";
|
||||
import { TProduct, TProductStyling } from "@formbricks/types/product";
|
||||
import { TSurveyStyling, TSurveyType } from "@formbricks/types/surveys/types";
|
||||
import { Badge } from "@formbricks/ui/Badge";
|
||||
import { CardArrangementTabs } from "@formbricks/ui/CardArrangementTabs";
|
||||
import { ColorPicker } from "@formbricks/ui/ColorPicker";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
|
||||
import { Slider } from "@formbricks/ui/Slider";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
import { Badge } from "@formbricks/ui/components/Badge";
|
||||
import { CardArrangementTabs } from "@formbricks/ui/components/CardArrangementTabs";
|
||||
import { ColorPicker } from "@formbricks/ui/components/ColorPicker";
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel } from "@formbricks/ui/components/Form";
|
||||
import { Slider } from "@formbricks/ui/components/Slider";
|
||||
import { Switch } from "@formbricks/ui/components/Switch";
|
||||
|
||||
type CardStylingSettingsProps = {
|
||||
open: boolean;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import { ColorPicker } from "@formbricks/ui/ColorPicker";
|
||||
import { ColorPicker } from "@formbricks/ui/components/ColorPicker";
|
||||
|
||||
interface ColorSurveyBgProps {
|
||||
handleBgChange: (bg: string, bgType: string) => void;
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import { LogicEditor } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/LogicEditor";
|
||||
import {
|
||||
getDefaultOperatorForQuestion,
|
||||
replaceEndingCardHeadlineRecall,
|
||||
} from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
CopyIcon,
|
||||
EllipsisVerticalIcon,
|
||||
PlusIcon,
|
||||
SplitIcon,
|
||||
TrashIcon,
|
||||
} from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { duplicateLogicItem } from "@formbricks/lib/surveyLogic/utils";
|
||||
import { replaceHeadlineRecall } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TSurvey, TSurveyLogic, TSurveyQuestion } from "@formbricks/types/surveys/types";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@formbricks/ui/components/DropdownMenu";
|
||||
import { Label } from "@formbricks/ui/components/Label";
|
||||
|
||||
interface ConditionalLogicProps {
|
||||
localSurvey: TSurvey;
|
||||
questionIdx: number;
|
||||
question: TSurveyQuestion;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: any) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export function ConditionalLogic({
|
||||
attributeClasses,
|
||||
localSurvey,
|
||||
question,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
}: ConditionalLogicProps) {
|
||||
const transformedSurvey = useMemo(() => {
|
||||
let modifiedSurvey = replaceHeadlineRecall(localSurvey, "default", attributeClasses);
|
||||
modifiedSurvey = replaceEndingCardHeadlineRecall(modifiedSurvey, "default", attributeClasses);
|
||||
|
||||
return modifiedSurvey;
|
||||
}, [localSurvey, attributeClasses]);
|
||||
|
||||
const addLogic = () => {
|
||||
const operator = getDefaultOperatorForQuestion(question);
|
||||
|
||||
const initialCondition: TSurveyLogic = {
|
||||
id: createId(),
|
||||
conditions: {
|
||||
id: createId(),
|
||||
connector: "and",
|
||||
conditions: [
|
||||
{
|
||||
id: createId(),
|
||||
leftOperand: {
|
||||
value: question.id,
|
||||
type: "question",
|
||||
},
|
||||
operator,
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createId(),
|
||||
objective: "jumpToQuestion",
|
||||
target: "",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
updateQuestion(questionIdx, {
|
||||
logic: [...(question?.logic ?? []), initialCondition],
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveLogic = (logicItemIdx: number) => {
|
||||
const logicCopy = structuredClone(question.logic ?? []);
|
||||
logicCopy.splice(logicItemIdx, 1);
|
||||
|
||||
updateQuestion(questionIdx, {
|
||||
logic: logicCopy,
|
||||
});
|
||||
};
|
||||
|
||||
const moveLogic = (from: number, to: number) => {
|
||||
const logicCopy = structuredClone(question.logic ?? []);
|
||||
const [movedItem] = logicCopy.splice(from, 1);
|
||||
logicCopy.splice(to, 0, movedItem);
|
||||
|
||||
updateQuestion(questionIdx, {
|
||||
logic: logicCopy,
|
||||
});
|
||||
};
|
||||
|
||||
const duplicateLogic = (logicItemIdx: number) => {
|
||||
const logicCopy = structuredClone(question.logic ?? []);
|
||||
const logicItem = logicCopy[logicItemIdx];
|
||||
const newLogicItem = duplicateLogicItem(logicItem);
|
||||
logicCopy.splice(logicItemIdx + 1, 0, newLogicItem);
|
||||
|
||||
updateQuestion(questionIdx, {
|
||||
logic: logicCopy,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<Label className="flex gap-2">
|
||||
Conditional Logic
|
||||
<SplitIcon className="h-4 w-4 rotate-90" />
|
||||
</Label>
|
||||
|
||||
{question.logic && question.logic.length > 0 && (
|
||||
<div className="mt-2 flex flex-col gap-4">
|
||||
{question.logic.map((logicItem, logicItemIdx) => (
|
||||
<div
|
||||
key={logicItem.id}
|
||||
className="flex w-full grow items-start gap-2 rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||||
<LogicEditor
|
||||
localSurvey={transformedSurvey}
|
||||
logicItem={logicItem}
|
||||
updateQuestion={updateQuestion}
|
||||
question={question}
|
||||
questionIdx={questionIdx}
|
||||
logicIdx={logicItemIdx}
|
||||
isLast={logicItemIdx === (question.logic ?? []).length - 1}
|
||||
/>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<EllipsisVerticalIcon className="h-4 w-4 text-slate-700 hover:text-slate-950" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
duplicateLogic(logicItemIdx);
|
||||
}}>
|
||||
<CopyIcon className="h-4 w-4" />
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2"
|
||||
disabled={logicItemIdx === 0}
|
||||
onClick={() => {
|
||||
moveLogic(logicItemIdx, logicItemIdx - 1);
|
||||
}}>
|
||||
<ArrowUpIcon className="h-4 w-4" />
|
||||
Move up
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2"
|
||||
disabled={logicItemIdx === (question.logic ?? []).length - 1}
|
||||
onClick={() => {
|
||||
moveLogic(logicItemIdx, logicItemIdx + 1);
|
||||
}}>
|
||||
<ArrowDownIcon className="h-4 w-4" />
|
||||
Move down
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => {
|
||||
handleRemoveLogic(logicItemIdx);
|
||||
}}>
|
||||
<TrashIcon className="h-4 w-4" />
|
||||
Remove
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex items-center space-x-2">
|
||||
<Button
|
||||
id="logicJumps"
|
||||
className="bg-slate-100 hover:bg-slate-50"
|
||||
type="button"
|
||||
name="logicJumps"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
EndIcon={PlusIcon}
|
||||
onClick={addLogic}>
|
||||
Add logic
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import { useState } from "react";
|
||||
import { LocalizedEditor } from "@formbricks/ee/multi-language/components/localized-editor";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TSurvey, TSurveyConsentQuestion } from "@formbricks/types/surveys/types";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Label } from "@formbricks/ui/components/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
|
||||
|
||||
interface ConsentQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TSurvey, TSurveyContactInfoQuestion } from "@formbricks/types/surveys/types";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
|
||||
import { QuestionToggleTable } from "@formbricks/ui/components/QuestionToggleTable";
|
||||
|
||||
interface ContactInfoQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
question: TSurveyContactInfoQuestion;
|
||||
questionIdx: number;
|
||||
updateQuestion: (questionIdx: number, updatedAttributes: Partial<TSurveyContactInfoQuestion>) => void;
|
||||
lastQuestion: boolean;
|
||||
isInvalid: boolean;
|
||||
selectedLanguageCode: string;
|
||||
setSelectedLanguageCode: (language: string) => void;
|
||||
attributeClasses: TAttributeClass[];
|
||||
}
|
||||
|
||||
export const ContactInfoQuestionForm = ({
|
||||
question,
|
||||
questionIdx,
|
||||
updateQuestion,
|
||||
isInvalid,
|
||||
localSurvey,
|
||||
selectedLanguageCode,
|
||||
setSelectedLanguageCode,
|
||||
attributeClasses,
|
||||
}: ContactInfoQuestionFormProps): JSX.Element => {
|
||||
const surveyLanguageCodes = extractLanguageCodes(localSurvey.languages ?? []);
|
||||
|
||||
const fields = [
|
||||
{
|
||||
id: "firstName",
|
||||
label: "First Name",
|
||||
...question.firstName,
|
||||
},
|
||||
{
|
||||
id: "lastName",
|
||||
label: "Last Name",
|
||||
...question.lastName,
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
label: "Email",
|
||||
...question.email,
|
||||
},
|
||||
{
|
||||
id: "phone",
|
||||
label: "Phone",
|
||||
...question.phone,
|
||||
},
|
||||
{
|
||||
id: "company",
|
||||
label: "Company",
|
||||
...question.company,
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const allFieldsAreOptional = [
|
||||
question.firstName,
|
||||
question.lastName,
|
||||
question.email,
|
||||
question.phone,
|
||||
question.company,
|
||||
]
|
||||
.filter((field) => field.show)
|
||||
.every((field) => !field.required);
|
||||
|
||||
if (allFieldsAreOptional) {
|
||||
updateQuestion(questionIdx, { required: false });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [question.firstName, question.lastName, question.email, question.phone, question.company]);
|
||||
|
||||
return (
|
||||
<form>
|
||||
<QuestionFormInput
|
||||
id="headline"
|
||||
value={question.headline}
|
||||
label={"Question*"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{question.subheader !== undefined && (
|
||||
<div className="inline-flex w-full items-center">
|
||||
<div className="w-full">
|
||||
<QuestionFormInput
|
||||
id="subheader"
|
||||
value={question.subheader}
|
||||
label={"Description"}
|
||||
localSurvey={localSurvey}
|
||||
questionIdx={questionIdx}
|
||||
isInvalid={isInvalid}
|
||||
updateQuestion={updateQuestion}
|
||||
selectedLanguageCode={selectedLanguageCode}
|
||||
setSelectedLanguageCode={setSelectedLanguageCode}
|
||||
attributeClasses={attributeClasses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{question.subheader === undefined && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="minimal"
|
||||
className="mt-4"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
updateQuestion(questionIdx, {
|
||||
subheader: createI18nString("", surveyLanguageCodes),
|
||||
});
|
||||
}}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
Add Description
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<QuestionToggleTable
|
||||
type="contact"
|
||||
fields={fields}
|
||||
onShowToggle={(field, show) => {
|
||||
updateQuestion(questionIdx, {
|
||||
[field.id]: {
|
||||
show,
|
||||
required: field.required,
|
||||
},
|
||||
// when show changes, and the field is required, the question should be required
|
||||
...(show && field.required && { required: true }),
|
||||
});
|
||||
}}
|
||||
onRequiredToggle={(field, required) => {
|
||||
updateQuestion(questionIdx, {
|
||||
[field.id]: {
|
||||
show: field.show,
|
||||
required,
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -11,13 +11,13 @@ import {
|
||||
ZActionClassInput,
|
||||
} from "@formbricks/types/action-classes";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@formbricks/ui/Form";
|
||||
import { Input } from "@formbricks/ui/Input";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { TabToggle } from "@formbricks/ui/TabToggle";
|
||||
import { CodeActionForm } from "@formbricks/ui/organisms/CodeActionForm";
|
||||
import { NoCodeActionForm } from "@formbricks/ui/organisms/NoCodeActionForm";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@formbricks/ui/components/Form";
|
||||
import { Input } from "@formbricks/ui/components/Input";
|
||||
import { Label } from "@formbricks/ui/components/Label";
|
||||
import { TabToggle } from "@formbricks/ui/components/TabToggle";
|
||||
import { CodeActionForm } from "@formbricks/ui/components/organisms/CodeActionForm";
|
||||
import { NoCodeActionForm } from "@formbricks/ui/components/organisms/NoCodeActionForm";
|
||||
import { createActionClassAction } from "../actions";
|
||||
|
||||
interface CreateNewActionTabProps {
|
||||
@@ -171,14 +171,14 @@ export const CreateNewActionTab = ({
|
||||
<div>
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={handleSubmit(submitHandler)}>
|
||||
<div className="max-h-[400px] w-full space-y-4 overflow-y-auto">
|
||||
<div className="max-h-[500px] w-full space-y-4 overflow-y-auto pr-4">
|
||||
<div className="w-3/5">
|
||||
<FormField
|
||||
name={`type`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<div>
|
||||
<Label className="font-semibold">Type</Label>
|
||||
<Label className="font-semibold">Action Type</Label>
|
||||
<TabToggle
|
||||
id="type"
|
||||
options={[
|
||||
|
||||
@@ -2,10 +2,10 @@ import { PlusIcon } from "lucide-react";
|
||||
import { createI18nString, extractLanguageCodes } from "@formbricks/lib/i18n/utils";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TSurvey, TSurveyDateQuestion } from "@formbricks/types/surveys/types";
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { OptionsSwitch } from "@formbricks/ui/OptionsSwitch";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Button } from "@formbricks/ui/components/Button";
|
||||
import { Label } from "@formbricks/ui/components/Label";
|
||||
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
|
||||
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
|
||||
|
||||
interface IDateQuestionFormProps {
|
||||
localSurvey: TSurvey;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { EditorCardMenu } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu";
|
||||
import { EndScreenForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EndScreenForm";
|
||||
import { RedirectUrlForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/RedirectUrlForm";
|
||||
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util";
|
||||
import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/utils";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
@@ -14,8 +14,8 @@ import { recallToHeadline } from "@formbricks/lib/utils/recall";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TOrganizationBillingPlan } from "@formbricks/types/organizations";
|
||||
import { TSurvey, TSurveyEndScreenCard, TSurveyRedirectUrlCard } from "@formbricks/types/surveys/types";
|
||||
import { OptionsSwitch } from "@formbricks/ui/OptionsSwitch";
|
||||
import { TooltipRenderer } from "@formbricks/ui/Tooltip";
|
||||
import { OptionsSwitch } from "@formbricks/ui/components/OptionsSwitch";
|
||||
import { TooltipRenderer } from "@formbricks/ui/components/Tooltip";
|
||||
|
||||
interface EditEndingCardProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -32,11 +32,6 @@ interface EditEndingCardProps {
|
||||
isFormbricksCloud: boolean;
|
||||
}
|
||||
|
||||
const endingCardTypes = [
|
||||
{ value: "endScreen", label: "Ending card" },
|
||||
{ value: "redirectToUrl", label: "Redirect to Url" },
|
||||
];
|
||||
|
||||
export const EditEndingCard = ({
|
||||
localSurvey,
|
||||
endingCardIndex,
|
||||
@@ -52,9 +47,16 @@ export const EditEndingCard = ({
|
||||
isFormbricksCloud,
|
||||
}: EditEndingCardProps) => {
|
||||
const endingCard = localSurvey.endings[endingCardIndex];
|
||||
|
||||
const isRedirectToUrlDisabled = isFormbricksCloud
|
||||
? plan === "free" && endingCard.type !== "redirectToUrl"
|
||||
: false;
|
||||
|
||||
const endingCardTypes = [
|
||||
{ value: "endScreen", label: "Ending card" },
|
||||
{ value: "redirectToUrl", label: "Redirect to Url", disabled: isRedirectToUrlDisabled },
|
||||
];
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: endingCard.id,
|
||||
});
|
||||
@@ -204,14 +206,16 @@ export const EditEndingCard = ({
|
||||
<OptionsSwitch
|
||||
options={endingCardTypes}
|
||||
currentOption={endingCard.type}
|
||||
handleOptionChange={() => {
|
||||
if (endingCard.type === "endScreen") {
|
||||
updateSurvey({ type: "redirectToUrl" });
|
||||
} else {
|
||||
updateSurvey({ type: "endScreen" });
|
||||
handleOptionChange={(newType) => {
|
||||
const selectedOption = endingCardTypes.find((option) => option.value === newType);
|
||||
if (!selectedOption?.disabled) {
|
||||
if (newType === "redirectToUrl") {
|
||||
updateSurvey({ type: "redirectToUrl" });
|
||||
} else {
|
||||
updateSurvey({ type: "endScreen" });
|
||||
}
|
||||
}
|
||||
}}
|
||||
disabled={isRedirectToUrlDisabled}
|
||||
/>
|
||||
</TooltipRenderer>
|
||||
{endingCard.type === "endScreen" && (
|
||||
|
||||
@@ -8,10 +8,10 @@ import { LocalizedEditor } from "@formbricks/ee/multi-language/components/locali
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TAttributeClass } from "@formbricks/types/attribute-classes";
|
||||
import { TSurvey, TSurveyWelcomeCard } from "@formbricks/types/surveys/types";
|
||||
import { FileInput } from "@formbricks/ui/FileInput";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/QuestionFormInput";
|
||||
import { Switch } from "@formbricks/ui/Switch";
|
||||
import { FileInput } from "@formbricks/ui/components/FileInput";
|
||||
import { Label } from "@formbricks/ui/components/Label";
|
||||
import { QuestionFormInput } from "@formbricks/ui/components/QuestionFormInput";
|
||||
import { Switch } from "@formbricks/ui/components/Switch";
|
||||
|
||||
interface EditWelcomeCardProps {
|
||||
localSurvey: TSurvey;
|
||||
@@ -110,7 +110,7 @@ export const EditWelcomeCard = ({
|
||||
<div className="mt-3 flex w-full items-center justify-center">
|
||||
<FileInput
|
||||
id="welcome-card-image"
|
||||
allowedFileExtensions={["png", "jpeg", "jpg"]}
|
||||
allowedFileExtensions={["png", "jpeg", "jpg", "webp"]}
|
||||
environmentId={environmentId}
|
||||
onFileUpload={(url: string[]) => {
|
||||
updateSurvey({ fileUrl: url[0] });
|
||||
|
||||