Compare commits
210 Commits
@formbrick
...
@formbrick
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
315467ef3f | ||
|
|
3dde021cd0 | ||
|
|
ebbde2b531 | ||
|
|
47a8fd6b62 | ||
|
|
c6686209be | ||
|
|
09436c78fc | ||
|
|
98cdf941e6 | ||
|
|
52a09aa3ae | ||
|
|
bf028a5f64 | ||
|
|
5c60694117 | ||
|
|
179f92077b | ||
|
|
9cdf446f65 | ||
|
|
f98d4f5c11 | ||
|
|
142c1bd35b | ||
|
|
5e3ec7e4f0 | ||
|
|
3bbb4170e2 | ||
|
|
dc085c41c0 | ||
|
|
33b3887b84 | ||
|
|
6a8805de0b | ||
|
|
10e149bb02 | ||
|
|
9f944249fc | ||
|
|
b5765fed74 | ||
|
|
eee9b29723 | ||
|
|
a3aae4ab95 | ||
|
|
5b9db8f353 | ||
|
|
9b98ca4f64 | ||
|
|
6572d5395b | ||
|
|
cd753f1a67 | ||
|
|
6f0a26904f | ||
|
|
e1c8a715d1 | ||
|
|
96e54dbb46 | ||
|
|
b71fdf3205 | ||
|
|
5520edb2c5 | ||
|
|
d824da610d | ||
|
|
2bebc9598c | ||
|
|
580e51dcea | ||
|
|
b570f3c79d | ||
|
|
cf94c1a6d1 | ||
|
|
71832c590f | ||
|
|
15050525fd | ||
|
|
dcc198b151 | ||
|
|
3793f29d0a | ||
|
|
b6da482e3f | ||
|
|
cd1d9196fc | ||
|
|
e3c09ebec3 | ||
|
|
2bda12d4fc | ||
|
|
b072d3b549 | ||
|
|
758fc9af4d | ||
|
|
d4a4b4ec41 | ||
|
|
5aa38a6e39 | ||
|
|
0ebef13805 | ||
|
|
a81ceff09e | ||
|
|
7ebdf9939e | ||
|
|
7631783e7d | ||
|
|
c92b2b00e0 | ||
|
|
e68a7fe763 | ||
|
|
a3b46ee532 | ||
|
|
a1a66ef6be | ||
|
|
6a1b8106b7 | ||
|
|
8b1a074e2c | ||
|
|
57733a75fc | ||
|
|
e5ef71ae87 | ||
|
|
89dae8f1d8 | ||
|
|
370041b0ae | ||
|
|
34ff14d43b | ||
|
|
b6c0dbf5d3 | ||
|
|
488e2801f0 | ||
|
|
ae7d0a4846 | ||
|
|
7d3fa70fe2 | ||
|
|
bb4052690e | ||
|
|
e8a286bd4e | ||
|
|
205593d8d3 | ||
|
|
37afd004af | ||
|
|
ad86c4dbf4 | ||
|
|
fb64eb50a2 | ||
|
|
8d7eeb045b | ||
|
|
fdb1aa2299 | ||
|
|
df9ff011f7 | ||
|
|
e85d95a4eb | ||
|
|
5c9605f4af | ||
|
|
de3d580614 | ||
|
|
ccb89548f0 | ||
|
|
ad42f4cc55 | ||
|
|
51dda67992 | ||
|
|
0598ad2eaa | ||
|
|
44e48e3c3f | ||
|
|
1551baeca7 | ||
|
|
c3f26f7ab8 | ||
|
|
e9e3de2ce8 | ||
|
|
34c4e9bc1a | ||
|
|
c707896eb6 | ||
|
|
ee545b7ade | ||
|
|
7bf0fa450a | ||
|
|
1a8618692a | ||
|
|
e5f371476c | ||
|
|
dba3677633 | ||
|
|
369c9ed7b2 | ||
|
|
0776138c1c | ||
|
|
235c1afe28 | ||
|
|
17b9d686bd | ||
|
|
73904e11a6 | ||
|
|
c423e43aee | ||
|
|
a1b447caad | ||
|
|
6b989487b2 | ||
|
|
d60e0c4e5c | ||
|
|
2a3ab3280f | ||
|
|
5b217e5483 | ||
|
|
ec0d3f2fa2 | ||
|
|
ae702ddd06 | ||
|
|
91f78d875b | ||
|
|
08110b0c34 | ||
|
|
42e6601f13 | ||
|
|
a5c33981a0 | ||
|
|
1a90d1b7e8 | ||
|
|
712431e842 | ||
|
|
3905c2227e | ||
|
|
86da5ff2f4 | ||
|
|
fc0feda5e9 | ||
|
|
3d0d633bc8 | ||
|
|
d707e2e49e | ||
|
|
288fc79366 | ||
|
|
000fcf8b02 | ||
|
|
730f0ba1e9 | ||
|
|
eed9b6635d | ||
|
|
38d3de2165 | ||
|
|
5d380a4986 | ||
|
|
892a58c45e | ||
|
|
4fb9851a6d | ||
|
|
09c37d78a2 | ||
|
|
6335565bf9 | ||
|
|
e32e47e272 | ||
|
|
e864829a79 | ||
|
|
b62a344e54 | ||
|
|
469590c2f6 | ||
|
|
5ae7f31d01 | ||
|
|
cb4cd706ad | ||
|
|
a165143c2a | ||
|
|
6b3f977d83 | ||
|
|
f743fb18fb | ||
|
|
1e816eb6d9 | ||
|
|
8c31c71251 | ||
|
|
62a08f304b | ||
|
|
35057322a4 | ||
|
|
b1a93de8db | ||
|
|
aca32655cd | ||
|
|
8b14559d5f | ||
|
|
43a623a61e | ||
|
|
2f8257ae62 | ||
|
|
8a5217b39c | ||
|
|
57e6c86e6a | ||
|
|
4519cb8a2d | ||
|
|
e5d06de68e | ||
|
|
49f61e2eeb | ||
|
|
4763cf3217 | ||
|
|
b6a0d0fe5d | ||
|
|
9be053d8a6 | ||
|
|
3a17c6b085 | ||
|
|
16b7e0d82a | ||
|
|
ae7075c746 | ||
|
|
78ed48adeb | ||
|
|
03ba9ecb58 | ||
|
|
d360c1f741 | ||
|
|
cf953db18f | ||
|
|
7475df147d | ||
|
|
06d620dbc8 | ||
|
|
feadefa90d | ||
|
|
754832f097 | ||
|
|
dad8ebe8da | ||
|
|
df27b4703f | ||
|
|
e385638c14 | ||
|
|
8cacb2ccee | ||
|
|
98a62949d5 | ||
|
|
dd6ac2e4cd | ||
|
|
c52df00d39 | ||
|
|
0c7c3c9ad2 | ||
|
|
5612fbfa22 | ||
|
|
186a4269a3 | ||
|
|
503e7649e2 | ||
|
|
e3b4ec4a9e | ||
|
|
b20cda2d06 | ||
|
|
6e8be0c0bd | ||
|
|
c68a9c8d15 | ||
|
|
e23f26f48c | ||
|
|
9e9fe5c09d | ||
|
|
3bb4a34e4c | ||
|
|
9fcc0a360c | ||
|
|
3824d95151 | ||
|
|
5bfaad9484 | ||
|
|
a5c8c1aa85 | ||
|
|
c92011b069 | ||
|
|
856247763c | ||
|
|
2199a4b102 | ||
|
|
cd49d687ad | ||
|
|
2852bf617a | ||
|
|
30623d5fd2 | ||
|
|
b673044890 | ||
|
|
bf9189c1af | ||
|
|
e262006f7e | ||
|
|
d44ea1f32d | ||
|
|
e24f6cd017 | ||
|
|
c6a4b7731f | ||
|
|
62d2c1af18 | ||
|
|
2ea3d42ff6 | ||
|
|
e275553425 | ||
|
|
1a83373099 | ||
|
|
701a7d9786 | ||
|
|
4410b14d0c | ||
|
|
cddbbc2be2 | ||
|
|
4fefa09ee8 | ||
|
|
09106188ba |
51
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,51 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: "Found a bug? Please fill out the sections below. \U0001F44D"
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
### Issue Summary
|
||||
|
||||
<!--
|
||||
A summary of the issue. This needs to be a clear detailed-rich summary.
|
||||
-->
|
||||
|
||||
(Write your answer here.)
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
1. (for example) Went to ...
|
||||
2. Clicked on...
|
||||
3. ...
|
||||
|
||||
### Expected behavior
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
### Other information
|
||||
|
||||
#### Screenshots
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
#### Environment
|
||||
|
||||
- [ ] Formbricks Cloud (app.formbricks.com)
|
||||
- [ ] self-hosted Formbricks, version/commit: [please provide]
|
||||
|
||||
#### Desktop (please complete the following information):
|
||||
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
#### Node.JS version
|
||||
|
||||
[e.g. v18.15.0]
|
||||
|
||||
#### Anything else?
|
||||
|
||||
- Screen recording, console logs, network requests: You can make a recording with [Loom](https://www.loom.com).
|
||||
- Anything else that you think could be an issue?
|
||||
81
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: Bug report
|
||||
description: "Found a bug? Please fill out the sections below. \U0001F44D"
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: issue-summary
|
||||
attributes:
|
||||
label: Issue Summary
|
||||
description: A summary of the issue. This needs to be a clear detailed-rich summary.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps-to-reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
value: |
|
||||
1. (for example) Went to ...
|
||||
2. Clicked on...
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected-behavior
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: other-information
|
||||
attributes:
|
||||
label: Other information
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: If applicable, add screenshots to help explain your problem.
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
options:
|
||||
- label: Formbricks Cloud (app.formbricks.com)
|
||||
- label: Self-hosted Formbricks
|
||||
- type: textarea
|
||||
id: desktop-version
|
||||
attributes:
|
||||
label: Desktop (please complete the following information)
|
||||
description: |
|
||||
examples:
|
||||
- **OS**: [e.g. iOS]
|
||||
- **Browser**: [e.g. chrome, safari]
|
||||
- **Version**: [e.g. 22]
|
||||
value: |
|
||||
- OS:
|
||||
- Node:
|
||||
- npm:
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
id: nodejs-version
|
||||
attributes:
|
||||
value: |
|
||||
#### Node.JS version
|
||||
|
||||
[e.g. v18.15.0]
|
||||
- type: markdown
|
||||
id: anything-else
|
||||
attributes:
|
||||
value: |
|
||||
#### Anything else?
|
||||
|
||||
- Screen recording, console logs, network requests: You can make a recording with [Loom](https://www.loom.com).
|
||||
- Anything else that you think could be an issue?
|
||||
26
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: "Suggest an idea for this project \U0001F680"
|
||||
title: "[FEATURE]"
|
||||
labels: enhancement
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
### How we code at Formbricks 🤓
|
||||
|
||||
- Everything is type-safe
|
||||
- All UI components are in the package `formbricks/ui`
|
||||
- Run `pnpm dev` to find a demo app to test in-app surveys at `localhost:3002`
|
||||
- We use **chatGPT** to help refactor code. Use our [Formbricks ✨ megaprompt ✨](https://github.com/formbricks/formbricks/blob/main/megaprompt.md) to create the right context before you write your prompt.
|
||||
45
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Feature request
|
||||
description: "Suggest an idea for this project \U0001F680"
|
||||
title: "[FEATURE]"
|
||||
labels: enhancement
|
||||
assignees: []
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem-description
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution-description
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternate-solution-description
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
- type: markdown
|
||||
id: formbricks-info
|
||||
attributes:
|
||||
value: |
|
||||
### How we code at Formbricks 🤓
|
||||
|
||||
- Everything is type-safe
|
||||
- All UI components are in the package `formbricks/ui`
|
||||
- Run `pnpm dev` to find a demo app to test in-app surveys at `localhost:3002`
|
||||
- We use **chatGPT** to help refactor code. Use our [Formbricks ✨ megaprompt ✨](https://github.com/formbricks/formbricks/blob/main/megaprompt.md) to create the right
|
||||
context before you write your prompt.
|
||||
8
.github/workflows/cron-closeOnDate.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Cron - weeklySummary
|
||||
name: Cron - closeOnDate
|
||||
|
||||
on:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
@@ -10,14 +10,14 @@ jobs:
|
||||
cron-weeklySummary:
|
||||
env:
|
||||
APP_URL: ${{ secrets.APP_URL }}
|
||||
CRON_API_KEY: ${{ secrets.CRON_SECRET }}
|
||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.CRON_SECRET }}
|
||||
run: |
|
||||
curl ${{ secrets.APP_URL }}/api/cron/close_surveys \
|
||||
curl ${{ env.APP_URL }}/api/cron/close_surveys \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'authorization: ${{ secrets.CRON_SECRET }}' \
|
||||
-H 'x-api-key: ${{ env.CRON_SECRET }}' \
|
||||
--fail
|
||||
|
||||
6
.github/workflows/cron-weeklySummary.yml
vendored
@@ -10,14 +10,14 @@ jobs:
|
||||
cron-weeklySummary:
|
||||
env:
|
||||
APP_URL: ${{ secrets.APP_URL }}
|
||||
CRON_API_KEY: ${{ secrets.CRON_SECRET }}
|
||||
CRON_SECRET: ${{ secrets.CRON_SECRET }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: cURL request
|
||||
if: ${{ env.APP_URL && env.CRON_SECRET }}
|
||||
run: |
|
||||
curl ${{ secrets.APP_URL }}/api/cron/weekly_summary \
|
||||
curl ${{ env.APP_URL }}/api/cron/weekly_summary \
|
||||
-X POST \
|
||||
-H 'content-type: application/json' \
|
||||
-H 'x-api-key: ${{ secrets.CRON_SECRET }}' \
|
||||
-H 'x-api-key: ${{ env.CRON_SECRET }}' \
|
||||
--fail
|
||||
|
||||
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2023 Matthias Nannt, Johannes Dancker
|
||||
Copyright (c) 2023 Formbricks GmbH
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"next": "13.4.9",
|
||||
"next": "13.4.12",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
|
||||
@@ -1,53 +1,76 @@
|
||||
import fbsetup from "../../public/fb-setup.png";
|
||||
import formbricks from "@formbricks/js";
|
||||
import Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import fbsetup from "../../public/fb-setup.png";
|
||||
|
||||
export default function AppPage({}) {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (darkMode) {
|
||||
document.body.classList.add("dark");
|
||||
} else {
|
||||
document.body.classList.remove("dark");
|
||||
}
|
||||
}, [darkMode]);
|
||||
|
||||
return (
|
||||
<div className="px-12 py-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">Formbricks In-product Survey Demo App</h1>
|
||||
<p className="text-slate-700">
|
||||
This app helps you test your in-app surveys. You can create an test user actions, create and update
|
||||
user attributes, etc.
|
||||
</p>
|
||||
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
|
||||
Formbricks In-product Survey Demo App
|
||||
</h1>
|
||||
<p className="text-slate-700 dark:text-slate-300">
|
||||
This app helps you test your in-app surveys. You can create and test user actions, create and
|
||||
update user attributes, etc.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
className="rounded-lg bg-slate-200 px-6 py-1 dark:bg-slate-700 dark:text-slate-100"
|
||||
onClick={() => setDarkMode(!darkMode)}>
|
||||
Toggle Dark Mode
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="my-4 grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<div className="rounded-lg border border-slate-300 bg-slate-100 p-6">
|
||||
<h3 className="text-lg font-semibold">Setup .env</h3>
|
||||
<p className="text-slate-700">
|
||||
<div className="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 text-slate-900 dark:text-white">Setup .env</h3>
|
||||
<p className="text-slate-700 dark:text-slate-300">
|
||||
Copy the environment ID of your Formbricks app to the env variable in demo/.env
|
||||
</p>
|
||||
<Image src={fbsetup} alt="fb setup" className="mt-4 rounded" priority />
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg border border-slate-300 bg-slate-100 p-6">
|
||||
<h3 className="text-lg font-semibold">Widget Logs</h3>
|
||||
<p className="text-slate-700">
|
||||
Look at the logs to understand how the widget works. <strong>Open your browser console</strong>{" "}
|
||||
to see the logs.
|
||||
<div className="mt-4 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 text-slate-900 dark:text-white">Widget Logs</h3>
|
||||
<p className="text-slate-700 dark:text-slate-300">
|
||||
Look at the logs to understand how the widget works.{" "}
|
||||
<strong className="dark:text-white">Open your browser console</strong> to see the logs.
|
||||
</p>
|
||||
{/* <div className="max-h-[40vh] overflow-y-auto py-4">
|
||||
{/* <div className="max-h-[40vh] overflow-y-auto py-4">
|
||||
<LogsContainer />
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="md:grid md:grid-cols-3">
|
||||
<div className="col-span-3 rounded-lg border border-slate-300 bg-slate-100 p-6">
|
||||
<h3 className="text-lg font-semibold">Reset person / pull data from Formbricks app</h3>
|
||||
<p className="text-slate-700">
|
||||
<div className="col-span-3 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-gray-600 dark:bg-gray-800">
|
||||
<h3 className="text-lg font-semibold dark:text-white">
|
||||
Reset person / pull data from Formbricks app
|
||||
</h3>
|
||||
<p className="text-slate-700 dark:text-gray-300">
|
||||
On formbricks.logout() a few things happen: <strong>New person is created</strong> and{" "}
|
||||
<strong>surveys & no-code actions are pulled from Formbricks:</strong>.
|
||||
</p>
|
||||
<button
|
||||
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700"
|
||||
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
formbricks.logout();
|
||||
}}>
|
||||
Logout
|
||||
</button>
|
||||
<p className="text-xs text-slate-700">
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">
|
||||
If you made a change in Formbricks app and it does not seem to work, hit 'Logout' and
|
||||
try again.
|
||||
</p>
|
||||
@@ -56,7 +79,7 @@ export default function AppPage({}) {
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700"
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
|
||||
onClick={() => {
|
||||
formbricks.track("Code Action");
|
||||
}}>
|
||||
@@ -64,7 +87,7 @@ export default function AppPage({}) {
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700">
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">
|
||||
This button sends a{" "}
|
||||
<a href="https://formbricks.com/docs/actions/code" className="underline" target="_blank">
|
||||
Code Action
|
||||
@@ -75,18 +98,24 @@ export default function AppPage({}) {
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div>
|
||||
<button className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700">
|
||||
<button className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
|
||||
No-Code Action
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700">
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">
|
||||
This button sends a{" "}
|
||||
<a href="https://formbricks.com/docs/actions/no-code" className="underline" target="_blank">
|
||||
<a
|
||||
href="https://formbricks.com/docs/actions/no-code"
|
||||
className="underline dark:text-blue-500"
|
||||
target="_blank">
|
||||
No Code Action
|
||||
</a>{" "}
|
||||
as long as you created it beforehand in the Formbricks App.{" "}
|
||||
<a href="https://formbricks.com/docs/actions/no-code" target="_blank" className="underline">
|
||||
<a
|
||||
href="https://formbricks.com/docs/actions/no-code"
|
||||
target="_blank"
|
||||
className="underline dark:text-blue-500">
|
||||
Here are instructions on how to do it.
|
||||
</a>
|
||||
</p>
|
||||
@@ -98,17 +127,17 @@ export default function AppPage({}) {
|
||||
onClick={() => {
|
||||
formbricks.setAttribute("Plan", "Free");
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700">
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
|
||||
Set Plan to 'Free'
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700">
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">
|
||||
This button sets the{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/attributes/custom-attributes"
|
||||
target="_blank"
|
||||
className="underline">
|
||||
className="underline dark:text-blue-500">
|
||||
attribute
|
||||
</a>{" "}
|
||||
'Plan' to 'Free'. If the attribute does not exist, it creates it.
|
||||
@@ -121,17 +150,17 @@ export default function AppPage({}) {
|
||||
onClick={() => {
|
||||
formbricks.setAttribute("Plan", "Paid");
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700">
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
|
||||
Set Plan to 'Paid'
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700">
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">
|
||||
This button sets the{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/attributes/custom-attributes"
|
||||
target="_blank"
|
||||
className="underline">
|
||||
className="underline dark:text-blue-500">
|
||||
attribute
|
||||
</a>{" "}
|
||||
'Plan' to 'Paid'. If the attribute does not exist, it creates it.
|
||||
@@ -144,17 +173,17 @@ export default function AppPage({}) {
|
||||
onClick={() => {
|
||||
formbricks.setEmail("test@web.com");
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700">
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
|
||||
Set Email
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700">
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">
|
||||
This button sets the{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/attributes/identify-users"
|
||||
target="_blank"
|
||||
className="underline">
|
||||
className="underline dark:text-blue-500">
|
||||
user email
|
||||
</a>{" "}
|
||||
'test@web.com'
|
||||
@@ -167,17 +196,17 @@ export default function AppPage({}) {
|
||||
onClick={() => {
|
||||
formbricks.setUserId("THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING");
|
||||
}}
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700">
|
||||
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
|
||||
Set User ID
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-700">
|
||||
<p className="text-xs text-slate-700 dark:text-gray-300">
|
||||
This button sets an external{" "}
|
||||
<a
|
||||
href="https://formbricks.com/docs/attributes/identify-users"
|
||||
target="_blank"
|
||||
className="underline">
|
||||
className="underline dark:text-blue-500">
|
||||
user ID
|
||||
</a>{" "}
|
||||
to 'THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING'
|
||||
|
||||
@@ -5,6 +5,7 @@ module.exports = {
|
||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { handleFeedbackSubmit, updateFeedback } from "../../lib/handleFeedbackSubmit";
|
||||
import { Popover, PopoverTrigger, PopoverContent } from "@formbricks/ui";
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
export const DocsFeedback: React.FC = () => {
|
||||
const router = useRouter();
|
||||
@@ -26,7 +25,7 @@ export const DocsFeedback: React.FC = () => {
|
||||
Is everything on this page clear?
|
||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
||||
<div className="mt-2 inline-flex space-x-3 md:ml-4 md:mt-0">
|
||||
{["Yes 👍", " No 👎"].map((option) => (
|
||||
{["Yes 👍", "No 👎"].map((option) => (
|
||||
<PopoverTrigger
|
||||
key={option}
|
||||
className="rounded border border-slate-200 bg-slate-50 px-4 py-2 text-slate-900 hover:bg-slate-100 hover:text-slate-600 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-slate-700 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600 dark:hover:text-slate-300"
|
||||
|
||||
@@ -2,18 +2,18 @@ import { CodeFileIcon, EyeIcon, HandPuzzleIcon } from "@formbricks/ui";
|
||||
import HeadingCentered from "../shared/HeadingCentered";
|
||||
|
||||
const features = [
|
||||
{
|
||||
id: "compliance",
|
||||
name: "Smoothly Compliant",
|
||||
description: "Use our GDPR-compliant Cloud or self-host the entire solution.",
|
||||
icon: EyeIcon,
|
||||
},
|
||||
{
|
||||
id: "customizable",
|
||||
name: "Fully Customizable",
|
||||
description: "Full customizability and extendability. Integrate with your stack easily.",
|
||||
icon: HandPuzzleIcon,
|
||||
},
|
||||
{
|
||||
id: "compliance",
|
||||
name: "Smoothly Compliant",
|
||||
description: "Self-host the entire product and fly through privacy compliance reviews.",
|
||||
icon: EyeIcon,
|
||||
},
|
||||
{
|
||||
id: "independent",
|
||||
name: "Stay independent",
|
||||
@@ -27,9 +27,9 @@ export const Features: React.FC = () => {
|
||||
<div className="relative mx-auto max-w-7xl">
|
||||
<HeadingCentered
|
||||
closer
|
||||
teaser="DATA Privacy at heart"
|
||||
teaser="Data Privacy at heart"
|
||||
heading="The only open-source solution"
|
||||
subheading="Comply with all data privacy regulation with ease. Simply self-host."
|
||||
subheading="Comply with all data privacy regulation with ease. Self-host if you want."
|
||||
/>
|
||||
|
||||
<ul role="list" className="grid grid-cols-1 gap-4 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:gap-10">
|
||||
|
||||
@@ -5,8 +5,11 @@ import Link from "next/link";
|
||||
|
||||
export const GitHubSponsorship: React.FC = () => {
|
||||
return (
|
||||
<div className="xs:mx-auto xs:w-full relative mx-auto my-4 mb-12 mt-12 rounded-xl bg-gradient-to-br from-slate-100 to-slate-200 px-4 py-8 dark:from-slate-800 dark:via-slate-800 dark:to-slate-700 sm:px-6 sm:pb-12 sm:pt-8 md:max-w-none lg:mt-6 lg:px-8 lg:pt-8 ">
|
||||
<div className="right-10 lg:absolute">
|
||||
<div className="mx-4 my-4 mb-12 mt-12 rounded-xl bg-gradient-to-br from-slate-100 to-slate-200 px-4 py-8 dark:from-slate-800 dark:via-slate-800 dark:to-slate-700 sm:px-6 sm:pb-12 sm:pt-8 md:max-w-none lg:mt-6 lg:px-8 lg:pt-8">
|
||||
<style jsx>{`
|
||||
@media (min-width: 426px);
|
||||
`}</style>
|
||||
<div className="right-24 lg:absolute">
|
||||
<Image
|
||||
src={GitHubMarkDark}
|
||||
alt="GitHub Sponsors Formbricks badge"
|
||||
|
||||
@@ -22,7 +22,7 @@ export const Hero: React.FC = ({}) => {
|
||||
<a
|
||||
href="https://github.com/formbricks/formbricks"
|
||||
target="_blank"
|
||||
className="border-brand-dark rounded-full border px-6 py-1.5 text-sm text-slate-500 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800">
|
||||
className="border-brand-dark rounded-full border px-4 py-1.5 text-sm text-slate-500 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800">
|
||||
We're Open-Source | Star us on GitHub{" "}
|
||||
<ChevronRightIcon className="inline h-5 w-5 text-slate-300" />
|
||||
</a>
|
||||
@@ -43,7 +43,7 @@ export const Hero: React.FC = ({}) => {
|
||||
<p className="hidden whitespace-nowrap pt-3 text-xs text-slate-400 dark:text-slate-500 md:block">
|
||||
Trusted by
|
||||
</p>
|
||||
<div className="grid grid-cols-3 items-center gap-8 pt-2 md:grid-cols-4">
|
||||
<div className="grid grid-cols-4 items-center gap-5 pt-2 md:gap-8">
|
||||
<Image
|
||||
src={CalLogoLight}
|
||||
alt="Cal Logo"
|
||||
|
||||
@@ -10,6 +10,7 @@ interface APICallProps {
|
||||
label: string;
|
||||
type: string;
|
||||
description: string;
|
||||
required?: boolean;
|
||||
}[];
|
||||
bodies: {
|
||||
label: string;
|
||||
@@ -69,7 +70,13 @@ export function APILayout({ method, url, description, headers, bodies, responses
|
||||
<p className="not-prose -mb-1 pt-2 font-bold">Headers</p>
|
||||
<div>
|
||||
{headers.map((q) => (
|
||||
<Parameter key={q.label} label={q.label} type={q.type} description={q.description} />
|
||||
<Parameter
|
||||
key={q.label}
|
||||
label={q.label}
|
||||
type={q.type}
|
||||
description={q.description}
|
||||
required={q.required}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
36
apps/formbricks-com/components/shared/AuthorBox.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import Image from "next/image";
|
||||
import AuthorJohannes from "@/images/blog/johannes-co-founder-formbricks-small.jpg";
|
||||
|
||||
interface AuthorBoxProps {
|
||||
name: string;
|
||||
title: string;
|
||||
date: string;
|
||||
duration: string;
|
||||
}
|
||||
|
||||
export default function AuthorBox({ name, title, date, duration }: AuthorBoxProps) {
|
||||
return (
|
||||
<div className="mb-8 flex items-center space-x-4 rounded-lg border border-slate-200 bg-slate-100 px-6 py-3 dark:border-slate-700 dark:bg-slate-800">
|
||||
<Image
|
||||
className="m-0 rounded-full"
|
||||
src={AuthorJohannes}
|
||||
alt={name}
|
||||
width={45}
|
||||
height={45}
|
||||
quality={100}
|
||||
placeholder="blur"
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
<div className="flex w-full items-end justify-between">
|
||||
<div>
|
||||
<p className="m-0 font-medium text-slate-600 dark:text-slate-300">{name}</p>
|
||||
<p className="m-0 text-sm text-slate-400">{title}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="m-0 font-medium text-slate-600 dark:text-slate-300">{duration} Minutes</p>
|
||||
<p className="m-0 text-sm text-slate-400">{date}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
import { Icon } from "@/components/shared/Icon";
|
||||
|
||||
const styles = {
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function Footer() {
|
||||
<p className="text-base text-slate-500 dark:text-slate-400">Privacy-first Experience Management</p>
|
||||
<div className="border-slate-500">
|
||||
<p className="text-sm text-slate-400 dark:text-slate-500">
|
||||
© 2022. All rights reserved.
|
||||
Formbricks GmbH © 2022. All rights reserved.
|
||||
<br />
|
||||
<Link href="/imprint">Imprint</Link> | <Link href="/privacy">Privacy Policy</Link> |{" "}
|
||||
<Link href="/terms">Terms</Link> | <Link href="/oss-friends">OSS Friends</Link>
|
||||
|
||||
@@ -26,6 +26,10 @@ interface Props {
|
||||
meta: {
|
||||
title: string;
|
||||
description: string;
|
||||
publishedTime: string;
|
||||
authors: string[];
|
||||
section: string;
|
||||
tags: string[];
|
||||
};
|
||||
children: JSX.Element;
|
||||
}
|
||||
@@ -34,7 +38,14 @@ export default function LayoutMdx({ meta, children }: Props) {
|
||||
useExternalLinks(".prose a");
|
||||
return (
|
||||
<div className="flex h-screen flex-col justify-between">
|
||||
<MetaInformation title={meta.title} description={meta.description} />
|
||||
<MetaInformation
|
||||
title={meta.title}
|
||||
description={meta.description}
|
||||
publishedTime={meta.publishedTime}
|
||||
authors={meta.authors}
|
||||
section={meta.section}
|
||||
tags={meta.tags}
|
||||
/>
|
||||
<Header />
|
||||
<main className="min-w-0 max-w-2xl flex-auto px-4 lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16">
|
||||
<article className="mx-auto my-16 max-w-3xl px-2">
|
||||
|
||||
@@ -3,10 +3,21 @@ import Head from "next/head";
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
publishedTime?: string;
|
||||
authors?: string[];
|
||||
section?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export default function MetaInformation({ title, description }: Props) {
|
||||
const pageTitle = `${title} | Open-Source Survey Software`;
|
||||
export default function MetaInformation({
|
||||
title,
|
||||
description,
|
||||
publishedTime,
|
||||
authors,
|
||||
section,
|
||||
tags,
|
||||
}: Props) {
|
||||
const pageTitle = `${title} | Open-Source Experience Management, Privacy-first`;
|
||||
return (
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
@@ -14,13 +25,22 @@ export default function MetaInformation({ title, description }: Props) {
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={`https://${process.env.VERCEL_URL}/social-image.png`} />
|
||||
<meta property="og:image:alt" content="Formbricks - Open Source Form and Survey Infrastructure" />
|
||||
<meta property="og:image:alt" content="Formbricks - Open Source Experience Management, Privacy-first" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Open Source Forms and Surveys by Formbricks" />
|
||||
<meta property="og:site_name" content="Open Source Experience Management, Privacy-first" />
|
||||
<meta property="article:publisher" content="Formbricks" />
|
||||
{publishedTime && <meta property="article:published_time" content={publishedTime} />}
|
||||
{authors && <meta property="article:author" content={authors.join(", ")} />}
|
||||
{section && <meta property="article:section" content={section} />}
|
||||
{tags && <meta property="article:tag" content={tags.join(", ")} />}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@formbricks" />
|
||||
<meta name="twitter:creator" content="@formbricks" />
|
||||
<meta name="theme-color" content="#00C4B8" />
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,39 +26,39 @@ const tiers = [
|
||||
href: "/docs/self-hosting/deployment",
|
||||
},
|
||||
{
|
||||
name: "Free",
|
||||
name: "Cloud",
|
||||
href: "https://app.formbricks.com/auth/signup",
|
||||
priceMonthly: "$0",
|
||||
paymentRythm: "/month",
|
||||
button: "highlight",
|
||||
discounted: false,
|
||||
highlight: true,
|
||||
description: "All Pro features included.",
|
||||
description: "Start with the 'Free forever' plan.",
|
||||
features: [
|
||||
"Unlimited surveys",
|
||||
"Unlimited team members",
|
||||
"Remove branding",
|
||||
"Granular targeting",
|
||||
"In-product surveys",
|
||||
"Link surveys",
|
||||
"Remove branding",
|
||||
"Granular targeting",
|
||||
"30+ templates",
|
||||
"API access",
|
||||
"Integrations (Slack, PostHog, Zapier)",
|
||||
"Integrations (Zapier, Make, ...)",
|
||||
"Unlimited team members",
|
||||
"100 responses per survey",
|
||||
],
|
||||
ctaName: "Start for free",
|
||||
ctaName: "Get started",
|
||||
plausibleGoal: "Pricing_CTA_FreePlan",
|
||||
},
|
||||
{
|
||||
name: "Pro",
|
||||
name: "Cloud Pro",
|
||||
href: "https://app.formbricks.com/auth/signup",
|
||||
priceMonthly: "$99",
|
||||
paymentRythm: "/month",
|
||||
button: "secondary",
|
||||
discounted: false,
|
||||
highlight: false,
|
||||
description: "All features included. Unlimited usage.",
|
||||
features: ["Unlimited responses per survey"],
|
||||
description: "All features, unlimited usage.",
|
||||
features: ["Everything in 'Cloud'", "Unlimited responses per survey"],
|
||||
ctaName: "Start for free",
|
||||
plausibleGoal: "Pricing_CTA_ProPlan",
|
||||
},
|
||||
@@ -146,9 +146,12 @@ export default function Pricing() {
|
||||
{tier.ctaName}
|
||||
</Button>
|
||||
|
||||
{tier.name !== "Self-hosting" && (
|
||||
{tier.name == "Cloud Pro" && (
|
||||
<p className="mt-1.5 text-center text-xs text-slate-500">No Creditcard required.</p>
|
||||
)}
|
||||
{tier.name == "Cloud" && (
|
||||
<p className="mt-1.5 text-center text-xs text-slate-500">Free forever 🤍</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -78,6 +78,7 @@ const navigation = [
|
||||
{ title: "Get Webhook", href: "/docs/webhook-api/get-webhook" },
|
||||
{ title: "Create Webhook", href: "/docs/webhook-api/create-webhook" },
|
||||
{ title: "Delete Webhook", href: "/docs/webhook-api/delete-webhook" },
|
||||
{ title: "Webhook Payload", href: "/docs/webhook-api/webhook-payload" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -91,12 +91,16 @@ const nextConfig = {
|
||||
destination: "/docs/actions/code",
|
||||
permanent: true,
|
||||
},
|
||||
|
||||
{
|
||||
source: "/pmf",
|
||||
destination: "/",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/blog/v1-and-how-we-got-here",
|
||||
destination: "/blog/experience-management-open-source",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,22 +11,22 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/embed-react": "^1.2.2",
|
||||
"@calcom/embed-react": "^1.3.0",
|
||||
"@docsearch/react": "^3.5.1",
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"@headlessui/react": "^1.7.15",
|
||||
"@headlessui/react": "^1.7.16",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@mapbox/rehype-prism": "^0.8.0",
|
||||
"@mdx-js/loader": "^2.3.0",
|
||||
"@mdx-js/react": "^2.3.0",
|
||||
"@next/mdx": "^13.4.9",
|
||||
"@next/mdx": "^13.4.12",
|
||||
"@paralleldrive/cuid2": "^2.2.1",
|
||||
"clsx": "^1.2.1",
|
||||
"clsx": "^2.0.0",
|
||||
"lottie-web": "^5.12.2",
|
||||
"next": "13.4.9",
|
||||
"next-plausible": "^3.9.1",
|
||||
"next": "13.4.12",
|
||||
"next-plausible": "^3.10.1",
|
||||
"next-sitemap": "^4.1.8",
|
||||
"prism-react-renderer": "^2.0.6",
|
||||
"prismjs": "^1.29.0",
|
||||
@@ -35,7 +35,7 @@
|
||||
"react-icons": "^4.10.1",
|
||||
"react-responsive-embed": "^2.1.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.32.2"
|
||||
"sharp": "^0.32.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
|
||||
138
apps/formbricks-com/pages/api/oss-friends/index.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
|
||||
// GET
|
||||
if (req.method === "GET") {
|
||||
return res.status(200).json({
|
||||
data: [
|
||||
{
|
||||
name: "Appsmith",
|
||||
description: "Build build custom software on top of your data.",
|
||||
href: "https://www.appsmith.com",
|
||||
},
|
||||
{
|
||||
name: "BoxyHQ",
|
||||
description:
|
||||
"BoxyHQ’s suite of APIs for security and privacy helps engineering teams build and ship compliant cloud applications faster.",
|
||||
href: "https://boxyhq.com",
|
||||
},
|
||||
{
|
||||
name: "Cal.com",
|
||||
description:
|
||||
"Cal.com is a scheduling tool that helps you schedule meetings without the back-and-forth emails.",
|
||||
href: "https://cal.com",
|
||||
},
|
||||
{
|
||||
name: "Crowd.dev",
|
||||
description:
|
||||
"Centralize community, product, and customer data to understand which companies are engaging with your open source project.",
|
||||
href: "https://www.crowd.dev",
|
||||
},
|
||||
{
|
||||
name: "Documenso",
|
||||
description:
|
||||
"The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.",
|
||||
href: "https://documenso.com",
|
||||
},
|
||||
{
|
||||
name: "Erxes",
|
||||
description:
|
||||
"The Open-Source HubSpot Alternative. A single XOS enables to create unique and life-changing experiences that work for all types of business.",
|
||||
href: "https://erxes.io",
|
||||
},
|
||||
{
|
||||
name: "Formbricks",
|
||||
description:
|
||||
"Survey granular user segments at any point in the user journey. Gather up to 6x more insights with targeted micro-surveys. All open-source.",
|
||||
href: "https://formbricks.com",
|
||||
},
|
||||
{
|
||||
name: "GitWonk",
|
||||
description:
|
||||
"GitWonk is an open-source technical documentation tool, designed and built focusing on the developer experience.",
|
||||
href: "https://gitwonk.com",
|
||||
},
|
||||
{
|
||||
name: "Hanko",
|
||||
description:
|
||||
"Open-source authentication and user management for the passkey era. Integrated in minutes, for web and mobile apps.",
|
||||
href: "https://www.hanko.io",
|
||||
},
|
||||
{
|
||||
name: "HTMX",
|
||||
description:
|
||||
"HTMX is a dependency-free JavaScript library that allows you to access AJAX, CSS Transitions, WebSockets, and Server Sent Events directly in HTML.",
|
||||
href: "https://htmx.org",
|
||||
},
|
||||
{
|
||||
name: "Infisical",
|
||||
description:
|
||||
"Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.",
|
||||
href: "https://infisical.com",
|
||||
},
|
||||
{
|
||||
name: "Mockoon",
|
||||
description: "Mockoon is the easiest and quickest way to design and run mock REST APIs.",
|
||||
href: "https://mockoon.com",
|
||||
},
|
||||
{
|
||||
name: "Novu",
|
||||
description:
|
||||
"The open-source notification infrastructure for developers. Simple components and APIs for managing all communication channels in one place.",
|
||||
href: "https://novu.co",
|
||||
},
|
||||
{
|
||||
name: "OpenBB",
|
||||
description:
|
||||
"Democratizing investment research through an open source financial ecosystem. The OpenBB Terminal allows everyone to perform investment research, from everywhere.",
|
||||
href: "https://openbb.co",
|
||||
},
|
||||
{
|
||||
name: "Sniffnet",
|
||||
description:
|
||||
"Sniffnet is a network monitoring tool to help you easily keep track of your Internet traffic.",
|
||||
href: "https://www.sniffnet.net",
|
||||
},
|
||||
{
|
||||
name: "Tolgee",
|
||||
description: "Software localization from A to Z made really easy.",
|
||||
href: "https://tolgee.io/",
|
||||
},
|
||||
{
|
||||
name: "Trigger.dev",
|
||||
description:
|
||||
"Create long-running Jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.",
|
||||
href: "https://trigger.dev",
|
||||
},
|
||||
{
|
||||
name: "Typebot",
|
||||
description:
|
||||
"Typebot gives you powerful blocks to create unique chat experiences. Embed them anywhere on your apps and start collecting results like magic.",
|
||||
href: "https://typebot.io",
|
||||
},
|
||||
{
|
||||
name: "Twenty",
|
||||
description:
|
||||
"A modern CRM offering the flexibility of open-source, advanced features and sleek design.",
|
||||
href: "https://twenty.com",
|
||||
},
|
||||
{
|
||||
name: "Webiny",
|
||||
description:
|
||||
"Open-source enterprise-grade serverless CMS. Own your data. Scale effortlessly. Customize everything.",
|
||||
href: "https://www.webiny.com",
|
||||
},
|
||||
{
|
||||
name: "Webstudio",
|
||||
description: "Webstudio is an open source alternative to Webflow",
|
||||
href: "https://webstudio.is",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Unknown HTTP Method
|
||||
else {
|
||||
throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
|
||||
}
|
||||
}
|
||||
@@ -6,35 +6,42 @@ import LimeSurvey from "./free-survey-tool-limesurvey-open-source-software-opens
|
||||
import OpnForm from "./opnform-free-open-source-form-survey-tools-builder-2023-self-hostign.jpg";
|
||||
import HeaderImage from "./2023-title-best-open-source-survey-software-tools-and-alternatives.png";
|
||||
import SurveyJS from "./surveyjs-free-opensource-form-survey-tool-software-to-make-surveys-2023.png";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
|
||||
export const meta = {
|
||||
title: "Best Open-source Form & Survey Tools (still maintained in 2023)",
|
||||
title: "5 Open Source Survey and Form Tools maintained in 2023",
|
||||
description:
|
||||
"Most open-source projects get abandoned after a while. But these 5 open-source form and survey tools are still alive and kicking in 2023.",
|
||||
"Most open source projects get abandoned after a while. But these 5 open source survey tools are still alive and kicking in 2023.",
|
||||
date: "2023-04-12",
|
||||
publishedTime: "2023-04-12T12:00:00",
|
||||
authors: ["Johannes"],
|
||||
section: "Open Source Surveys",
|
||||
tags: ["Open Source Surveys", "Formbricks", "Typeform", "SurveyJS", "Typebot", "OpnForm", "LimeSurvey"],
|
||||
};
|
||||
|
||||
_Most open-source projects get abandoned after a while. But these 5 open-source form and survey tools are still alive and kicking in 2023._
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
|
||||
|
||||
_Most open source projects get abandoned after a while. But these 5 open source survey tools are still alive and kicking in 2023._
|
||||
|
||||
<Image
|
||||
src={HeaderImage}
|
||||
alt="Free and self-hostable: Find the 5 best (and maintained) open-source survey tools 2023."
|
||||
alt="Open source survey tool self-hostable: Find the 5 best (and maintained) open source survey tool 2023."
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
Looking for the perfect open-source form and survey tool to help you gather valuable insights and improve your business? Look no further!
|
||||
Looking for the perfect open source survey tool to help you gather valuable insights and improve your business? Look no further!
|
||||
|
||||
We've compiled a list of the top 5 open-source form and survey tools that are still maintained in 2023. In-product surveys, conversational bots, AI-generated surveys: These tools offer various features that cater to different needs.
|
||||
We've compiled a list of the top 5 open source form and survey tools that are still maintained in 2023. In app surveys, conversational bots, AI-generated surveys: These open source tools offer various features that cater to different needs.
|
||||
|
||||
## 1. Formbricks - In-product micro-surveys
|
||||
## 1. Formbricks - In app micro surveys
|
||||
|
||||
<Image
|
||||
src={Formbricks}
|
||||
alt="Formbricks is a free and open-source survey software for in-product micro-surveys. Ask any segment at any point in time."
|
||||
alt="Formbricks is a free and open source survey software for in app micro surveys. Ask any user segment at any point in the user journey."
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
Formbricks is a powerful open-source form and survey solution designed to help you get better experience data for your business. This tool allows you to survey specific customer segments at any point in the user journey, providing you with invaluable insights into what your customers think and feel.
|
||||
Formbricks is a powerful open source survey tool designed to help you get better experience data for your business. This tool allows you to survey specific customer segments at any point in the user journey, providing you with invaluable insights into what your customers think and feel about your product.
|
||||
|
||||
- 👍 **Pre-segment users:** Don't ask everyone, all the time. Granularly segment your user base to get deep insights
|
||||
- 👍 **Event-based surveys:** Trigger surveys based on user behavior, such as page views, clicks, and more
|
||||
@@ -42,11 +49,15 @@ Formbricks is a powerful open-source form and survey solution designed to help y
|
||||
- 👍 **Easy self-hosting:** Docker makes it possible to self-host Formbricks in minutes.
|
||||
- ⚠️ **It's early for Formbricks.** Product developes rapidly but might encounter a bug here and there.
|
||||
|
||||
[Try it out](https://app.formbricks.com), [read more](https://formbricks.com) or dive into the [code base.](https://formbricks.com/github)
|
||||
[Try it out](https://app.formbricks.com), [read more](https://formbricks.com) or dive into the [code base](https://formbricks.com/github) or comprehensive [docs](https://formbricks.com/docs).
|
||||
|
||||
## 2. SurveyJS - Build-it-yourself libraries
|
||||
## 2. SurveyJS - Build-it-yourself library
|
||||
|
||||
<Image src={SurveyJS} alt="SurveyJS is a comprehensive" className="rounded-lg" />
|
||||
<Image
|
||||
src={SurveyJS}
|
||||
alt="SurveyJS is a comprehensive JS library to build your own form or survey application."
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
SurveyJS is a collection of JavaScript Librarys to build forms. Building your own form management system has never been easier than with SurveyJS. It packs:
|
||||
|
||||
@@ -58,9 +69,13 @@ SurveyJS is a collection of JavaScript Librarys to build forms. Building your ow
|
||||
|
||||
[Dive into the code on GitHub](https://github.com/surveyjs)
|
||||
|
||||
## 3. Typebot - Truly conversational Forms
|
||||
## 3. Typebot - Truly conversational forms
|
||||
|
||||
<Image src={Typebot} alt="SurveyJS is a comprehensive" className="rounded-lg" />
|
||||
<Image
|
||||
src={Typebot}
|
||||
alt="Open source survey and form builder SurveyJS lets you build surveys fast"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
Coming in at number three on our list is Typebot, that makes it really easy to create conversational forms and surveys. Typebot helps you engage with your audience in a more interactive way, leading to higher response rates and better data. It comes with:
|
||||
|
||||
@@ -74,9 +89,13 @@ Coming in at number three on our list is Typebot, that makes it really easy to c
|
||||
|
||||
## 4. OpnForm - Straight-forward survey builder
|
||||
|
||||
<Image src={OpnForm} alt="SurveyJS is a comprehensive" className="rounded-lg" />
|
||||
<Image
|
||||
src={OpnForm}
|
||||
alt="OpnForm is an open source form builder for experience management"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
OpnForms is a flexible and powerful open-source form and survey tool designed to make data collection easy and efficient. OpnForm packs lots of features, especially for a Beta:
|
||||
OpnForms is a flexible and powerful open source form and survey tool designed to make data collection easy and efficient. OpnForm packs lots of features, especially for a Beta:
|
||||
|
||||
- 👍 **Multiple Question Types:** Choose from a wide variety of question types to create highly customizable forms and surveys.
|
||||
- 👍 **Conditional Logic:** Show or hide questions based on previous responses to create personalized surveys.
|
||||
@@ -87,7 +106,11 @@ OpnForms is a flexible and powerful open-source form and survey tool designed to
|
||||
|
||||
## 5. LimeSurvey - Old (but gold?)
|
||||
|
||||
<Image src={LimeSurvey} alt="SurveyJS is a comprehensive" className="rounded-lg" />
|
||||
<Image
|
||||
src={LimeSurvey}
|
||||
alt="LimeSurvey is open source survey builder to manage experiences with forms"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
LimeSurvey has been around for at least a decade. It's a powerful survey tool made for more classical, scientific surveying. It packs:
|
||||
|
||||
@@ -101,9 +124,9 @@ LimeSurvey has been around for at least a decade. It's a powerful survey tool ma
|
||||
|
||||
## Summary ☟
|
||||
|
||||
In this article, we've rounded up the top 5 open-source form and survey tools that are still rocking it in 2023. Perfect for devs who are always on the lookout for the latest and greatest!
|
||||
In this article, we've rounded up the top 5 open source form and survey tools that are still rocking it in 2023. Perfect for devs who are always on the lookout for the latest and greatest!
|
||||
|
||||
1. Formbricks: A game-changer for in-product micro-surveys, letting you target specific customer segments at any point in their journey. It's still early days, but this bad boy is worth keeping an eye on.
|
||||
1. Formbricks: A game-changer for in app micro surveys, letting you target specific customer segments at any point in their journey. It's still early days, but this bad boy is worth keeping an eye on.
|
||||
|
||||
2. SurveyJS: A must-have for DIY enthusiasts, this collection of JavaScript libraries makes building your own form management system a breeze. Just remember, the starting price is $499/year.
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,178 @@
|
||||
import Image from "next/image";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import { Callout } from "@/components/shared/Callout";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
|
||||
import TweetPeer from "./peer-tweet-typeform-open-source.png";
|
||||
import SnoopForms from "./snoopforms-open-source-typeform-alternative.png";
|
||||
import TwitterResult from "./twitter-results-PMF-cal.png";
|
||||
import EmailResult from "./email-results-PMF-cal.png";
|
||||
import TitleImage from "../github-accelerator-experience/formbricks-sponsored-by-github-accelerator-2023.webp";
|
||||
import PMFDashboard from "./pmf-survey-dashboard.png";
|
||||
import MattiJojo from "./matti-jojo.jpg";
|
||||
|
||||
export const meta = {
|
||||
title: "Formbricks v1 - How we got here 🤸",
|
||||
description:
|
||||
"A lot has happened since Matti and I had a chat about open-source surveys in May last year. The release of Formbricks v1.0 is a perfect opportunity to look back on how it all started.",
|
||||
date: "2023-07-14",
|
||||
publishedTime: "2023-07-14T08:10:33",
|
||||
authors: ["Johannes"],
|
||||
section: "Open Source Experience Management",
|
||||
tags: ["Open Source", "Experience Management", "Formbricks"],
|
||||
};
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
|
||||
|
||||
_A lot has happened since Matti and I had a chat about open-source surveys in May last year. The release of Formbricks v1.0 is a perfect opportunity to look back on how it all started._
|
||||
|
||||
Funnily enough, it started with a tweet:
|
||||
|
||||
<Image
|
||||
src={TweetPeer}
|
||||
alt="Peer tweets about the best open-source Typeform alternative"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
When I read this tweet, I was sitting in a WeWork in Mexico City, feeling a bit guilty for being such a cliché digital nomad. Sipping on my decaffeinated matcha with lactose-free goat milk, I slid into Matti's DMs. I knew he had built an open-source survey tool a few months ago and suggested he comment on the tweet. We started chatting.
|
||||
|
||||
Both Matti and I had been freelancing for several months. While the money was good, we missed working on a project of our own. We met a couple of years ago while working on our startups. I had an app and wanted to be GDPR compliant; Matti offered to help out. We've stayed in contact ever since, giving feedback on projects and ideas.
|
||||
|
||||
So we chatted about the opportunity of building a commercial open-source alternative to Typeform. After reading up on the advantages - and challenges - of open-source, we decided to build a side project together: **[snoopForms: The Open-Source Typeform Alternative](https://snoopforms.com/)** 🦝
|
||||
|
||||
<Image
|
||||
src={SnoopForms}
|
||||
alt="SnoopForms was the OS Typeform alternative with a twist"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
### **snoopForms and why OS Typeform isn’t a good business**
|
||||
|
||||
We shipped a first version of the product and the landing page and shared it on ProductHunt and HackerNews. It stirred up quite some interest—1.5k people signed up over a few weeks. During the launch, we sold 10 early bird deals for $179 each. We got excited! Is it really that easy?
|
||||
|
||||
Well no, it is not. Apart from the Early Birds (🤍) we attracted a looooooooot of people who liked Typeform, but didn’t want to pay the high price tag. Generally, it's not the best idea to build a product with high competition (100s of survey tools) for a price-sensitive target group. Even data privacy isn’t a good selling point when Jotform offers self-hosting and Typeform is an EU-based company (GDPR). We’ve written **[in-depth](https://formbricks.com/blog/open-source-qualtrics-beats-typeform)** about why we eventually decided to shift our focus away from open-source Typeform.
|
||||
|
||||
Nonetheless, we were hooked on open-source surveys; we just hadn't found the right application yet.
|
||||
|
||||
### **snoopForms → Formbricks, a detour, and going full-time**
|
||||
|
||||
We decided to wrap up our freelance gigs and go in full-time, to find the right angle quicker. We built a MVP for _Building Bricks for Forms and Surveys_, but while the idea made sense, we struggled to build an easy, opionated solution which can be used for several use cases. To help engineers build form and survey applications faster, it needed to pack form creation, form analytics, graphs, data handling, data storage, analysis, integrations, and ways to act on the insights for a variety of use cases 🤷 There's a reason why successful dev tools focus on one of these aspects and do them well. So we kept looking…
|
||||
|
||||
### **Data Processing vs. Experience Management**
|
||||
|
||||
We had talked to and surveyed a lot of people. Zooming out, we saw that there are two applications for forms: data processing and experience management.
|
||||
|
||||
**Data processing** is essentially getting information out of a human brain into a digital system. The focus here lies on high integrability with (legacy) systems, versatility, and a great developer experience. Libraries like React Hook Forms or the more productized form.io fall into this category. While there certainly is a need here, it was a less inspiring field for us, especially after reading how [tedious](https://medium.com/@jgee/what-i-learned-in-two-years-of-moving-government-forms-online-1edc4c2aa089) it can be to bring [legacy](https://medium.com/san-francisco-digital-services/how-to-make-a-form-d1d1b67d95d7) form systems up to speed.
|
||||
|
||||
**Experience Management** on the other hand is a lot more exciting! Helping teams build better products, services, and organizations is a mission both Matti and I could get behind ❤️
|
||||
|
||||
<Callout title="In a nutshell" type="note">
|
||||
Experience Management, in a nutshell, is enabling teams to gather, analyze, and act on qualitative data
|
||||
collected from their users, customers, and employees.
|
||||
</Callout>
|
||||
|
||||
We researched what was out there and talked to more founders. Initially, we wanted to build a solution for a problem we had ourselves: Gathering qualitative insights along the user journey of early adopters. It’s common sense to never launch without analytics, but why does everyone launch without a system to gather qualitative user insights continuously? Mostly, because there's no easy, cheap and quick way to do it (yet).
|
||||
|
||||
### **The Product-Market Fit Survey Tool**
|
||||
|
||||
Following Paul Graham's advice to start with a **[narrow but deep hole](http://paulgraham.com/startupideas.html)**, we narrowed it down to one specific problem every founder has: Measuring Product-Market Fit. Many know the **[article](https://review.firstround.com/how-superhuman-built-an-engine-to-find-product-market-fit)** on how Superhuman built an engine to measure and optimize PMF. However, to run it correctly within your product, you do have to do quite some custom coding. So we decided to offer a solution which works out of the box.
|
||||
|
||||
<Image
|
||||
src={PMFDashboard}
|
||||
alt="Peer tweets about the best open-source Typeform alternative"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
_We built a custom dashboard just to visualize the PMF survey results_
|
||||
|
||||
We shipped an MVP, launched it, and picked up the conversations with the founders who had tried to measure PMF
|
||||
before. We quickly learned this:
|
||||
|
||||
- Our MVP was scoped so rigidly that we had to do quite some custom coding ourselves to get it implemented. Setting it up and maintaining it stood in no relation to the value it provided because…
|
||||
- The PMF survey is for teams that "kinda" have PMF, not for teams which are just starting out. You need a few hundred recurring users in your app to be able to reach statistical significance and run the survey every 3 months without asking anyone twice. For teams who don’t have that, talking to users 1:1 will remain the best way to go.
|
||||
- Founders were reluctant to add another tool just for one survey and purpose.
|
||||
|
||||
After the rather disappointing launch of the PMF survey tool, we hit a low. The excitement about the initial snoopForm traction had passed. Since November, we had been working full-time for four months iterating on different products in the forms and survey space, and it didn’t feel like we made much progress. But that wasn’t really true.
|
||||
|
||||
### **"The essence of strategy is choosing what not to do.”**
|
||||
|
||||
It was quite a journey since Matti wrote the first line of code in July last year. Even though it didn’t _feel_ like much progress, we had learned a lot. Most importantly, we knew what **not** to build.
|
||||
|
||||
With snoopForms, we learned that there is a latent need for an open-source standalone survey tool, with a Typeform-like feature set: It’s a great opportunity to build a community and **not** a great business opportunity. The PMF survey tool taught us about the needs of early-stage teams and that surveys are **not** the best solution to their problems. However, they do need some form to gather qualitative feedback and a smooth way to work with it 😏
|
||||
|
||||
We took everything we knew and wrote a Masterplan. And then we sat down coded.
|
||||
|
||||
### **Open-source in-product surveys**
|
||||
|
||||
March and the first half of April flew by as we hacked together the MVP of what Formbricks is today: A tool which enables teams to target specific segments of their user base with versatile micro-surveys. Matti solved some tricky technical challenges around the Formbricks Widget and shipped the backend. ChatGPT and I warmed up with next.js and built frontend. Seeing the product come together was fun and felt like progress - exactly what we needed!
|
||||
|
||||
<Image
|
||||
src={TitleImage}
|
||||
alt="GitHub sponsors Formbricks to join their open-source accelerator program"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
A welcomed motivator came in the form of an email from GitHub: We we were chosen to take part in the first-ever batch of the GitHub Open-Source Accelerator! It was a really fun programme and we learned a lot! You can read about our experience [here.](https://formbricks.com/blog/github-accelerator-experience)
|
||||
|
||||
### **On the right track 🚆**
|
||||
|
||||
Once we were ready to share our MVP with the world, we knew we were on the right track! The feedback was really good and usage picked up right from the start. One morning we woke up to our analytics dashboard showing thousands of survey displays with 500+ responses in the past couple of hours! This is when we knew we’re onto something 🚀
|
||||
|
||||
A couple of weeks later, Peer from [Cal.com](http://Cal.com) came back to northern Germany to visit his family and friends. We hung out in the co-working space, where he did his first steps as an entrepreneur. As we were chatting about how to separate opinion from experience, Peer decided to shoot an email with a survey to the 12k most active users of Cal.com. A few days before, he asked his Twitter following the PMF question:
|
||||
|
||||
<Image
|
||||
src={TwitterResult}
|
||||
alt="SnoopForms was the OS Typeform alternative with a twist"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
We wanted to compare how the publicly asked survey (Twitter) compared to a survey among the community of users. The results were eye-opening:
|
||||
|
||||
<Image
|
||||
src={EmailResult}
|
||||
alt="SnoopForms was the OS Typeform alternative with a twist"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
_Anything above 40% is considered PMF, 60% is 🔥🔥🔥_
|
||||
|
||||
For us, this was not only a good test for our system but it was a great example that Experience Management does not only happen within apps, but on several touch points right from the start. If we want to offer a solution which can comprehensively measure and evaluate experiences, it cannot be confined to in-product surveys for too long.
|
||||
|
||||
### **Typeform sneaking back in?**
|
||||
|
||||
Given our early traction with snoopForms, it’s not a big surprise that community requests for a Typeform-like standalone survey popped up more and more. Since we had all the pieces in place, we shipped standalone surveys in a day. However, it’s not our focus right now, so we hand over most of the feature requests to the community. And the community ships! Just in the past couple of weeks, we got:
|
||||
|
||||
- **Prefill Data in Link Surveys** by Piyush - [Docs](https://formbricks.com/docs/link-surveys/data-prefilling)
|
||||
- **Identify Users Link identification** by Zorig - [Docs](https://formbricks.com/docs/link-surveys/user-identification)
|
||||
- **Close Survey on Date** by Piyush
|
||||
- **Close Survey after x Responses** by Pradumn
|
||||
- **Redirect on Link Survey Completion** by Dhruwang
|
||||
- **Embed Link Survey via iframe** by Ankur
|
||||
- **Set Custom ‘Survey Closed’ Message** by Pradumn
|
||||
- **Randomize Answer Options** by Shubdeep
|
||||
|
||||
We absolutely love the idea of being the platform for a community-developed standalone survey experience! It’s a win-win: The community gets a free, self-hostable standalone survey product and we get a growing upper funnel for our Experience Management solution 🤸
|
||||
|
||||
### **It’s coming together 🔥**
|
||||
|
||||
Looking back at the amount of value Formbricks can deliver after not even 5 months fills us with excitement. Matti and I have been hacking it together for the most part but we wouldn’t be anywhere nearly as close without the support of our community and friends - thank you!
|
||||
|
||||
And we’re only getting started: We were able to hire two engineers from our community of contributors to build up more shipping velocity. We also started working with Kristian who runs our [open-source Design repo](https://github.com/formbricks/design) more closely and are super excited about what we’re cooking up. We’ll make sure Formbricks stays easy-to-use, accessible and simply beautiful as it grows more powerful! We can't tell you how excited we are about what's coming 😁
|
||||
|
||||
If you’re curious to learn about all the things you can do with Formbricks today, we’ve written and keep updating a list of Best Practices incl. survey templates in our [Docs](https://formbricks.com/docs/best-practices/cancel-subscription).
|
||||
|
||||
### Sounds good? **Build with us!**
|
||||
|
||||
- **Eager to contribute?** [Join our Discord](https://formbricks.com/discord) and say Hi! We’re more than happy to find the perfect issue for your level of experience 🙌 😊
|
||||
|
||||
- **Got an experience to measure?** Open-source is the way to go. [Let’s have a chat 🤙](https://cal.com/johannes/15)
|
||||
|
||||
<Image
|
||||
src={MattiJojo}
|
||||
alt="SnoopForms was the OS Typeform alternative with a twist"
|
||||
className="rounded-lg w-1/2"
|
||||
/>
|
||||
|
||||
### All the best!
|
||||
|
||||
Johannes & Matti
|
||||
|
||||
export default ({ children }) => <LayoutMdx meta={meta}>{children}</LayoutMdx>;
|
||||
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
@@ -5,14 +5,21 @@ import Demo from "./our-experience-github-acc-demo-screenshot.png";
|
||||
import Mail from "./github-accelerator-selection-mail.png";
|
||||
import Teams from "./github-accelerator-2022-teams.png";
|
||||
import NewsletterSignup from "@/components/shared/NewsletterSignup";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
|
||||
export const meta = {
|
||||
title: "Our GitHub Accelerator Experience 👀",
|
||||
description:
|
||||
"What we learned during the first GitHub Open-Source Accelerator Programm - our experience and if we would do it again.",
|
||||
date: "2023-04-13",
|
||||
date: "2023-06-06",
|
||||
publishedTime: "2023-06-06T05:12:25",
|
||||
authors: ["Johannes"],
|
||||
section: "GitHub Accelerator",
|
||||
tags: ["GitHub Accelerator", "Open-Source", "Startup"],
|
||||
};
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
|
||||
|
||||
_We were among the first 20 teams ever to run through the Open-Source Accelerator by Github. Read about our experience and if we would do it again:_
|
||||
|
||||
<Image
|
||||
@@ -37,7 +44,7 @@ January and February came and went. On the 22nd of March, we received an email f
|
||||
|
||||
Needless to say, we were thrilled! We were selected from over 1000 open-source projects, alongside renowned and popular projects like [Nuxt](https://github.com/nuxt/nuxt), [TRPC](https://github.com/trpc/trpc), and [Responsively App](https://github.com/responsively-org/responsively-app). Here is a summary of what we got:
|
||||
|
||||
### What we got on paper
|
||||
### What the GitHub Accelerator offers on paper
|
||||
|
||||
✅ Ten sessions with **well-known** figures from the open-source community (Wednesdays)
|
||||
|
||||
@@ -105,7 +112,7 @@ Ericas talk was really impressive and so is she: Founder of Bitnami, COO of GitH
|
||||
|
||||
**GitHub Demo Day:** All teams presented what they achieved during the 10 week programm. It was great fun to present Formbricks, [you can watch it on Youtube.](https://www.youtube.com/live/Gj6Bez2182k?feature=share&t=1448)
|
||||
|
||||
## Would we do it again? And should you?
|
||||
## Would we do the GitHub Accelerator again? And should you?
|
||||
|
||||
Yes, absolutely. The sessions were excellent, we met a handful of inspiring builders, and the 20k USD was a helpful financial boost. The application process might evolve, but since all of your code is open-source anyway, you might as well throw your hat in the ring.
|
||||
|
||||
|
||||
@@ -2,14 +2,21 @@ import Image from "next/image";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import TitleImage from "./formbricks-sponsored-by-github-accelerator-2023.webp";
|
||||
import NewsletterSignup from "@/components/shared/NewsletterSignup";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
|
||||
export const meta = {
|
||||
title: "Formbricks Joins GitHub Accelerator's Inaugural Cohort 💃",
|
||||
description:
|
||||
"We're getting ready to take our open-source experience management platform to new heights, thanks to being part of the first-ever GitHub Accelerator program!",
|
||||
date: "2023-04-13",
|
||||
publishedTime: "2023-04-13T05:12:25",
|
||||
authors: ["Johannes"],
|
||||
section: "GitHub Accelerator",
|
||||
tags: ["GitHub Accelerator", "Open-Source"],
|
||||
};
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
|
||||
|
||||
_We're getting ready to take our open-source experience management platform to new heights, thanks to being part of the first-ever GitHub Accelerator program!_
|
||||
|
||||
<Image
|
||||
|
||||
@@ -4,13 +4,20 @@ import RobinHoodMeme from "./robin-hood-meme.png";
|
||||
import WhyWeDoIt from "./why-we-do-it.png";
|
||||
import EverythinEverywhereAllAtOnce from "./everything_everywhere_all_at_once.png";
|
||||
import ResponsiveEmbed from "react-responsive-embed";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
|
||||
export const meta = {
|
||||
title: "Open source forms will save the world.",
|
||||
description: "What motivates us to build open source tech in such a crowded space?",
|
||||
date: "2022-08-26",
|
||||
publishedTime: "2022-08-26T12:12:25",
|
||||
authors: ["Johannes"],
|
||||
section: "Open Source Survey Tool",
|
||||
tags: ["Open Source", "Survey Tool", "Forms"],
|
||||
};
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
|
||||
|
||||
<Image src={RobinHoodMeme} alt="Robin Hood Meme" className="rounded-lg" />
|
||||
|
||||
_What motivates us to build open source tech in such a crowded space? What do we see what others might not? And how do we understand the relationship between free open source tech and a commercial complement?_
|
||||
|
||||
@@ -9,14 +9,21 @@ import Wrestling from "./wrestling.jpg";
|
||||
import TypeformValue from "./typeform-value-prop.png";
|
||||
import ResponsiveEmbed from "react-responsive-embed";
|
||||
import { Callout } from "@/components/shared/Callout";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
|
||||
export const meta = {
|
||||
title: "Why Qualtrics beats Typeform, especially Open-Source",
|
||||
description:
|
||||
"We kicked it off with a Typeform open-source alternative. As we build and learn, our focus is shifting. Read why:",
|
||||
date: "2023-03-24",
|
||||
publishedTime: "2023-03-23T08:20:15",
|
||||
authors: ["Johannes"],
|
||||
section: "Open Source Experience Management",
|
||||
tags: ["Open Source", "Experience Management", "Typeform", "Qualtrics"],
|
||||
};
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
|
||||
|
||||
<Image src={Wrestling} alt="Why we do it" className="rounded-lg" />
|
||||
|
||||
_In September, we kicked it off with a Typeform open-source alternative. As we build and learn, our focus is shifting. We talk about how we look at form and survey tools today, why experience management not only matters for enterprise and why the endgame looks a lot more like open-source Qualtrics. Qualtrics? What happened to Typeform?_
|
||||
|
||||
@@ -2,13 +2,20 @@ import Image from "next/image";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import HeaderImage from "./formbricks-logo-header-open-source-form-infrastructure.svg";
|
||||
import HeroAnimation from "../../../components/shared/HeroAnimation.tsx";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
|
||||
export const meta = {
|
||||
title: "snoopForms → Formbricks 🎉",
|
||||
description: "A new name, a new look, key learnings and what's next.",
|
||||
date: "2022-11-07",
|
||||
publishedTime: "2023-07-11T12:00:06",
|
||||
authors: ["Johannes"],
|
||||
section: "Formbricks",
|
||||
tags: ["Formbricks", "snoopForms", "Open Source"],
|
||||
};
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
|
||||
|
||||
<Image src={HeaderImage} alt="Formbricks - Open Source Forms and Surveys" className="rounded-lg" />
|
||||
|
||||
_It has been quiet in the past weeks, but we didn't spend our days sitting around. Find out what we were up to and where we are taking Formbricks from here._
|
||||
|
||||
|
Before Width: | Height: | Size: 976 KiB |
@@ -1,47 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import LayoutMdx from "@/components/shared/LayoutMdx";
|
||||
import FormbricksSneak from "./formbricks-sneak.png";
|
||||
import ResponsiveEmbed from "react-responsive-embed";
|
||||
|
||||
export const meta = {
|
||||
title: "Video: Walk-through of the new Formbricks",
|
||||
description: "The new, powerful Formbricks is almost ready!",
|
||||
date: "2023-03-30",
|
||||
};
|
||||
|
||||
_The new, powerful Formbricks is almost ready!_
|
||||
|
||||
<Image src={FormbricksSneak} alt="Sneakpeek into what the new Formbricks can do" className="rounded-lg" />
|
||||
|
||||
We've been working hard on getting a revamped Formbricks ready - we're almost there!
|
||||
|
||||
What you can do with it:
|
||||
|
||||
1. Design **any survey** you want
|
||||
2. Trigger at any point in your app both **No Code** (page view, element click) and **Code** (hook `formbricks.track` into your event)
|
||||
3. Pass custom user attributes to Formbricks to **segment your user base**
|
||||
|
||||
## Have a look:
|
||||
|
||||
<ResponsiveEmbed
|
||||
src="https://www.tella.tv/video/clfrymq2f00sk0fjqd9r6btf1/embed?b=0&title=0&a=1&loop=0&t=0&muted=0"
|
||||
allowFullScreen
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
Formbricks is a lot more powerful than ever before! :mechanical_arm:
|
||||
|
||||
You can create:
|
||||
|
||||
- Onboarding surveys,
|
||||
- PMF surveys,
|
||||
- Churn surveys,
|
||||
- Feature chaser,
|
||||
- Feedback box,
|
||||
- Identify customer goals,
|
||||
- Measure task completion,
|
||||
- etc, etc.
|
||||
|
||||
## Stay tuned, Formbricks Cloud goes live soon!
|
||||
|
||||
export default ({ children }) => <LayoutMdx meta={meta}>{children}</LayoutMdx>;
|
||||
@@ -6,14 +6,21 @@ import TitleImage from "./title-image.png";
|
||||
import HeaderImage from "./formbricks-logo.svg";
|
||||
import ProprietaryDependence from "./propietary-dependence.jpeg";
|
||||
import ResponsiveEmbed from "react-responsive-embed";
|
||||
import AuthorBox from "@/components/shared/AuthorBox";
|
||||
|
||||
export const meta = {
|
||||
title: "Why Open-Source + No-Code is the Future of Enterprise & Goverment Software",
|
||||
description:
|
||||
"Here is why a no-code interface is cheatcode for OSS and why particularly large corporations and governments are to benefit the most",
|
||||
date: "2022-06-03",
|
||||
publishedTime: "2022-06-03T06:04:08",
|
||||
authors: ["Johannes"],
|
||||
section: "Open-Source",
|
||||
tags: ["Open-Source", "No-Code", "Enterprise", "Government"],
|
||||
};
|
||||
|
||||
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
|
||||
|
||||
<Image src={TitleImage} alt="Title Image" className="rounded-lg" />
|
||||
|
||||
_Open source software (OSS) beats out proprietary software in every regard - except for value capturing. No-Code tools shorten the feedback loop between builders and consumers, kicking productivity through the roof. Here is why a no-code interface is cheatcode for OSS and why particularly large corporations and governments are to benefit the most._
|
||||
|
||||
@@ -3,12 +3,13 @@ import Layout from "@/components/shared/Layout";
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
import { useEffect } from "react";
|
||||
import { CheckBadgeIcon } from "@heroicons/react/24/solid";
|
||||
import { Button } from "@formbricks/ui";
|
||||
|
||||
const XMOffer = [
|
||||
{
|
||||
step: "1",
|
||||
header: "Kick-off call",
|
||||
description: "You share with our seasoned PMs which areas of your customer experience need improvement.",
|
||||
header: "Kick-off call (FREE)",
|
||||
description: "Share with our seasoned PMs which areas of customer experience need improvement.",
|
||||
},
|
||||
{
|
||||
step: "2",
|
||||
@@ -69,20 +70,29 @@ const ConciergePage = () => {
|
||||
</div>
|
||||
))}
|
||||
<div className="border-b border-t border-slate-300 p-6 text-4xl font-semibold text-slate-800">
|
||||
<p className="mr-2 font-light">$2.290</p>
|
||||
<p className="mr-2 font-light">$1.290</p>
|
||||
</div>
|
||||
<div className="p-6 text-sm text-slate-800">
|
||||
<p>
|
||||
<CheckBadgeIcon className="mr-1 inline h-5 w-5 text-slate-800" />
|
||||
100% Risk-free: Pay after the kick-off call.
|
||||
100% Risk-free: Pay after the kick-off call, if you liked it.
|
||||
</p>
|
||||
<p>
|
||||
<CheckBadgeIcon className="mr-1 inline h-5 w-5 text-slate-800" />
|
||||
Money-back: If you're not happy, get a full refund.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-6">
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
className="w-full justify-center"
|
||||
href="https://cal.com/johannes/kick-off"
|
||||
target="_blank">
|
||||
Book free Kick-Off call
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl">
|
||||
<div className="!mt-0 rounded-xl">
|
||||
<Cal
|
||||
calLink="johannes/kick-off"
|
||||
style={{
|
||||
|
||||
@@ -7,7 +7,7 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation";
|
||||
export default function DocsFeedbackPage() {
|
||||
return (
|
||||
<Layout
|
||||
title="Feedback Box"
|
||||
title="Docs Feedback"
|
||||
description="The better your docs, the higher your user adoption. Measure granularly how clear your documentation is.">
|
||||
<div className="grid grid-cols-1 items-center md:grid-cols-2 md:gap-12 md:py-20">
|
||||
<div className="p-6 md:p-0">
|
||||
|
||||
@@ -33,7 +33,7 @@ The API requests are authorized with a personal API key. This API key gives you
|
||||
|
||||
### Delete a personal API key
|
||||
|
||||
1. Go to settings on [app.formbricks.com](https://app.formbricks.com/me/settings).
|
||||
1. Go to settings on [app.formbricks.com](https://app.formbricks.com/).
|
||||
2. Go to page “API keys”.
|
||||
3. Find the key you wish to revoke and select “Delete”.
|
||||
4. Your API key will stop working immediately.
|
||||
|
||||
@@ -39,7 +39,7 @@ export const meta = {
|
||||
},
|
||||
]}
|
||||
example={`{
|
||||
"personId: "clfqjny0v000ayzgsycx54a2c",
|
||||
"personId": "clfqjny0v000ayzgsycx54a2c",
|
||||
"surveyId": "clfqz1esd0000yzah51trddn8",
|
||||
"finished": true,
|
||||
"data": {
|
||||
|
||||
@@ -36,25 +36,27 @@ pnpm add @formbricks/js
|
||||
|
||||
## Integrating with Next.js 13 App Directory
|
||||
|
||||
The Next.js 13 app directory requires us to initialize Formbricks differently than the pages directory. Specifically, the app directory server-side renders components by default, and the formbricks-js library is a client-side library. To make these work together, create a `formbricks.js` file (or formbricks.ts if you are using Typescript) and set up the FormbricksProvider with the 'use client' directive:
|
||||
The Next.js 13 app directory requires us to initialize Formbricks differently than the pages directory. Specifically, the app directory server-side renders components by default, and the formbricks-js library is a client-side library. To make these work together, create a `formbricks.tsx` file (or `formbricks.js` if you don't use Typescript) and set up the FormbricksProvider with the 'use client' directive:
|
||||
|
||||
```tsx
|
||||
// app/formbricks.js
|
||||
// app/formbricks.tsx
|
||||
"use client";
|
||||
import formbricks from "@formbricks/js";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
formbricks.init({
|
||||
environmentId: "your-environment-id",
|
||||
apiHost: "your-api-host", // e.g. https://app.formbricks.com
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
}
|
||||
|
||||
export default function Providers({ Component, pageProps }: AppProps) {
|
||||
export default function FormbricksProvider() {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
formbricks.init({
|
||||
environmentId: "clj66eqzu00m5qu0g8leglrns",
|
||||
apiHost: "https://app.formbricks.com", // e.g. https://app.formbricks.com
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
formbricks?.registerRouteChange();
|
||||
}, [pathname, searchParams]);
|
||||
@@ -63,21 +65,17 @@ export default function Providers({ Component, pageProps }: AppProps) {
|
||||
}
|
||||
```
|
||||
|
||||
Once we do this, we can then import the `provider.js` file in our `app/layout.js` file, and wrap our app in the Formbricks provider.
|
||||
Once we do this, we can then import the `formbricks.tsx` file in our `app/layout.tsx` file, and wrap our app in the Formbricks provider.
|
||||
|
||||
```tsx
|
||||
// app/layout.js
|
||||
import Providers from "./formbricks";
|
||||
// app/layout.tsx
|
||||
import FormbricksProvider from "./formbricks";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }) {
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<Providers />
|
||||
<FormbricksProvider />
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@ Add the following script to the `<head>` tag of your HTML file:
|
||||
var t = document.createElement("script");
|
||||
(t.type = "text/javascript"),
|
||||
(t.async = !0),
|
||||
(t.src = "https://unpkg.com/@formbricks/js@^0.1.17/dist/index.umd.js");
|
||||
(t.src = "https://unpkg.com/@formbricks/js@^1.0.0/dist/index.umd.js");
|
||||
var e = document.getElementsByTagName("script")[0];
|
||||
e.parentNode.insertBefore(t, e),
|
||||
setTimeout(function () {
|
||||
|
||||
@@ -8,24 +8,56 @@ export const meta = {
|
||||
"Utilize Docker-Compose for easy deployment on your machine. Clone the repo, configure settings, and build the image to access the app on localhost.",
|
||||
};
|
||||
|
||||
At Formbricks, we understand that different users have different needs, and we strive to cater to a wide variety of situations. This is why we provide two ways of running our application using Docker:
|
||||
At Formbricks, we understand that different users have different needs, and we strive to cater to a wide variety of situations. This is why we provide three ways of running our application:
|
||||
|
||||
1. **Fast Setup with a Pre-built Docker Image:** This method is designed for those who want to quickly set up and start using Formbricks without getting into the technicalities of Docker or the build process. When you choose this method, you're using an image that we've already built for you. The pre-built image is ready-to-run, and it only requires minimal configuration on your part. This approach is perfect for getting a functional instance of Formbricks up and running with minimal hassle. It's as easy as downloading the Docker image and firing up the container.
|
||||
1. **Production Instance Setup with Shell Script on Ubuntu**: If you want to quickly set up a production instance of Formbricks on a server running Ubuntu, we've got you covered! This method utilizes a convenient shell script that takes care of everything, including Docker, Postgres DB, and SSL certificate configuration. The shell script will automatically install all the required dependencies and configure your server, making the process a breeze.
|
||||
|
||||
2. **Manual Setup by Building the Docker Image from Source:** This approach provides the flexibility to configure every aspect of your Formbricks instance, including environment variables that need to be set at build time. While we don't recommend changing the source code of Formbricks, this method allows you to set your own configuration that might be necessary for specific deployment needs. Keep in mind that this method requires a more in-depth understanding of Docker and the build process. However, the trade-off is the additional control and flexibility you gain, making it worth considering if you're a more advanced user or have very specific configuration needs.
|
||||
2. **Fast Setup with a Pre-built Docker Image:** This method is designed for those who want to quickly set up and start using Formbricks without getting into the technicalities of Docker or the build process. When you choose this method, you're using an image that we've already built for you. The pre-built image is ready-to-run, and it only requires minimal configuration on your part. This approach is perfect for getting a functional instance of Formbricks up and running with minimal hassle. It's as easy as downloading the Docker image and firing up the container.
|
||||
|
||||
3. **Manual Setup by Building the Docker Image from Source:** This approach provides the flexibility to configure every aspect of your Formbricks instance, including environment variables that need to be set at build time. While we don't recommend changing the source code of Formbricks, this method allows you to set your own configuration that might be necessary for specific deployment needs. Keep in mind that this method requires a more in-depth understanding of Docker and the build process. However, the trade-off is the additional control and flexibility you gain, making it worth considering if you're a more advanced user or have very specific configuration needs.
|
||||
|
||||
Please note that regardless of the method you choose, Formbricks is designed to be easy-to-use and flexible. So choose the method that best fits your comfort level and requirements, and start leveraging the power of Formbricks today!
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
## (Most users: One-click Ubuntu setup) Running the Shell Script
|
||||
|
||||
This is the quickest way to get Formbricks up and running on an Ubuntu server. The shell script will automatically install all the required dependencies and configure your server, making the process a breeze.
|
||||
|
||||
### Requirements
|
||||
|
||||
Before you proceed, make sure you have the following:
|
||||
|
||||
- A Linux Ubuntu Virtual Machine deployed with SSH access.
|
||||
|
||||
- An A record set up to connect a custom domain to your instance. Formbricks will automatically create an SSL certificate for your domain using Let's Encrypt.
|
||||
|
||||
## Single Command Setup
|
||||
|
||||
Copy and paste the following command into your terminal:
|
||||
|
||||
```bash
|
||||
/bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/formbricks/formbricks/main/docker/production.sh)"
|
||||
```
|
||||
|
||||
The script will prompt you for the following information:
|
||||
|
||||
1. **Overwriting Docker GPG Keys**: If Docker GPG keys already exist, the script will ask if you want to overwrite them.
|
||||
|
||||
2. **Email Address**: Provide your email address for SSL certificate registration with Let's Encrypt.
|
||||
|
||||
3. **Domain Name**: Enter the domain name that Traefik will use to create the SSL certificate and forward requests to Formbricks.
|
||||
|
||||
That's it! After running the command and providing the required information, visit the domain name you entered, and you should see the Formbricks home wizard!
|
||||
|
||||
## (Manual Deployment) Running the pre-built Docker Image
|
||||
|
||||
### Requirements
|
||||
|
||||
Ensure `docker` & `docker compose` are installed on your server/system. Both are typically included with Docker utilities, like Docker Desktop and Rancher Desktop.
|
||||
|
||||
**Note**: `docker compose` without the hyphen is now the primary method of using docker-compose, according to the Docker documentation.
|
||||
|
||||
## (Most users) Running the pre-built Docker Image
|
||||
|
||||
This is suitable for those who are testing Formbricks or running it with minimal to no modifications. For this we use the [public Docker image](https://hub.docker.com/r/formbricks/formbricks) and a simple docker-compose file.
|
||||
|
||||
1. **Create a New Directory for Formbricks**
|
||||
@@ -41,7 +73,7 @@ This is suitable for those who are testing Formbricks or running it with minimal
|
||||
Download the docker-compose file directly from the Formbricks repository:
|
||||
|
||||
```bash
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/docker/main/docker-compose.yml
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/main/docker/docker-compose.yml
|
||||
```
|
||||
|
||||
3. **Generate NextAuth Secret**
|
||||
@@ -89,21 +121,21 @@ This is suitable for those who are testing Formbricks or running it with minimal
|
||||
|
||||
## (Advanced users) Build and Run Formbricks
|
||||
|
||||
### Requirements
|
||||
|
||||
Ensure `docker` & `docker compose` are installed on your server/system. Both are typically included with Docker utilities, like Docker Desktop and Rancher Desktop.
|
||||
|
||||
**Note**: `docker compose` without the hyphen is now the primary method of using docker-compose, according to the Docker documentation.
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/formbricks/formbricks.git && cd formbricks
|
||||
```
|
||||
|
||||
Create a `.env` file based on `.env.docker` and change all fields according to your setup. This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings in the `.env` file. If you configured your email credentials, you can also comment the following lines to enable email verification (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1`)
|
||||
2. Modify the `.env.docker` file as required by your setup. This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings. If you configured your email credentials, you can also comment the following lines to enable email verification (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1`)
|
||||
|
||||
2. Copy the `.env.docker` file to `.env` and edit it with an editor of your choice if needed.
|
||||
|
||||
```bash
|
||||
cp .env.docker .env
|
||||
```
|
||||
|
||||
Note: The environment variables are used at build time. When you change environment variables later, you need to rebuild the image with `docker compose build` for the changes to take effect.
|
||||
Note: All environment variables starting with `NEXT_PUBLIC_` are used at build time. When you change environment variables later, you need to rebuild the image with `docker compose build` for the changes to take effect.
|
||||
|
||||
3. Finally start the docker compose process to build and spin up the Formbricks container as well as the PostgreSQL database.
|
||||
|
||||
@@ -120,12 +152,12 @@ Certainly, here is the reformatted version for Formbricks environment variables.
|
||||
|
||||
These variables must also be provided at runtime.
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| ---------------------- | --------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------- |
|
||||
| NEXT_PUBLIC_WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
|
||||
| DATABASE_URL | Database URL with credentials. | required | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` |
|
||||
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
|
||||
| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` |
|
||||
| Variable | Description | Required | Default |
|
||||
| --------------- | --------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------- |
|
||||
| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
|
||||
| DATABASE_URL | Database URL with credentials. | required | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` |
|
||||
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
|
||||
| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` |
|
||||
|
||||
### Build-time Variables
|
||||
|
||||
@@ -164,4 +196,16 @@ docker compose logs -f
|
||||
|
||||
You can close the logs again with `CTRL + C`.
|
||||
|
||||
## Troubleshooting for the Shell Script Setup
|
||||
|
||||
If you encounter any issues, consider the following steps:
|
||||
|
||||
- **Inbound Rules**: Make sure you have added inbound rules for Port 80 and 443 in your VM's Security Group.
|
||||
|
||||
- **A Record**: Verify that you have set up an A record for your domain, pointing to your VM's IP address.
|
||||
|
||||
- **Check Docker Instances**: Run `docker ps` to check the status of the Docker instances.
|
||||
|
||||
- **Check Formbricks Logs**: Run `cd formbricks && docker compose logs` to check the logs of the Formbricks stack.
|
||||
|
||||
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;
|
||||
|
||||
@@ -41,7 +41,7 @@ export const meta = {
|
||||
]}
|
||||
example={`{
|
||||
"url": "https://mysystem.com/myendpoint",
|
||||
"trigger": "responseFinished"
|
||||
"triggers": ["responseFinished"]
|
||||
}`}
|
||||
responses={[
|
||||
{
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Layout } from "@/components/docs/Layout";
|
||||
import { Fence } from "@/components/shared/Fence";
|
||||
|
||||
export const meta = {
|
||||
title: "Webhook Payload",
|
||||
description: "Learn how to handle the Formbricks API payload.",
|
||||
};
|
||||
|
||||
This documentation helps understand the payload structure that will be received when the webhook is triggered in Formbricks.
|
||||
|
||||
## An example webhook payload
|
||||
|
||||
```
|
||||
{
|
||||
"webhookId": "cljwxvjos0003qhnvj2jg4k5i",
|
||||
"event": "responseCreated",
|
||||
"data": {
|
||||
"id": "cljwy2m8r0001qhclco1godnu",
|
||||
"createdAt": "2023-07-10T14:14:17.115Z",
|
||||
"updatedAt": "2023-07-10T14:14:17.115Z",
|
||||
"surveyId": "cljsf3d7a000019cv9apt2t27",
|
||||
"finished": false,
|
||||
"data": {
|
||||
"qumbk3fkr6cky8850bvvq5z1": "Executive"
|
||||
},
|
||||
"meta": {
|
||||
"userAgent": {
|
||||
"os": "Mac OS",
|
||||
"browser": "Chrome"
|
||||
}
|
||||
},
|
||||
"personAttributes": {
|
||||
"email": "test@web.com",
|
||||
"userId": "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING"
|
||||
},
|
||||
"person": {
|
||||
"id": "cljold01t0000qh8ewzigzmjk",
|
||||
"attributes": {
|
||||
"email": "test@web.com",
|
||||
"userId": "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING"
|
||||
},
|
||||
"createdAt": "2023-07-04T17:56:17.154Z",
|
||||
"updatedAt": "2023-07-04T17:56:17.154Z"
|
||||
},
|
||||
"notes": [],
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
| Variable | Type | Description |
|
||||
| --------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| webhookId | String | Webhook's Id |
|
||||
| event | String | The name of the trigger event [responseCreated, responseUpdated, responseFinished] |
|
||||
| data | Object | Contains the details of the newly created response. |
|
||||
| data.id | String | Formbricks Response ID. |
|
||||
| data.createdAt | String | The timestamp when the response was created. |
|
||||
| data.updatedAt | String | The timestamp when the response was last updated. |
|
||||
| data.surveyId | String | The identifier of the survey associated with this response. |
|
||||
| data.finished | Boolean | A boolean value indicating whether the survey response is marked as finished. |
|
||||
| data.data | Object | An object containing the response data, where keys are question identifiers, and values are the corresponding answers given by the respondent. |
|
||||
| data.meta | Object | Additional metadata related to the response, such as the user's operating system and browser information. |
|
||||
| data.personAttributes | Object | An object with attributes related to the respondent, such as their email and a user ID (if available). |
|
||||
| data.person | Object | Information about the respondent, including their unique id, attributes, and creation/update timestamps. |
|
||||
| data.notes | Array | An array of notes associated with the response (if any). |
|
||||
| data.tags | Array | An array of tags assigned to the response (if any). |
|
||||
|
||||
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;
|
||||
@@ -2,13 +2,13 @@ import Layout from "@/components/shared/LayoutMdx";
|
||||
|
||||
export const meta = {
|
||||
title: "Imprint",
|
||||
description: "Imprint of formbricks.com",
|
||||
};
|
||||
|
||||
## Information according to § 5 TMG
|
||||
|
||||
Johannes Dancker & Matthias Nannt\
|
||||
Formbricks GmbH\
|
||||
Kuhnkestr. 6\
|
||||
c/o Starterkitchen\
|
||||
24118 Kiel\
|
||||
Germany
|
||||
|
||||
@@ -18,19 +18,24 @@ E-Mail: hola@formbricks.com
|
||||
|
||||
## EU dispute resolution
|
||||
|
||||
The European Commission provides a platform for online dispute resolution (OS): https://ec.europa.eu/consumers/odr.\
|
||||
You can find our e-mail address in the imprint above.\
|
||||
Consumer dispute resolution/universal dispute resolution body\
|
||||
The European Commission provides a platform for online dispute resolution (OS): https://ec.europa.eu/consumers/odr
|
||||
|
||||
You can also reach out via the e-mail address in the imprint above.
|
||||
|
||||
### Consumer dispute resolution/universal dispute resolution body
|
||||
|
||||
We are not willing or obliged to participate in dispute resolution proceedings before a consumer arbitration board.
|
||||
|
||||
## Liability for contents
|
||||
|
||||
As a service provider, we are responsible for our own content on these pages in accordance with § 7 paragraph 1 TMG under the general laws. According to §§ 8 to 10 TMG, we are not obligated to monitor transmitted or stored information or to investigate circumstances that indicate illegal activity.\
|
||||
|
||||
Obligations to remove or block the use of information under the general laws remain unaffected. However, liability in this regard is only possible from the point in time at which a concrete infringement of the law becomes known. If we become aware of any such infringements, we will remove the relevant content immediately.
|
||||
|
||||
## Liability for links
|
||||
|
||||
Our offer contains links to external websites of third parties, on whose contents we have no influence. Therefore, we cannot assume any liability for these external contents. The respective provider or operator of the sites is always responsible for the content of the linked sites. The linked pages were checked for possible legal violations at the time of linking. Illegal contents were not recognizable at the time of linking.\
|
||||
|
||||
However, a permanent control of the contents of the linked pages is not reasonable without concrete evidence of a violation of the law. If we become aware of any infringements, we will remove such links immediately.
|
||||
|
||||
## Copyright
|
||||
|
||||
@@ -2,105 +2,17 @@ import Layout from "@/components/shared/Layout";
|
||||
import HeroTitle from "@/components/shared/HeroTitle";
|
||||
import { Button } from "@formbricks/ui";
|
||||
|
||||
const OSSFriends = [
|
||||
{
|
||||
name: "BoxyHQ",
|
||||
description:
|
||||
"BoxyHQ’s suite of APIs for security and privacy helps engineering teams build and ship compliant cloud applications faster.",
|
||||
href: "https://boxyhq.com",
|
||||
},
|
||||
{
|
||||
name: "Cal.com",
|
||||
description:
|
||||
"Cal.com is a scheduling tool that helps you schedule meetings without the back-and-forth emails.",
|
||||
href: "https://cal.com",
|
||||
},
|
||||
{
|
||||
name: "Crowd.dev",
|
||||
description:
|
||||
"Centralize community, product, and customer data to understand which companies are engaging with your open source project.",
|
||||
href: "https://www.crowd.dev",
|
||||
},
|
||||
{
|
||||
name: "Documenso",
|
||||
description:
|
||||
"The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.",
|
||||
href: "https://documenso.com",
|
||||
},
|
||||
{
|
||||
name: "Erxes",
|
||||
description:
|
||||
"The Open-Source HubSpot Alternative. A single XOS enables to create unique and life-changing experiences that work for all types of business.",
|
||||
href: "https://erxes.io",
|
||||
},
|
||||
{
|
||||
name: "Formbricks",
|
||||
description:
|
||||
"Survey granular user segments at any point in the user journey. Gather up to 6x more insights with targeted micro-surveys. All open-source.",
|
||||
href: "https://formbricks.com",
|
||||
},
|
||||
{
|
||||
name: "GitWonk",
|
||||
description:
|
||||
"GitWonk is an open-source technical documentation tool, designed and built focusing on the developer experience.",
|
||||
href: "https://gitwonk.com",
|
||||
},
|
||||
{
|
||||
name: "Hanko",
|
||||
description:
|
||||
"Open-source authentication and user management for the passkey era. Integrated in minutes, for web and mobile apps.",
|
||||
href: "https://www.hanko.io",
|
||||
},
|
||||
{
|
||||
name: "HTMX",
|
||||
description:
|
||||
"HTMX is a dependency-free JavaScript library that allows you to access AJAX, CSS Transitions, WebSockets, and Server Sent Events directly in HTML.",
|
||||
href: "https://htmx.org",
|
||||
},
|
||||
{
|
||||
name: "Infisical",
|
||||
description:
|
||||
"Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.",
|
||||
href: "https://infisical.com",
|
||||
},
|
||||
{
|
||||
name: "Novu",
|
||||
description:
|
||||
"The open-source notification infrastructure for developers. Simple components and APIs for managing all communication channels in one place.",
|
||||
href: "https://novu.co",
|
||||
},
|
||||
{
|
||||
name: "OpenBB",
|
||||
description:
|
||||
"Democratizing investment research through an open source financial ecosystem. The OpenBB Terminal allows everyone to perform investment research, from everywhere.",
|
||||
href: "https://openbb.co",
|
||||
},
|
||||
{
|
||||
name: "Sniffnet",
|
||||
description:
|
||||
"Sniffnet is a network monitoring tool to help you easily keep track of your Internet traffic.",
|
||||
href: "https://www.sniffnet.net",
|
||||
},
|
||||
{
|
||||
name: "Typebot",
|
||||
description:
|
||||
"Typebot gives you powerful blocks to create unique chat experiences. Embed them anywhere on your apps and start collecting results like magic.",
|
||||
href: "https://typebot.io",
|
||||
},
|
||||
{
|
||||
name: "Webiny",
|
||||
description:
|
||||
"Open-source enterprise-grade serverless CMS. Own your data. Scale effortlessly. Customize everything.",
|
||||
href: "https://www.webiny.com",
|
||||
},
|
||||
{
|
||||
name: "Webstudio",
|
||||
description: "Webstudio is an open source alternative to Webflow",
|
||||
href: "https://webstudio.is",
|
||||
},
|
||||
];
|
||||
type OSSFriend = {
|
||||
href: string;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export default function OSSFriendsPage() {
|
||||
type Props = {
|
||||
OSSFriends: OSSFriend[];
|
||||
};
|
||||
|
||||
export default function OSSFriendsPage({ OSSFriends }: Props) {
|
||||
return (
|
||||
<Layout title="OSS Friends" description="Open-source projects and tools for an open world.">
|
||||
<HeroTitle headingPt1="Our" headingTeal="Open-source" headingPt2="Friends" />
|
||||
@@ -122,3 +34,16 @@ export default function OSSFriendsPage() {
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
const res = await fetch("https://formbricks.com/api/oss-friends");
|
||||
const data = await res.json();
|
||||
|
||||
// By returning { props: { OSSFriends } }, the OSSFriendsPage component
|
||||
// will receive `OSSFriends` as a prop at build time
|
||||
return {
|
||||
props: {
|
||||
OSSFriends: data.data,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import Layout from "@/components/shared/LayoutMdx";
|
||||
|
||||
export const meta = {
|
||||
title: "Privacy Policy",
|
||||
description: "Formbricks Privacy Policy",
|
||||
};
|
||||
|
||||
## **1. Introduction**
|
||||
@@ -114,9 +115,8 @@ Please use the following contact information for privacy inquiries:
|
||||
|
||||
privacy@formbricks.com
|
||||
|
||||
Johannes Dancker & Matthias Nannt<br/>
|
||||
Formbricks GmbH<br/>
|
||||
Kuhnkestr. 6<br/>
|
||||
c/o Starterkitchen<br/>
|
||||
24118 Kiel<br/>
|
||||
Germany
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Callout } from "@/components/shared/Callout";
|
||||
|
||||
export const meta = {
|
||||
title: "Terms of Service",
|
||||
description: "Terms of Service of Formbricks Cloud.",
|
||||
};
|
||||
|
||||
These Terms of Use constitute a legally binding agreement made between you, whether personally or on behalf of an entity (“you”) and Formbricks ("**Company**", “**we**”, “**us**”, or “**our**”), concerning your access to and use of the https://formbricks.com website as well as any other media form, media channel, mobile website or mobile application related, linked, or otherwise connected thereto (collectively, the “Site”). You agree that by accessing the Site, you have read, understood, and agree to be bound by all of these Terms of Use. If you do not agree with all of these terms of use, then you are expressly prohibited from using the site and you must discontinue use immediately.
|
||||
|
||||
3
apps/web/.gitignore
vendored
@@ -34,3 +34,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Sentry Auth Token
|
||||
.sentryclirc
|
||||
|
||||
23
apps/web/CHANGELOG.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# @formbricks/web
|
||||
|
||||
## 1.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3dde021c: Release version 1.0.2
|
||||
- Updated dependencies [3dde021c]
|
||||
- @formbricks/js@1.0.3
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [a1b447ca]
|
||||
- @formbricks/js@1.0.2
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [3d0d633b]
|
||||
- @formbricks/js@1.0.1
|
||||
@@ -10,7 +10,7 @@ COPY .env.docker /app/apps/web/.env
|
||||
RUN pnpm install
|
||||
|
||||
# Build the project
|
||||
RUN pnpm prebuild --filter=web...
|
||||
RUN pnpm post-install --filter=web...
|
||||
RUN pnpm turbo run build --filter=web...
|
||||
|
||||
FROM node:18-alpine AS runner
|
||||
@@ -36,6 +36,8 @@ COPY --from=installer --chown=nextjs:nodejs /app/packages/database/migrations ./
|
||||
|
||||
ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD if [ "$NEXTAUTH_SECRET" != "RANDOM_STRING" ]; then \
|
||||
pnpm dlx prisma migrate deploy && node apps/web/server.js; \
|
||||
else \
|
||||
|
||||
@@ -5,14 +5,6 @@ import { formbricksEnabled } from "@/lib/formbricks";
|
||||
import formbricks from "@formbricks/js";
|
||||
import { useEffect } from "react";
|
||||
|
||||
/* if (typeof window !== "undefined" && formbricksEnabled) {
|
||||
formbricks.init({
|
||||
environmentId: env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID || "",
|
||||
apiHost: env.NEXT_PUBLIC_FORMBRICKS_API_HOST || "",
|
||||
debug: true,
|
||||
});
|
||||
} */
|
||||
|
||||
export default function FormbricksClient({ session }) {
|
||||
useEffect(() => {
|
||||
if (formbricksEnabled && session.user && formbricks) {
|
||||
@@ -17,8 +17,7 @@ export default function ConfirmationPage() {
|
||||
<div className="my-6 sm:flex-auto">
|
||||
<h1 className="text-center text-xl font-semibold text-slate-900">Upgrade successful</h1>
|
||||
<p className="mt-2 text-center text-sm text-slate-700">
|
||||
Thanks a lot for upgrading your formbricks subscription. You can now access all features and
|
||||
improve your user research.
|
||||
Thanks a lot for upgrading your Formbricks subscription. You have now unlimited access.
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="darkCTA" className="w-full justify-center" href="/">
|
||||
@@ -1,18 +1,18 @@
|
||||
import SecondNavbar from "../environments/SecondNavBar";
|
||||
import SecondNavbar from "@/components/environments/SecondNavBar";
|
||||
import { CursorArrowRaysIcon, TagIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
interface EventsAttributesTabsProps {
|
||||
interface ActionsAttributesTabsProps {
|
||||
activeId: string;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
export default function EventsAttributesTabs({ activeId, environmentId }: EventsAttributesTabsProps) {
|
||||
export default function ActionsAttributesTabs({ activeId, environmentId }: ActionsAttributesTabsProps) {
|
||||
const tabs = [
|
||||
{
|
||||
id: "events",
|
||||
id: "actions",
|
||||
label: "Actions",
|
||||
icon: <CursorArrowRaysIcon />,
|
||||
href: `/environments/${environmentId}/events`,
|
||||
href: `/environments/${environmentId}/actions`,
|
||||
},
|
||||
{
|
||||
id: "attributes",
|
||||
@@ -8,11 +8,11 @@ import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/r
|
||||
|
||||
interface ActivityTabProps {
|
||||
environmentId: string;
|
||||
eventClassId: string;
|
||||
actionClassId: string;
|
||||
}
|
||||
|
||||
export default function EventActivityTab({ environmentId, eventClassId }: ActivityTabProps) {
|
||||
const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, eventClassId);
|
||||
export default function EventActivityTab({ environmentId, actionClassId }: ActivityTabProps) {
|
||||
const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClassId);
|
||||
|
||||
if (isLoadingEventClass) return <LoadingSpinner />;
|
||||
if (isErrorEventClass) return <ErrorComponent />;
|
||||
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
import { useState } from "react";
|
||||
import AddNoCodeActionModal from "./AddNoCodeActionModal";
|
||||
import ActionDetailModal from "./ActionDetailModal";
|
||||
import { TActionClass } from "@formbricks/types/v1/actionClasses";
|
||||
|
||||
export default function ActionClassesTable({
|
||||
environmentId,
|
||||
actionClasses,
|
||||
children: [TableHeading, actionRows],
|
||||
}: {
|
||||
environmentId: string;
|
||||
actionClasses: TActionClass[];
|
||||
children: [JSX.Element, JSX.Element[]];
|
||||
}) {
|
||||
const [isActionDetailModalOpen, setActionDetailModalOpen] = useState(false);
|
||||
const [isAddActionModalOpen, setAddActionModalOpen] = useState(false);
|
||||
|
||||
const [activeActionClass, setActiveActionClass] = useState<TActionClass>({
|
||||
environmentId,
|
||||
id: "",
|
||||
name: "",
|
||||
type: "noCode",
|
||||
description: "",
|
||||
noCodeConfig: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
const handleOpenActionDetailModalClick = (e, actionClass: TActionClass) => {
|
||||
e.preventDefault();
|
||||
setActiveActionClass(actionClass);
|
||||
setActionDetailModalOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 text-right">
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
onClick={() => {
|
||||
setAddActionModalOpen(true);
|
||||
}}>
|
||||
<CursorArrowRaysIcon className="mr-2 h-5 w-5 text-white" />
|
||||
Add Action
|
||||
</Button>
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
{TableHeading}
|
||||
<div className="grid-cols-7">
|
||||
{actionClasses.map((actionClass, index) => (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
handleOpenActionDetailModalClick(e, actionClass);
|
||||
}}
|
||||
className="w-full"
|
||||
key={actionClass.id}>
|
||||
{actionRows[index]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ActionDetailModal
|
||||
environmentId={environmentId}
|
||||
open={isActionDetailModalOpen}
|
||||
setOpen={setActionDetailModalOpen}
|
||||
actionClass={activeActionClass}
|
||||
/>
|
||||
<AddNoCodeActionModal
|
||||
environmentId={environmentId}
|
||||
open={isAddActionModalOpen}
|
||||
setOpen={setAddActionModalOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +1,31 @@
|
||||
import ModalWithTabs from "@/components/shared/ModalWithTabs";
|
||||
import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/react/24/solid";
|
||||
import type { EventClass } from "@prisma/client";
|
||||
import EventActivityTab from "./EventActivityTab";
|
||||
import EventSettingsTab from "./EventSettingsTab";
|
||||
import EventActivityTab from "./ActionActivityTab";
|
||||
import ActionSettingsTab from "./ActionSettingsTab";
|
||||
import { TActionClass } from "@formbricks/types/v1/actionClasses";
|
||||
|
||||
interface EventDetailModalProps {
|
||||
interface ActionDetailModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
eventClass: EventClass;
|
||||
actionClass: TActionClass;
|
||||
}
|
||||
|
||||
export default function EventDetailModal({
|
||||
export default function ActionDetailModal({
|
||||
environmentId,
|
||||
open,
|
||||
setOpen,
|
||||
eventClass,
|
||||
}: EventDetailModalProps) {
|
||||
actionClass,
|
||||
}: ActionDetailModalProps) {
|
||||
const tabs = [
|
||||
{
|
||||
title: "Activity",
|
||||
children: <EventActivityTab environmentId={environmentId} eventClassId={eventClass.id} />,
|
||||
children: <EventActivityTab environmentId={environmentId} actionClassId={actionClass.id} />,
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
children: (
|
||||
<EventSettingsTab environmentId={environmentId} eventClassId={eventClass.id} setOpen={setOpen} />
|
||||
<ActionSettingsTab environmentId={environmentId} actionClass={actionClass} setOpen={setOpen} />
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -37,16 +37,16 @@ export default function EventDetailModal({
|
||||
setOpen={setOpen}
|
||||
tabs={tabs}
|
||||
icon={
|
||||
eventClass.type === "code" ? (
|
||||
actionClass.type === "code" ? (
|
||||
<CodeBracketIcon />
|
||||
) : eventClass.type === "noCode" ? (
|
||||
) : actionClass.type === "noCode" ? (
|
||||
<CursorArrowRaysIcon />
|
||||
) : eventClass.type === "automatic" ? (
|
||||
) : actionClass.type === "automatic" ? (
|
||||
<SparklesIcon />
|
||||
) : null
|
||||
}
|
||||
label={eventClass.name}
|
||||
description={eventClass.description || ""}
|
||||
label={actionClass.name}
|
||||
description={actionClass.description || ""}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
import { timeSinceConditionally } from "@formbricks/lib/time";
|
||||
import { TActionClass } from "@formbricks/types/v1/actionClasses";
|
||||
import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export default function ActionClassDataRow({ actionClass }: { actionClass: TActionClass }) {
|
||||
return (
|
||||
<div className="m-2 grid h-16 grid-cols-6 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-5 w-5 flex-shrink-0 text-slate-500">
|
||||
{actionClass.type === "code" ? (
|
||||
<CodeBracketIcon />
|
||||
) : actionClass.type === "noCode" ? (
|
||||
<CursorArrowRaysIcon />
|
||||
) : actionClass.type === "automatic" ? (
|
||||
<SparklesIcon />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">{actionClass.name}</div>
|
||||
<div className="text-xs text-slate-400">{actionClass.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
{timeSinceConditionally(actionClass.createdAt.toString())}
|
||||
</div>
|
||||
<div className="text-center"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import DeleteDialog from "@/components/shared/DeleteDialog";
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import { deleteEventClass, useEventClass, useEventClasses } from "@/lib/eventClasses/eventClasses";
|
||||
import { useEventClassMutation } from "@/lib/eventClasses/mutateEventClasses";
|
||||
import type { Event, NoCodeConfig } from "@formbricks/types/events";
|
||||
import type { NoCodeConfig } from "@formbricks/types/events";
|
||||
import {
|
||||
Button,
|
||||
ErrorComponent,
|
||||
Input,
|
||||
Label,
|
||||
RadioGroup,
|
||||
@@ -18,46 +16,46 @@ import {
|
||||
} from "@formbricks/ui";
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import clsx from "clsx";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { testURLmatch } from "./testURLmatch";
|
||||
import { deleteActionClass, updateActionClass } from "@formbricks/lib/services/actionClass";
|
||||
import { TActionClassInput } from "@formbricks/types/v1/actionClasses";
|
||||
|
||||
interface EventSettingsTabProps {
|
||||
interface ActionSettingsTabProps {
|
||||
environmentId: string;
|
||||
eventClassId: string;
|
||||
actionClass: any;
|
||||
setOpen: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export default function EventSettingsTab({ environmentId, eventClassId, setOpen }: EventSettingsTabProps) {
|
||||
const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, eventClassId);
|
||||
export default function ActionSettingsTab({ environmentId, actionClass, setOpen }: ActionSettingsTabProps) {
|
||||
const router = useRouter();
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||
|
||||
const { register, handleSubmit, control, watch } = useForm({
|
||||
defaultValues: {
|
||||
name: eventClass.name,
|
||||
description: eventClass.description,
|
||||
noCodeConfig: eventClass.noCodeConfig,
|
||||
name: actionClass.name,
|
||||
description: actionClass.description,
|
||||
noCodeConfig: actionClass.noCodeConfig,
|
||||
},
|
||||
});
|
||||
const { triggerEventClassMutate, isMutatingEventClass } = useEventClassMutation(
|
||||
environmentId,
|
||||
eventClass.id
|
||||
);
|
||||
|
||||
const { mutateEventClasses } = useEventClasses(environmentId);
|
||||
const [isUpdatingAction, setIsUpdatingAction] = useState(false);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as NoCodeConfig);
|
||||
|
||||
const updatedData: Event = {
|
||||
const updatedData: TActionClassInput = {
|
||||
...data,
|
||||
noCodeConfig: filteredNoCodeConfig,
|
||||
type: "noCode",
|
||||
} as Event;
|
||||
} as TActionClassInput;
|
||||
|
||||
await triggerEventClassMutate(updatedData);
|
||||
mutateEventClasses();
|
||||
setIsUpdatingAction(true);
|
||||
await updateActionClass(environmentId, actionClass.id, updatedData);
|
||||
router.refresh();
|
||||
setIsUpdatingAction(false);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
@@ -83,9 +81,6 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
|
||||
if (match === "no") toast.error("Your survey would not be shown.");
|
||||
};
|
||||
|
||||
if (isLoadingEventClass) return <LoadingSpinner />;
|
||||
if (isErrorEventClass) return <ErrorComponent />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
@@ -95,8 +90,8 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
|
||||
type="text"
|
||||
placeholder="e.g. Product Team Info"
|
||||
{...register("name", {
|
||||
value: eventClass.name,
|
||||
disabled: eventClass.type === "automatic" || eventClass.type === "code" ? true : false,
|
||||
value: actionClass.name,
|
||||
disabled: actionClass.type === "automatic" || actionClass.type === "code" ? true : false,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
@@ -106,18 +101,18 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
|
||||
type="text"
|
||||
placeholder="e.g. Triggers when user changed subscription"
|
||||
{...register("description", {
|
||||
value: eventClass.description,
|
||||
disabled: eventClass.type === "automatic" ? true : false,
|
||||
value: actionClass.description,
|
||||
disabled: actionClass.type === "automatic" ? true : false,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
<div className="">
|
||||
<Label>Action Type</Label>
|
||||
{eventClass.type === "code" ? (
|
||||
{actionClass.type === "code" ? (
|
||||
<p className="text-sm text-slate-600">
|
||||
This is a code action. Please make changes in your code base.
|
||||
</p>
|
||||
) : eventClass.type === "noCode" ? (
|
||||
) : actionClass.type === "noCode" ? (
|
||||
<div className="flex justify-between rounded-lg">
|
||||
<div className="w-full space-y-4">
|
||||
<Controller
|
||||
@@ -258,7 +253,7 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : eventClass.type === "automatic" ? (
|
||||
) : actionClass.type === "automatic" ? (
|
||||
<p className="text-sm text-slate-600">
|
||||
This action was created automatically. You cannot make changes to it.
|
||||
</p>
|
||||
@@ -266,7 +261,7 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
|
||||
</div>
|
||||
<div className="flex justify-between border-t border-slate-200 py-6">
|
||||
<div>
|
||||
{eventClass.type !== "automatic" && (
|
||||
{actionClass.type !== "automatic" && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="warn"
|
||||
@@ -281,9 +276,9 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
|
||||
Read Docs
|
||||
</Button>
|
||||
</div>
|
||||
{eventClass.type !== "automatic" && (
|
||||
{actionClass.type !== "automatic" && (
|
||||
<div className="flex space-x-2">
|
||||
<Button type="submit" variant="darkCTA" loading={isMutatingEventClass}>
|
||||
<Button type="submit" variant="darkCTA" loading={isUpdatingAction}>
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
@@ -298,8 +293,8 @@ export default function EventSettingsTab({ environmentId, eventClassId, setOpen
|
||||
onDelete={async () => {
|
||||
setOpen(false);
|
||||
try {
|
||||
await deleteEventClass(environmentId, eventClass.id);
|
||||
mutateEventClasses();
|
||||
await deleteActionClass(environmentId, actionClass.id);
|
||||
router.refresh();
|
||||
toast.success("Action deleted successfully");
|
||||
} catch (error) {
|
||||
toast.error("Something went wrong. Please try again.");
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function ActionTableHeading() {
|
||||
return (
|
||||
<>
|
||||
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-4 pl-6 ">User Actions</div>
|
||||
<div className="col-span-2 text-center">Created</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { createEventClass } from "@/lib/eventClasses/eventClasses";
|
||||
import type { Event, NoCodeConfig } from "@formbricks/types/events";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
@@ -21,24 +19,22 @@ import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
import { testURLmatch } from "./testURLmatch";
|
||||
import { createActionClass } from "@formbricks/lib/services/actionClass";
|
||||
import { TActionClassInput, TActionClassNoCodeConfig } from "@formbricks/types/v1/actionClasses";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface EventDetailModalProps {
|
||||
interface AddNoCodeActionModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
mutateEventClasses: (data?: any) => void;
|
||||
}
|
||||
|
||||
export default function AddNoCodeEventModal({
|
||||
environmentId,
|
||||
open,
|
||||
setOpen,
|
||||
mutateEventClasses,
|
||||
}: EventDetailModalProps) {
|
||||
export default function AddNoCodeActionModal({ environmentId, open, setOpen }: AddNoCodeActionModalProps) {
|
||||
const router = useRouter();
|
||||
const { register, control, handleSubmit, watch, reset } = useForm();
|
||||
|
||||
// clean up noCodeConfig before submitting by removing unnecessary fields
|
||||
const filterNoCodeConfig = (noCodeConfig: NoCodeConfig): NoCodeConfig => {
|
||||
const filterNoCodeConfig = (noCodeConfig: TActionClassNoCodeConfig): TActionClassNoCodeConfig => {
|
||||
const { type } = noCodeConfig;
|
||||
return {
|
||||
type,
|
||||
@@ -46,18 +42,18 @@ export default function AddNoCodeEventModal({
|
||||
};
|
||||
};
|
||||
|
||||
const submitEventClass = async (data: Partial<Event>): Promise<void> => {
|
||||
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as NoCodeConfig);
|
||||
const submitEventClass = async (data: Partial<TActionClassInput>): Promise<void> => {
|
||||
const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as TActionClassNoCodeConfig);
|
||||
|
||||
const updatedData: Event = {
|
||||
const updatedData: TActionClassInput = {
|
||||
...data,
|
||||
noCodeConfig: filteredNoCodeConfig,
|
||||
type: "noCode",
|
||||
} as Event;
|
||||
} as TActionClassInput;
|
||||
|
||||
try {
|
||||
await createEventClass(environmentId, updatedData);
|
||||
mutateEventClasses();
|
||||
await createActionClass(environmentId, updatedData);
|
||||
router.refresh();
|
||||
reset();
|
||||
setOpen(false);
|
||||
toast.success("Action added successfully.");
|
||||
@@ -0,0 +1,11 @@
|
||||
import ActionsAttributesTabs from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs";
|
||||
import ContentWrapper from "@/components/shared/ContentWrapper";
|
||||
|
||||
export default function ActionsAndAttributesLayout({ params, children }) {
|
||||
return (
|
||||
<>
|
||||
<ActionsAttributesTabs activeId="actions" environmentId={params.environmentId} />
|
||||
<ContentWrapper>{children}</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 text-right">
|
||||
<Button
|
||||
variant="darkCTA"
|
||||
className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-gray-200">
|
||||
<CursorArrowRaysIcon className="mr-2 h-5 w-5 text-white" />
|
||||
Loading
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-6 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<span className="sr-only">Edit</span>
|
||||
<div className="col-span-4 pl-6 ">User Actions</div>
|
||||
<div className="col-span-2 text-center">Created</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div key={index} className="m-2 grid h-16 grid-cols-6 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-6 w-6 flex-shrink-0 animate-pulse rounded-full bg-gray-200 text-slate-500"></div>
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">
|
||||
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-400">
|
||||
<div className="h-2 w-24 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-28 h-4 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
<div className="text-center"></div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import ActionClassesTable from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionClassesTable";
|
||||
import ActionClassDataRow from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionRowData";
|
||||
import ActionTableHeading from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionTableHeading";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getActionClasses } from "@formbricks/lib/services/actionClass";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Actions",
|
||||
};
|
||||
|
||||
export default async function ActionClassesComponent({ params }) {
|
||||
let actionClasses = await getActionClasses(params.environmentId);
|
||||
return (
|
||||
<>
|
||||
<ActionClassesTable environmentId={params.environmentId} actionClasses={actionClasses}>
|
||||
<ActionTableHeading />
|
||||
{actionClasses.map((actionClass) => (
|
||||
<ActionClassDataRow key={actionClass.id} actionClass={actionClass} />
|
||||
))}
|
||||
</ActionClassesTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import { Switch } from "@formbricks/ui";
|
||||
import { useState } from "react";
|
||||
import AttributeDetailModal from "./AttributeDetailModal";
|
||||
import UploadAttributesModal from "./UploadAttributesModal";
|
||||
import { useMemo } from "react";
|
||||
import { TAttributeClass } from "@formbricks/types/v1/attributeClasses";
|
||||
|
||||
export default function AttributeClassesTable({
|
||||
environmentId,
|
||||
attributeClasses,
|
||||
children: [TableHeading, howToAddAttributeButton, attributeRows],
|
||||
}: {
|
||||
environmentId: string;
|
||||
attributeClasses: TAttributeClass[];
|
||||
children: [JSX.Element, JSX.Element, JSX.Element[]];
|
||||
}) {
|
||||
const [isAttributeDetailModalOpen, setAttributeDetailModalOpen] = useState(false);
|
||||
const [isUploadCSVModalOpen, setUploadCSVModalOpen] = useState(false);
|
||||
const [activeAttributeClass, setActiveAttributeClass] = useState("" as any);
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
|
||||
const displayedAttributeClasses = useMemo(() => {
|
||||
return attributeClasses
|
||||
? showArchived
|
||||
? attributeClasses
|
||||
: attributeClasses.filter((ac) => !ac.archived)
|
||||
: [];
|
||||
}, [showArchived, attributeClasses]);
|
||||
|
||||
const hasArchived = useMemo(() => {
|
||||
return attributeClasses ? attributeClasses.some((ac) => ac.archived) : false;
|
||||
}, [attributeClasses]);
|
||||
|
||||
const handleOpenAttributeDetailModalClick = (e, attributeClass) => {
|
||||
e.preventDefault();
|
||||
setActiveAttributeClass(attributeClass);
|
||||
setAttributeDetailModalOpen(true);
|
||||
};
|
||||
|
||||
const toggleShowArchived = () => {
|
||||
setShowArchived(!showArchived);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 flex items-center justify-end text-right">
|
||||
{hasArchived && (
|
||||
<div className="flex items-center text-sm font-medium">
|
||||
Show archived
|
||||
<Switch className="mx-3" checked={showArchived} onCheckedChange={toggleShowArchived} />
|
||||
</div>
|
||||
)}
|
||||
{howToAddAttributeButton}
|
||||
</div>
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
{TableHeading}
|
||||
<div className="grid-cols-7">
|
||||
{displayedAttributeClasses.map((attributeClass, index) => (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
handleOpenAttributeDetailModalClick(e, attributeClass);
|
||||
}}
|
||||
className="w-full"
|
||||
key={attributeClass.id}>
|
||||
{attributeRows[index]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<AttributeDetailModal
|
||||
environmentId={environmentId}
|
||||
open={isAttributeDetailModalOpen}
|
||||
setOpen={setAttributeDetailModalOpen}
|
||||
attributeClass={activeAttributeClass}
|
||||
/>
|
||||
<UploadAttributesModal open={isUploadCSVModalOpen} setOpen={setUploadCSVModalOpen} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -24,13 +24,7 @@ export default function AttributeDetailModal({
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
children: (
|
||||
<AttributeSettingsTab
|
||||
attributeClass={attributeClass}
|
||||
environmentId={environmentId}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
),
|
||||
children: <AttributeSettingsTab attributeClass={attributeClass} setOpen={setOpen} />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { timeSinceConditionally } from "@formbricks/lib/time";
|
||||
import { Badge } from "@formbricks/ui";
|
||||
import { TagIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export default function AttributeClassDataRow({ attributeClass }) {
|
||||
return (
|
||||
<div className="m-2 grid h-16 grid-cols-5 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 flex-shrink-0">
|
||||
<TagIcon className="h-8 w-8 flex-shrink-0 text-slate-500" />
|
||||
</div>
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">
|
||||
{attributeClass.name}
|
||||
<span className="ml-2">
|
||||
{attributeClass.archived && <Badge text="Archived" type="gray" size="tiny" />}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-400">{attributeClass.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="text-slate-900">{timeSinceConditionally(attributeClass.createdAt.toString())}</div>
|
||||
</div>
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="text-slate-900">{timeSinceConditionally(attributeClass.updatedAt.toString())}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +1,38 @@
|
||||
import { useAttributeClasses } from "@/lib/attributeClasses/attributeClasses";
|
||||
import { useAttributeClassMutation } from "@/lib/attributeClasses/mutateAttributeClasses";
|
||||
"use client";
|
||||
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import type { AttributeClass } from "@prisma/client";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { ArchiveBoxArrowDownIcon, ArchiveBoxXMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { updatetAttributeClass } from "@formbricks/lib/services/attributeClass";
|
||||
import { useState } from "react";
|
||||
|
||||
interface AttributeSettingsTabProps {
|
||||
environmentId: string;
|
||||
attributeClass: AttributeClass;
|
||||
setOpen: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export default function AttributeSettingsTab({
|
||||
environmentId,
|
||||
attributeClass,
|
||||
setOpen,
|
||||
}: AttributeSettingsTabProps) {
|
||||
export default function AttributeSettingsTab({ attributeClass, setOpen }: AttributeSettingsTabProps) {
|
||||
const router = useRouter();
|
||||
const { register, handleSubmit } = useForm({
|
||||
defaultValues: { name: attributeClass.name, description: attributeClass.description },
|
||||
});
|
||||
const { triggerAttributeClassMutate, isMutatingAttributeClass } = useAttributeClassMutation(
|
||||
environmentId,
|
||||
attributeClass.id
|
||||
);
|
||||
|
||||
const { mutateAttributeClasses } = useAttributeClasses(environmentId);
|
||||
const [isAttributeBeingSubmitted, setisAttributeBeingSubmitted] = useState(false);
|
||||
|
||||
const onSubmit = async (data) => {
|
||||
await triggerAttributeClassMutate(data);
|
||||
mutateAttributeClasses();
|
||||
setisAttributeBeingSubmitted(true);
|
||||
setOpen(false);
|
||||
await updatetAttributeClass(attributeClass.id, data);
|
||||
router.refresh();
|
||||
setisAttributeBeingSubmitted(false);
|
||||
};
|
||||
|
||||
const handleArchiveToggle = async () => {
|
||||
setisAttributeBeingSubmitted(true);
|
||||
const data = { archived: !attributeClass.archived };
|
||||
await triggerAttributeClassMutate(data);
|
||||
mutateAttributeClasses();
|
||||
await updatetAttributeClass(attributeClass.id, data);
|
||||
setisAttributeBeingSubmitted(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -101,7 +98,7 @@ export default function AttributeSettingsTab({
|
||||
</div>
|
||||
{attributeClass.type !== "automatic" && (
|
||||
<div className="flex space-x-2">
|
||||
<Button type="submit" variant="darkCTA" loading={isMutatingAttributeClass}>
|
||||
<Button type="submit" variant="darkCTA" loading={isAttributeBeingSubmitted}>
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
export default function AttributeTableHeading() {
|
||||
return (
|
||||
<>
|
||||
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">Name</div>
|
||||
<div className="text-center">Created</div>
|
||||
<div className="text-center">Last Updated</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { QuestionMarkCircleIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export default function HowToAddAttributesButton() {
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
href="http://formbricks.com/docs/attributes/custom-attributes"
|
||||
target="_blank">
|
||||
<QuestionMarkCircleIcon className="mr-2 h-4 w-4" />
|
||||
How to add attributes
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import ActionsAttributesTabs from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs";
|
||||
import ContentWrapper from "@/components/shared/ContentWrapper";
|
||||
|
||||
export default function ActionsAndAttributesLayout({ params, children }) {
|
||||
return (
|
||||
<>
|
||||
<ActionsAttributesTabs activeId="attributes" environmentId={params.environmentId} />
|
||||
<ContentWrapper>{children}</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Button } from "@formbricks/ui";
|
||||
import { TagIcon } from "@heroicons/react/24/solid";
|
||||
import { QuestionMarkCircleIcon } from "@heroicons/react/24/solid";
|
||||
|
||||
export default function Loading() {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-6 text-right">
|
||||
<div className="mb-6 flex items-center justify-end text-right">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="pointer-events-none animate-pulse cursor-not-allowed select-none">
|
||||
<QuestionMarkCircleIcon className="mr-2 h-4 w-4" />
|
||||
Loading Attributes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-5 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
|
||||
<div className="col-span-3 pl-6 ">Name</div>
|
||||
<div className="text-center">Created</div>
|
||||
<div className="text-center">Last Updated</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<div key={index} className="m-2 grid h-16 grid-cols-5 content-center rounded-lg hover:bg-slate-100">
|
||||
<div className="col-span-3 flex items-center pl-6 text-sm">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 flex-shrink-0">
|
||||
<TagIcon className="h-8 w-8 flex-shrink-0 animate-pulse text-slate-500" />
|
||||
</div>
|
||||
<div className="ml-4 text-left">
|
||||
<div className="font-medium text-slate-900">
|
||||
<div className="mt-0 h-4 w-48 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-slate-400">
|
||||
<div className="h-2 w-24 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
<div className="my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||
<div className="m-4 h-4 animate-pulse rounded-full bg-gray-200"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import AttributeClassesTable from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable";
|
||||
import AttributeClassDataRow from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeRowData";
|
||||
import AttributeTableHeading from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeTableHeading";
|
||||
import HowToAddAttributesButton from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/HowToAddAttributesButton";
|
||||
import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { getAttributeClasses } from "@formbricks/lib/services/attributeClass";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Attributes",
|
||||
};
|
||||
|
||||
export default async function AttributesPage({ params }) {
|
||||
let attributeClasses = await getAttributeClasses(params.environmentId);
|
||||
return (
|
||||
<>
|
||||
<AttributeClassesTable environmentId={params.environmentId} attributeClasses={attributeClasses}>
|
||||
<AttributeTableHeading />
|
||||
<HowToAddAttributesButton />
|
||||
|
||||
{attributeClasses.map((attributeClass) => (
|
||||
<AttributeClassDataRow key={attributeClass.id} attributeClass={attributeClass} />
|
||||
))}
|
||||
</AttributeClassesTable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/shared/DropdownMenu";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui";
|
||||
import LoadingSpinner from "@/components/shared/LoadingSpinner";
|
||||
import CreateTeamModal from "@/components/team/CreateTeamModal";
|
||||
import {
|
||||
@@ -65,6 +66,8 @@ import AddProductModal from "./AddProductModal";
|
||||
import { formbricksLogout } from "@/lib/formbricks";
|
||||
import formbricks from "@formbricks/js";
|
||||
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
|
||||
import { MenuIcon } from "lucide-react";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
|
||||
interface EnvironmentsNavbarProps {
|
||||
environmentId: string;
|
||||
@@ -86,6 +89,8 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
|
||||
const [showAddProductModal, setShowAddProductModal] = useState(false);
|
||||
const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
|
||||
|
||||
const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (environment && environment.widgetSetupCompleted) {
|
||||
setWidgetSetupCompleted(true);
|
||||
@@ -117,9 +122,9 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
|
||||
},
|
||||
{
|
||||
name: "Actions & Attributes",
|
||||
href: `/environments/${environmentId}/events`,
|
||||
href: `/environments/${environmentId}/actions`,
|
||||
icon: FilterIcon,
|
||||
current: pathname?.includes("/events") || pathname?.includes("/attributes"),
|
||||
current: pathname?.includes("/actions") || pathname?.includes("/attributes"),
|
||||
},
|
||||
{
|
||||
name: "Integrations",
|
||||
@@ -166,7 +171,7 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
|
||||
icon: CreditCardIcon,
|
||||
label: "Billing & Plan",
|
||||
href: `/environments/${environmentId}/settings/billing`,
|
||||
hidden: IS_FORMBRICKS_CLOUD,
|
||||
hidden: !IS_FORMBRICKS_CLOUD,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -221,19 +226,20 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
|
||||
<nav className="top-0 z-10 w-full border-b border-slate-200 bg-white">
|
||||
{environment?.type === "development" && (
|
||||
<div className="h-6 w-full bg-[#A33700] p-0.5 text-center text-sm text-white">
|
||||
You're in development mode. Use it to test surveys, events and attributes.
|
||||
You're in development mode. Use it to test surveys, actions and attributes.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full px-4 sm:px-6">
|
||||
<div className="flex h-14 justify-between">
|
||||
<div className="flex space-x-4 py-2">
|
||||
<div className="flex space-x-4 py-2">
|
||||
<Link
|
||||
href={`/environments/${environmentId}/surveys/`}
|
||||
className=" flex items-center justify-center rounded-md bg-gradient-to-b text-white transition-all ease-in-out hover:scale-105">
|
||||
{/* <PlusIcon className="h-6 w-6" /> */}
|
||||
<Image src={FaveIcon} width={30} height={30} alt="faveicon" />
|
||||
</Link>
|
||||
|
||||
{navigation.map((item) => {
|
||||
const IconComponent: React.ElementType = item.icon;
|
||||
|
||||
@@ -245,7 +251,7 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
|
||||
item.current
|
||||
? "bg-slate-100 text-slate-900"
|
||||
: "text-slate-900 hover:bg-slate-50 hover:text-slate-900",
|
||||
"inline-flex items-center rounded-md px-2 py-1 text-sm font-medium"
|
||||
"hidden items-center rounded-md px-2 py-1 text-sm font-medium sm:inline-flex"
|
||||
)}
|
||||
aria-current={item.current ? "page" : undefined}>
|
||||
<IconComponent className="mr-3 h-5 w-5" />
|
||||
@@ -254,6 +260,34 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center sm:hidden">
|
||||
<Popover open={mobileNavMenuOpen} onOpenChange={setMobileNavMenuOpen}>
|
||||
<PopoverTrigger onClick={() => setMobileNavMenuOpen(!mobileNavMenuOpen)}>
|
||||
<span>
|
||||
<MenuIcon className="h-6 w-6 rounded-md bg-slate-200 p-1 text-slate-600" />
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="mr-4 bg-slate-200">
|
||||
<div className="flex flex-col">
|
||||
{navigation.map((navItem) => (
|
||||
<Link key={navItem.name} href={navItem.href}>
|
||||
<div
|
||||
onClick={() => setMobileNavMenuOpen(false)}
|
||||
className={cn(
|
||||
"flex items-center space-x-2 rounded-md p-2",
|
||||
navItem.current && "bg-slate-300"
|
||||
)}>
|
||||
<navItem.icon className="h-5 w-5" />
|
||||
<span className="font-medium text-slate-600">{navItem.name}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:ml-6 sm:flex sm:items-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -3,8 +3,8 @@
|
||||
import {
|
||||
QuestionOption,
|
||||
QuestionOptions,
|
||||
} from "@/app/environments/[environmentId]/surveys/[surveyId]/QuestionsComboBox";
|
||||
import { QuestionFilterOptions } from "@/app/environments/[environmentId]/surveys/[surveyId]/ResponseFilter";
|
||||
} from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/QuestionsComboBox";
|
||||
import { QuestionFilterOptions } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/ResponseFilter";
|
||||
import { getTodayDate } from "@/lib/surveys/surveys";
|
||||
import { createContext, useContext, useState } from "react";
|
||||
|
||||
300
apps/web/app/(app)/environments/[environmentId]/actions.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { ResourceNotFoundError } from "@formbricks/errors";
|
||||
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { deleteSurvey, getSurvey } from "@formbricks/lib/services/survey";
|
||||
import { Team } from "@prisma/client";
|
||||
import { Prisma as prismaClient } from "@prisma/client/";
|
||||
|
||||
export async function createTeam(teamName: string, ownerUserId: string): Promise<Team> {
|
||||
const newTeam = await prisma.team.create({
|
||||
data: {
|
||||
name: teamName,
|
||||
memberships: {
|
||||
create: {
|
||||
user: { connect: { id: ownerUserId } },
|
||||
role: "owner",
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
products: {
|
||||
create: [
|
||||
{
|
||||
name: "My Product",
|
||||
environments: {
|
||||
create: [
|
||||
{
|
||||
type: "production",
|
||||
eventClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "Exit Intent (Desktop)",
|
||||
description: "A user on Desktop leaves the website with the cursor.",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "50% Scroll",
|
||||
description: "A user scrolled 50% of the current page",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
attributeClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "userId",
|
||||
description: "The internal ID of the person",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
description: "The email of the person",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "development",
|
||||
eventClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "New Session",
|
||||
description: "Gets fired when a new session is created",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "Exit Intent (Desktop)",
|
||||
description: "A user on Desktop leaves the website with the cursor.",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "50% Scroll",
|
||||
description: "A user scrolled 50% of the current page",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
attributeClasses: {
|
||||
create: [
|
||||
{
|
||||
name: "userId",
|
||||
description: "The internal ID of the person",
|
||||
type: "automatic",
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
description: "The email of the person",
|
||||
type: "automatic",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
memberships: true,
|
||||
products: {
|
||||
include: {
|
||||
environments: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const teamId = newTeam?.id;
|
||||
|
||||
if (teamId) {
|
||||
fetch(`${WEBAPP_URL}/api/v1/teams/${teamId}/add_demo_product`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-api-key": INTERNAL_SECRET,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return newTeam;
|
||||
}
|
||||
|
||||
export async function duplicateSurveyAction(environmentId: string, surveyId: string) {
|
||||
const existingSurvey = await getSurvey(surveyId);
|
||||
|
||||
if (!existingSurvey) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
|
||||
// create new survey with the data of the existing survey
|
||||
const newSurvey = await prisma.survey.create({
|
||||
data: {
|
||||
...existingSurvey,
|
||||
id: undefined, // id is auto-generated
|
||||
environmentId: undefined, // environmentId is set below
|
||||
name: `${existingSurvey.name} (copy)`,
|
||||
status: "draft",
|
||||
questions: JSON.parse(JSON.stringify(existingSurvey.questions)),
|
||||
thankYouCard: JSON.parse(JSON.stringify(existingSurvey.thankYouCard)),
|
||||
triggers: {
|
||||
create: existingSurvey.triggers.map((trigger) => ({
|
||||
eventClassId: trigger.id,
|
||||
})),
|
||||
},
|
||||
attributeFilters: {
|
||||
create: existingSurvey.attributeFilters.map((attributeFilter) => ({
|
||||
attributeClassId: attributeFilter.attributeClassId,
|
||||
condition: attributeFilter.condition,
|
||||
value: attributeFilter.value,
|
||||
})),
|
||||
},
|
||||
environment: {
|
||||
connect: {
|
||||
id: environmentId,
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage
|
||||
? JSON.parse(JSON.stringify(existingSurvey.surveyClosedMessage))
|
||||
: prismaClient.JsonNull,
|
||||
},
|
||||
});
|
||||
return newSurvey;
|
||||
}
|
||||
|
||||
export async function copyToOtherEnvironmentAction(
|
||||
environmentId: string,
|
||||
surveyId: string,
|
||||
targetEnvironmentId: string
|
||||
) {
|
||||
const existingSurvey = await prisma.survey.findFirst({
|
||||
where: {
|
||||
id: surveyId,
|
||||
environmentId,
|
||||
},
|
||||
include: {
|
||||
triggers: {
|
||||
include: {
|
||||
eventClass: true,
|
||||
},
|
||||
},
|
||||
attributeFilters: {
|
||||
include: {
|
||||
attributeClass: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingSurvey) {
|
||||
throw new ResourceNotFoundError("Survey", surveyId);
|
||||
}
|
||||
|
||||
let targetEnvironmentTriggers: string[] = [];
|
||||
// map the local triggers to the target environment
|
||||
for (const trigger of existingSurvey.triggers) {
|
||||
const targetEnvironmentTrigger = await prisma.eventClass.findFirst({
|
||||
where: {
|
||||
name: trigger.eventClass.name,
|
||||
environment: {
|
||||
id: targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!targetEnvironmentTrigger) {
|
||||
// if the trigger does not exist in the target environment, create it
|
||||
const newTrigger = await prisma.eventClass.create({
|
||||
data: {
|
||||
name: trigger.eventClass.name,
|
||||
environment: {
|
||||
connect: {
|
||||
id: targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
description: trigger.eventClass.description,
|
||||
type: trigger.eventClass.type,
|
||||
noCodeConfig: trigger.eventClass.noCodeConfig
|
||||
? JSON.parse(JSON.stringify(trigger.eventClass.noCodeConfig))
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
targetEnvironmentTriggers.push(newTrigger.id);
|
||||
} else {
|
||||
targetEnvironmentTriggers.push(targetEnvironmentTrigger.id);
|
||||
}
|
||||
}
|
||||
|
||||
let targetEnvironmentAttributeFilters: string[] = [];
|
||||
// map the local attributeFilters to the target env
|
||||
for (const attributeFilter of existingSurvey.attributeFilters) {
|
||||
// check if attributeClass exists in target env.
|
||||
// if not, create it
|
||||
const targetEnvironmentAttributeClass = await prisma.attributeClass.findFirst({
|
||||
where: {
|
||||
name: attributeFilter.attributeClass.name,
|
||||
environment: {
|
||||
id: targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!targetEnvironmentAttributeClass) {
|
||||
const newAttributeClass = await prisma.attributeClass.create({
|
||||
data: {
|
||||
name: attributeFilter.attributeClass.name,
|
||||
description: attributeFilter.attributeClass.description,
|
||||
type: attributeFilter.attributeClass.type,
|
||||
environment: {
|
||||
connect: {
|
||||
id: targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
targetEnvironmentAttributeFilters.push(newAttributeClass.id);
|
||||
} else {
|
||||
targetEnvironmentAttributeFilters.push(targetEnvironmentAttributeClass.id);
|
||||
}
|
||||
}
|
||||
|
||||
// create new survey with the data of the existing survey
|
||||
const newSurvey = await prisma.survey.create({
|
||||
data: {
|
||||
...existingSurvey,
|
||||
id: undefined, // id is auto-generated
|
||||
environmentId: undefined, // environmentId is set below
|
||||
name: `${existingSurvey.name} (copy)`,
|
||||
status: "draft",
|
||||
questions: JSON.parse(JSON.stringify(existingSurvey.questions)),
|
||||
thankYouCard: JSON.parse(JSON.stringify(existingSurvey.thankYouCard)),
|
||||
triggers: {
|
||||
create: targetEnvironmentTriggers.map((eventClassId) => ({
|
||||
eventClassId: eventClassId,
|
||||
})),
|
||||
},
|
||||
attributeFilters: {
|
||||
create: existingSurvey.attributeFilters.map((attributeFilter, idx) => ({
|
||||
attributeClassId: targetEnvironmentAttributeFilters[idx],
|
||||
condition: attributeFilter.condition,
|
||||
value: attributeFilter.value,
|
||||
})),
|
||||
},
|
||||
environment: {
|
||||
connect: {
|
||||
id: targetEnvironmentId,
|
||||
},
|
||||
},
|
||||
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
|
||||
},
|
||||
});
|
||||
return newSurvey;
|
||||
}
|
||||
|
||||
export const deleteSurveyAction = async (surveyId: string) => {
|
||||
await deleteSurvey(surveyId);
|
||||
};
|
||||
@@ -1,9 +1,10 @@
|
||||
import JsLogo from "@/images/jslogo.png";
|
||||
import WebhookLogo from "@/images/webhook.png";
|
||||
import ZapierLogo from "@/images/zapier-small.png";
|
||||
import { Card } from "@formbricks/ui";
|
||||
import Image from "next/image";
|
||||
import JsLogo from "@/images/jslogo.png";
|
||||
import ZapierLogo from "@/images/zapier-small.png";
|
||||
|
||||
export default function IntegrationsPage() {
|
||||
export default function IntegrationsPage({ params }) {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="my-2 text-3xl font-bold text-slate-800">Integrations</h1>
|
||||
@@ -11,17 +12,34 @@ export default function IntegrationsPage() {
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
<Card
|
||||
docsHref="https://formbricks.com/docs/getting-started/nextjs-app"
|
||||
docsText="Docs"
|
||||
docsNewTab={true}
|
||||
label="Javascript Widget"
|
||||
description="Integrate Formbricks into your Webapp"
|
||||
icon={<Image src={JsLogo} alt="Javascript Logo" />}
|
||||
/>
|
||||
<Card
|
||||
docsHref="https://formbricks.com/docs/integrations/zapier"
|
||||
docsText="Docs"
|
||||
docsNewTab={true}
|
||||
connectHref="https://zapier.com/apps/formbricks/integrations"
|
||||
connectText="Connect"
|
||||
connectNewTab={true}
|
||||
label="Zapier"
|
||||
description="Integrate Formbricks with 5000+ apps via Zapier"
|
||||
icon={<Image src={ZapierLogo} alt="Zapier Logo" />}
|
||||
/>
|
||||
<Card
|
||||
connectHref={`/environments/${params.environmentId}/integrations/webhooks`}
|
||||
connectText="Manage Webhooks"
|
||||
connectNewTab={false}
|
||||
docsHref="https://formbricks.com/docs/webhook-api/overview"
|
||||
docsText="Docs"
|
||||
docsNewTab={true}
|
||||
label="Webhooks"
|
||||
description="Trigger Webhooks based on actions in your surveys"
|
||||
icon={<Image src={WebhookLogo} alt="Webhook Logo" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,231 @@
|
||||
import SurveyCheckboxGroup from "@/app/(app)/environments/[environmentId]/integrations/webhooks/SurveyCheckboxGroup";
|
||||
import TriggerCheckboxGroup from "@/app/(app)/environments/[environmentId]/integrations/webhooks/TriggerCheckboxGroup";
|
||||
import { triggers } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/HardcodedTriggers";
|
||||
import { testEndpoint } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/testEndpoint";
|
||||
import Modal from "@/components/shared/Modal";
|
||||
import { createWebhook } from "@formbricks/lib/services/webhook";
|
||||
import { TPipelineTrigger } from "@formbricks/types/v1/pipelines";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { TWebhookInput } from "@formbricks/types/v1/webhooks";
|
||||
import { Button, Input, Label } from "@formbricks/ui";
|
||||
import clsx from "clsx";
|
||||
import { Webhook } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
interface AddWebhookModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
surveys: TSurvey[];
|
||||
setOpen: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export default function AddWebhookModal({ environmentId, surveys, open, setOpen }: AddWebhookModalProps) {
|
||||
const router = useRouter();
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
register,
|
||||
formState: { isSubmitting },
|
||||
} = useForm();
|
||||
|
||||
const [testEndpointInput, setTestEndpointInput] = useState("");
|
||||
const [hittingEndpoint, setHittingEndpoint] = useState<boolean>(false);
|
||||
const [endpointAccessible, setEndpointAccessible] = useState<boolean>();
|
||||
const [selectedTriggers, setSelectedTriggers] = useState<TPipelineTrigger[]>([]);
|
||||
const [selectedSurveys, setSelectedSurveys] = useState<string[]>([]);
|
||||
const [selectedAllSurveys, setSelectedAllSurveys] = useState(false);
|
||||
const [creatingWebhook, setCreatingWebhook] = useState(false);
|
||||
|
||||
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
|
||||
try {
|
||||
setHittingEndpoint(true);
|
||||
await testEndpoint(testEndpointInput);
|
||||
setHittingEndpoint(false);
|
||||
if (sendSuccessToast) toast.success("Yay! We are able to ping the webhook!");
|
||||
setEndpointAccessible(true);
|
||||
return true;
|
||||
} catch (err) {
|
||||
setHittingEndpoint(false);
|
||||
toast.error("Oh no! We are unable to ping the webhook!");
|
||||
setEndpointAccessible(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAllSurveys = () => {
|
||||
setSelectedAllSurveys(!selectedAllSurveys);
|
||||
setSelectedSurveys([]);
|
||||
};
|
||||
|
||||
const handleSelectedSurveyChange = (surveyId: string) => {
|
||||
setSelectedSurveys((prevSelectedSurveys: string[]) =>
|
||||
prevSelectedSurveys.includes(surveyId)
|
||||
? prevSelectedSurveys.filter((id) => id !== surveyId)
|
||||
: [...prevSelectedSurveys, surveyId]
|
||||
);
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (selectedValue: TPipelineTrigger) => {
|
||||
setSelectedTriggers((prevValues) =>
|
||||
prevValues.includes(selectedValue)
|
||||
? prevValues.filter((value) => value !== selectedValue)
|
||||
: [...prevValues, selectedValue]
|
||||
);
|
||||
};
|
||||
|
||||
const submitWebhook = async (data: TWebhookInput): Promise<void> => {
|
||||
if (!isSubmitting) {
|
||||
try {
|
||||
setCreatingWebhook(true);
|
||||
if (!testEndpointInput || testEndpointInput === "") {
|
||||
throw new Error("Please enter a URL");
|
||||
}
|
||||
if (selectedTriggers.length === 0) {
|
||||
throw new Error("Please select at least one trigger");
|
||||
}
|
||||
|
||||
if (!selectedAllSurveys && selectedSurveys.length === 0) {
|
||||
throw new Error("Please select at least one survey");
|
||||
}
|
||||
|
||||
const endpointHitSuccessfully = await handleTestEndpoint(false);
|
||||
if (!endpointHitSuccessfully) return;
|
||||
|
||||
const updatedData: TWebhookInput = {
|
||||
name: data.name,
|
||||
url: testEndpointInput,
|
||||
triggers: selectedTriggers,
|
||||
surveyIds: selectedSurveys,
|
||||
};
|
||||
|
||||
await createWebhook(environmentId, updatedData);
|
||||
router.refresh();
|
||||
setOpenWithStates(false);
|
||||
toast.success("Webhook added successfully.");
|
||||
} catch (e) {
|
||||
toast.error(e.message);
|
||||
} finally {
|
||||
setCreatingWebhook(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const setOpenWithStates = (isOpen: boolean) => {
|
||||
setOpen(isOpen);
|
||||
reset();
|
||||
setTestEndpointInput("");
|
||||
setEndpointAccessible(undefined);
|
||||
setSelectedSurveys([]);
|
||||
setSelectedTriggers([]);
|
||||
setSelectedAllSurveys(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal open={open} setOpen={setOpenWithStates} noPadding closeOnOutsideClick={false}>
|
||||
<div className="flex h-full flex-col rounded-lg">
|
||||
<div className="rounded-t-lg bg-slate-100">
|
||||
<div className="flex w-full items-center justify-between p-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="mr-1.5 h-6 w-6 text-slate-500">
|
||||
<Webhook />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xl font-medium text-slate-700">Add Webhook</div>
|
||||
<div className="text-sm text-slate-500">Send survey response data to a custom endpoint</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit(submitWebhook)}>
|
||||
<div className="flex justify-between rounded-lg p-6">
|
||||
<div className="w-full space-y-4">
|
||||
<div className="col-span-1">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<div className="mt-1 flex">
|
||||
<Input
|
||||
type="text"
|
||||
id="name"
|
||||
{...register("name")}
|
||||
placeholder="Optional: Label your webhook for easy identification"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-1">
|
||||
<Label htmlFor="URL">URL</Label>
|
||||
<div className="mt-1 flex">
|
||||
<Input
|
||||
type="url"
|
||||
id="URL"
|
||||
value={testEndpointInput}
|
||||
onChange={(e) => {
|
||||
setTestEndpointInput(e.target.value);
|
||||
}}
|
||||
className={clsx(
|
||||
endpointAccessible === true
|
||||
? "border-green-500 bg-green-50"
|
||||
: endpointAccessible === false
|
||||
? "border-red-200 bg-red-50"
|
||||
: endpointAccessible === undefined
|
||||
? "border-slate-200 bg-white"
|
||||
: null
|
||||
)}
|
||||
placeholder="Paste the URL you want the event to trigger on"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
loading={hittingEndpoint}
|
||||
className="ml-2 whitespace-nowrap"
|
||||
onClick={() => {
|
||||
handleTestEndpoint(true);
|
||||
}}>
|
||||
Test Endpoint
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="Triggers">Triggers</Label>
|
||||
<TriggerCheckboxGroup
|
||||
triggers={triggers}
|
||||
selectedTriggers={selectedTriggers}
|
||||
onCheckboxChange={handleCheckboxChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="Surveys">Surveys</Label>
|
||||
<SurveyCheckboxGroup
|
||||
surveys={surveys}
|
||||
selectedSurveys={selectedSurveys}
|
||||
selectedAllSurveys={selectedAllSurveys}
|
||||
onSelectAllSurveys={handleSelectAllSurveys}
|
||||
onSelectedSurveyChange={handleSelectedSurveyChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end border-t border-slate-200 p-6">
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="minimal"
|
||||
onClick={() => {
|
||||
setOpenWithStates(false);
|
||||
}}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="darkCTA" type="submit" loading={creatingWebhook}>
|
||||
Add Webhook
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { TPipelineTrigger } from "@formbricks/types/v1/pipelines";
|
||||
|
||||
export const triggers = [
|
||||
{ title: "Response Created", value: "responseCreated" as TPipelineTrigger },
|
||||
{ title: "Response Updated", value: "responseUpdated" as TPipelineTrigger },
|
||||
{ title: "Response Finished", value: "responseFinished" as TPipelineTrigger },
|
||||
];
|
||||
@@ -0,0 +1,63 @@
|
||||
import React from "react";
|
||||
import { Checkbox } from "@formbricks/ui";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
|
||||
interface SurveyCheckboxGroupProps {
|
||||
surveys: TSurvey[];
|
||||
selectedSurveys: string[];
|
||||
selectedAllSurveys: boolean;
|
||||
onSelectAllSurveys: () => void;
|
||||
onSelectedSurveyChange: (surveyId: string) => void;
|
||||
}
|
||||
|
||||
export const SurveyCheckboxGroup: React.FC<SurveyCheckboxGroupProps> = ({
|
||||
surveys,
|
||||
selectedSurveys,
|
||||
selectedAllSurveys,
|
||||
onSelectAllSurveys,
|
||||
onSelectedSurveyChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
<div className="my-1 flex items-center space-x-2">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id="allSurveys"
|
||||
className="bg-white"
|
||||
value=""
|
||||
checked={selectedAllSurveys}
|
||||
onCheckedChange={onSelectAllSurveys}
|
||||
/>
|
||||
<label
|
||||
htmlFor="allSurveys"
|
||||
className={`flex cursor-pointer items-center ${selectedAllSurveys ? "font-semibold" : ""}`}>
|
||||
All current and new surveys
|
||||
</label>
|
||||
</div>
|
||||
{surveys.map((survey) => (
|
||||
<div key={survey.id} className="my-1 flex items-center space-x-2">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={survey.id}
|
||||
value={survey.id}
|
||||
className="bg-white"
|
||||
checked={selectedSurveys.includes(survey.id) && !selectedAllSurveys}
|
||||
disabled={selectedAllSurveys}
|
||||
onCheckedChange={() => onSelectedSurveyChange(survey.id)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={survey.id}
|
||||
className={`flex cursor-pointer items-center ${
|
||||
selectedAllSurveys ? "cursor-not-allowed opacity-50" : ""
|
||||
}`}>
|
||||
{survey.name}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SurveyCheckboxGroup;
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
import { Checkbox } from "@formbricks/ui";
|
||||
import { TPipelineTrigger } from "@formbricks/types/v1/pipelines";
|
||||
|
||||
interface TriggerCheckboxGroupProps {
|
||||
triggers: { title: string; value: TPipelineTrigger }[];
|
||||
selectedTriggers: TPipelineTrigger[];
|
||||
onCheckboxChange: (selectedValue: TPipelineTrigger) => void;
|
||||
}
|
||||
|
||||
export const TriggerCheckboxGroup: React.FC<TriggerCheckboxGroupProps> = ({
|
||||
triggers,
|
||||
selectedTriggers,
|
||||
onCheckboxChange,
|
||||
}) => {
|
||||
return (
|
||||
<div className="mt-1 rounded-lg border border-slate-200">
|
||||
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
|
||||
{triggers.map((trigger) => (
|
||||
<div key={trigger.value} className="my-1 flex items-center space-x-2">
|
||||
<label htmlFor={trigger.value} className="flex cursor-pointer items-center">
|
||||
<Checkbox
|
||||
type="button"
|
||||
id={trigger.value}
|
||||
value={trigger.value}
|
||||
className="bg-white"
|
||||
checked={selectedTriggers.includes(trigger.value)}
|
||||
onCheckedChange={() => {
|
||||
onCheckboxChange(trigger.value);
|
||||
}}
|
||||
/>
|
||||
<span className="ml-2">{trigger.title}</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TriggerCheckboxGroup;
|
||||
@@ -0,0 +1,47 @@
|
||||
import ModalWithTabs from "@/components/shared/ModalWithTabs";
|
||||
import { TWebhook } from "@formbricks/types/v1/webhooks";
|
||||
import WebhookOverviewTab from "@/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookOverviewTab";
|
||||
import WebhookSettingsTab from "@/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookSettingsTab";
|
||||
import { TSurvey } from "@formbricks/types/v1/surveys";
|
||||
import { Webhook } from "lucide-react";
|
||||
|
||||
interface WebhookModalProps {
|
||||
environmentId: string;
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
webhook: TWebhook;
|
||||
surveys: TSurvey[];
|
||||
}
|
||||
|
||||
export default function WebhookModal({ environmentId, open, setOpen, webhook, surveys }: WebhookModalProps) {
|
||||
const tabs = [
|
||||
{
|
||||
title: "Overview",
|
||||
children: <WebhookOverviewTab webhook={webhook} surveys={surveys} />,
|
||||
},
|
||||
{
|
||||
title: "Settings",
|
||||
children: (
|
||||
<WebhookSettingsTab
|
||||
environmentId={environmentId}
|
||||
webhook={webhook}
|
||||
surveys={surveys}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalWithTabs
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
tabs={tabs}
|
||||
icon={<Webhook />}
|
||||
label={webhook.name ? webhook.name : webhook.url}
|
||||
description={""}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||