Compare commits

..

87 Commits

Author SHA1 Message Date
github-actions[bot]
73904e11a6 Update formbricks-js to 1.0.2 (#640)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-02 16:12:39 +02:00
Matti Nannt
a1b447caad Increase formbricks-js z-index to 999999 to increase compatibility with more websites (#639)
* Fix formbricks-js modal covered by other elements

* Fix wrong usage of prefix

* add changeset
2023-08-02 15:59:16 +02:00
Johannes
6b989487b2 Fix SEMRush SEO issues & move OSS friends to static
Fix SEMRush SEO issues & move OSS friends to static
2023-08-02 07:40:45 -05:00
Johannes
d60e0c4e5c fix SEO issues and move OSS friends to static 2023-08-02 14:24:26 +02:00
Shubham Palriwala
2a3ab3280f Fix NEXTAUTH_SECRET not get filled correctly in deployment script (#632)
* feat: handle openssl producing special characters that were causing errrs for sed to read

* feat: use all variables in dockerfile from the sole env itself
2023-08-02 13:33:29 +02:00
Matti Nannt
5b217e5483 Update pnpm-lock to solve build issues (#636) 2023-08-02 13:20:03 +02:00
tyjkerr
ec0d3f2fa2 Add Back Button to Surveys (#501)
* add back button, next with local storaage wip

* handle submission and skip submission logic

* handle showing stored value on same concurrent question type.

* remove console.log

* fix next button not showing, add saving answer on pressing back to local storage

* add temp props to QuestionCondition in preview modal

* add temp props to QuestionCondition in preview modal again...

* update navigation logic

* update survey question preview

* add back-button component

* add back button to formbricks/js

* refactor localStorage functions to lib

* remove unused import

* add form prefilling when reloading forms

* merge main into branch

* Revert "merge main into branch"

This reverts commit 13bc9c06ec.

* rename localStorage key answers->responses

* rename answers -> responses in linkSurvey lib

* when survey page reloaded jump to next question instead of current question

* rename getStoredAnswer -> getStoredResponse

* continue renaming

* continue renaming

* rename answerValue -> responseValue

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-08-02 13:08:20 +02:00
Johannes
ae702ddd06 Add Twenty.com to OSS friends
Add Twenty.com to OSS friends
2023-08-02 04:34:19 -05:00
Johannes
91f78d875b Add Twenty.com to OSS friends 2023-08-02 04:34:00 -05:00
Johannes
08110b0c34 LP: Add OSS friends via API and update pricing wording
LP: Add OSS friends via API and update pricing wording
2023-08-02 03:59:31 -05:00
Johannes
42e6601f13 update fetch URL 2023-08-02 10:47:26 +02:00
Johannes
a5c33981a0 update pricing wording, add OSS friends API 2023-08-02 10:29:06 +02:00
Johannes
1a90d1b7e8 Merge branch 'main' of github.com:formbricks/formbricks into lp/add-oss-friends 2023-08-02 09:57:03 +02:00
Johannes
3905c2227e Patch: Close survey on date can be set to past
Patch: Close survey on date can be set to past
2023-08-01 03:33:37 -05:00
github-actions[bot]
fc0feda5e9 Release formbricks-js 1.0.1 (#625)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-07-31 16:44:56 +02:00
Matti Nannt
3d0d633bc8 Fix new Session event not triggered every time a new session is created (#624)
* Fix new Session event not triggered every time a new session is created

* make syncWithBackend method private
2023-07-31 16:40:21 +02:00
Matti Nannt
d707e2e49e Remove ports from docker compose file in production script (#623) 2023-07-31 15:33:02 +02:00
Matti Nannt
288fc79366 Fix Authentication issues for Webhook API (#622) 2023-07-31 14:10:28 +02:00
Matti Nannt
000fcf8b02 Update npm dependencies (#621) 2023-07-31 12:49:05 +02:00
Matti Nannt
730f0ba1e9 Fix session creation endpoint throw unexpected error (#620)
* fix env variables in weekly summary action

* Fix session creation endpoint throw unexpected error
2023-07-31 11:59:21 +02:00
Matti Nannt
38d3de2165 Add sentry error reporting (#619) 2023-07-31 10:26:59 +02:00
Matti Nannt
5d380a4986 Fix smileys in rating questions not showing in Safari Browser (#618) 2023-07-30 21:23:02 +02:00
Matti Nannt
e32e47e272 Fix: Disable autoFocus when embedded with iframe (#615) 2023-07-28 21:08:45 +02:00
Moritz Rengert
e864829a79 Fix: Logic Jumps stop working when options get renamed (#540)
* update logic values if multi select options change

* run pnpm format

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-07-28 20:21:26 +02:00
Dhruwang Jariwala
b62a344e54 Update Webhook Documentation (#611)
* added webhook payload docs

* ran pnpm format

* update data.id explanation, reformat

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-07-28 10:41:21 +02:00
Shubham Palriwala
469590c2f6 Add Quickstart for self-hosting using automated Shell Script (#613)
* feat: poc

* merge: individual docker-compose files

* fix: nits

* feat: postgres now only accessible internally

* feat: emoji time

* cleanup: my commented commands

* better emoji and warning on domain name

* fix: better handling of docker check

* feat: follow principle of least privilege and remove excess sudo in commands

* feat: read machine name dynamically

* feat: documentation for prod script

* feat: remove custom networks in the docker compose

* cleanup: comments in script

* update emojis to fix spacing

* attempt: new groyp

* attemp: move new group command at end for the ability to parse vars

* feat: it all works without sudo yay

* feat: cleanup docs as suggested

* documentation: self hosting for prod script

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-07-26 16:12:33 +02:00
Matthias Nannt
5ae7f31d01 update pnpm lock 2023-07-25 16:14:00 +02:00
Matthias Nannt
cb4cd706ad Merge branch 'main' of github.com:formbricks/formbricks into feat/close-date-edge-case 2023-07-25 16:00:05 +02:00
Dhruwang Jariwala
a165143c2a Add Input Validation to the Survey Editor (#588)
* added validation to survey edit

* made refactors

* extracted validation rules into a single object and resolved draggable issue

* ran pnpm format

* fixed a validation bug

* fixed similar  validation bug in other component

* implemented default and specific validation

* made some code refactors

* handled case where a question is deleted

* instead of storing questionIdx now we are storing question id in invalidQuestions array && ran pnpm format

* removed unused comment

* run pnpm format

* made requested changes

* removed unused export

* add types to validation.ts

* run pnpm format

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-07-25 11:27:09 +02:00
Meet Patel
6b3f977d83 Store current Url in the responses meta data (#566)
* url add to link-survey

* fixed

* fixed

* fixed

* fixed

* ran pnpm format

* make url optional in response input to not break existing integrations

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-07-24 17:05:54 +02:00
Salim B
f743fb18fb Fix Formatting in Docker Readme (#608)
Fix formatting
2023-07-24 10:57:14 +02:00
Johannes
2f8257ae62 added new members 2023-07-22 13:41:15 +02:00
Johannes
8a5217b39c OSS Api 2023-07-22 13:26:33 +02:00
Piyush Gupta
57e6c86e6a refactor: summary header 2023-07-22 11:02:59 +05:30
Piyush Gupta
4519cb8a2d Merge branch 'main' of https://github.com/gupta-piyush19/formbricks into feat/close-date-edge-case 2023-07-21 20:38:22 +05:30
Moritz Rengert
e5d06de68e Fix multiple NPS questions in one survey not working (#596)
* clear nps question on submit

* fix onboarding color picker

* update onChange to onClick for NPS Questions

* update border color

* remove console.log

---------

Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-07-21 17:00:29 +02:00
Matti Nannt
49f61e2eeb Fix pnpm go bug and format code (#605) 2023-07-21 16:58:48 +02:00
Yash Gupta
4763cf3217 Enhance Github Issue Templates for better usability (#594)
* feat: adds enhanced bug report github page and removes md version of it

* feat: adds enhanced feature request .yml github page and removes .md version of it

* refactor: makes the Desktop field as md editor on Bug Report Github Page

* refactor: converts the Environment field as checkbox on Bug Report Github Page
2023-07-21 16:48:44 +02:00
Matti Nannt
b6a0d0fe5d Add OSS friends api endpoint (#604) 2023-07-21 16:44:41 +02:00
Matti Nannt
9be053d8a6 Fix: Add revalidation to surveys overview page (#601) 2023-07-21 15:19:29 +02:00
Matti Nannt
3a17c6b085 Fix errors in pnpm go script dependencies (#600) 2023-07-21 15:12:12 +02:00
Johannes
16b7e0d82a Improve Weekly Summary Email (#584)
* fix bugs in weekly

* fix CTA

* update bg color of completed badge
2023-07-20 16:33:01 +02:00
Johannes
ae7075c746 Fix 404 when Email exists in GitHub Login
Fix 404 when Email exists in GitHub Login
2023-07-20 09:11:55 -05:00
Johannes
78ed48adeb fix error messages 2023-07-20 15:59:21 +02:00
Matti Nannt
03ba9ecb58 fix build error (#592) 2023-07-20 15:10:18 +02:00
Matti Nannt
d360c1f741 Fix legacy sdk endpoints not working with new services (#591) 2023-07-20 14:44:58 +02:00
Johannes
cf953db18f Refactor Survey Overview Page
Refactor Survey Overview Page
2023-07-20 06:35:36 -05:00
Johannes
7475df147d Move Stripe to a new account (#589) 2023-07-20 11:03:32 +02:00
Matti Nannt
06d620dbc8 Update docs for nextjs setup with app directory (#590) 2023-07-20 10:45:06 +02:00
Matthias Nannt
feadefa90d clean up and move to new action -> service structure 2023-07-19 19:00:04 +02:00
Johannes
754832f097 Add Author Box to Blog articles
Add Author Box to Blog articles
2023-07-19 07:01:08 -05:00
Johannes
dad8ebe8da update blog articles 2023-07-19 13:44:01 +02:00
Johannes
df27b4703f add author box 2023-07-19 13:36:53 +02:00
Matthias Nannt
e385638c14 merge latest changes from main 2023-07-19 13:35:03 +02:00
Johannes
8cacb2ccee Landing Page: Balance cal.com widget with Concierge Div, Update Concierge Offer
Landing Page: Balance cal.com widget with Concierge Div, Update Concierge Offer
2023-07-19 05:56:50 -05:00
Johannes
98a62949d5 update concierge offer 2023-07-19 12:52:43 +02:00
Matti Nannt
dd6ac2e4cd Fix build errors and update contact to Formbricks GmbH (#585) 2023-07-19 12:50:19 +02:00
Matti Nannt
c52df00d39 Fix Email Notifications (#583)
* Fix email notifications not working properly

* Fix response notification not working

* fix response meta schema

* fix typo in docs

* improve error message in webhooks
2023-07-19 12:30:31 +02:00
Ankur Datta
0c7c3c9ad2 [RM] getProductWithEnvironments 2023-07-19 10:10:49 +00:00
Ankur Datta
5612fbfa22 [RM] getSurveysWithResponseCount 2023-07-19 09:41:06 +00:00
Ankur Datta
186a4269a3 [CHANGE] make survey-list as server component 2023-07-19 07:29:18 +00:00
Shubham Palriwala
503e7649e2 Fix formbricks-js unit tests not working properly (#582) 2023-07-19 08:46:39 +02:00
ShubhamPalriwala
e3b4ec4a9e fix: cal widget had an internal predefined margin causing it to be unabalanced with the concierge div 2023-07-19 00:59:52 +05:30
Piyush Gupta
b20cda2d06 fix: removed today's date from closeOnDate date picker 2023-07-19 00:11:01 +05:30
Piyush Gupta
6e8be0c0bd fixed merge conflict 2023-07-18 19:40:01 +05:30
Piyush Gupta
c68a9c8d15 fix: edge case of close on date 2023-07-18 19:38:03 +05:30
Johannes
e23f26f48c Improve Responsiveness of LP
Improve Responsiveness of LP
2023-07-18 07:04:26 -05:00
Johannes
9e9fe5c09d update logos 2023-07-18 13:57:17 +02:00
ShubhamPalriwala
3bb4a34e4c fix: hero component padding for smaller screen & github div conditional horizontal margin 2023-07-18 17:15:18 +05:30
Matti Nannt
9fcc0a360c Fix Posthog environment variables not recognized in Frontend (#578)
* move posthog variable to client in t3env

* remove groups, add revalidation to action/attributes pages
2023-07-18 12:53:40 +02:00
Shubham Palriwala
3824d95151 Move Actions & Attributes pages over to server components (#495)
* feat: server rendering of event actions summary page & server actions

* chore: renaming event to action and minor refactoring

* fix: logging message

* delete: unnecessary file

* feat: migrate attributes overview page

* feat: impl grouped page & layout, logically differentiate attributes and actions

* pnpm format

* fix: logical addressing of dirs and minot bugs

* move: actionsAndAttributes navbar to dedicated dir from components

* fix: use server-only build-time checks and move actionsAttributes navbar

* revert: unnecessary docker compose changes

* resolve merge conflicts dynamically

* fix: address feedback comments

* use sparkles icon from heroicons

* fix updated action not updating in table

* remove async from client function due to warning

* move router.refresh in AddNoActionModal

* small rename

* feat: replace swr w server action in ActionSettingsTab

* replace custom error with ResourceNotFoundError error class

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-07-18 12:40:03 +02:00
Matti Nannt
5bfaad9484 Improve Folder Structure for App-wide Client-imports (#577)
* move posthog to new implementation

* update folder structure
2023-07-18 11:32:58 +02:00
Matti Nannt
a5c8c1aa85 Update formbricks-js version in html docs (#576) 2023-07-18 09:12:33 +02:00
Johannes
c92011b069 Improve UI for delete account functionality (#575) 2023-07-17 19:25:48 +02:00
Anshuman Pandey
856247763c Improve Tags UI/UX (#563)
* fix: fixes tag merging logic

* fix: moves tag merge logic into transactions

* update tags settings icon

---------

Co-authored-by: Johannes <johannes@formbricks.com>
2023-07-17 19:24:08 +02:00
Matti Nannt
2199a4b102 Improve pnpm go command pipeline & remove database tsup setup (#574)
* move docker compose dev

* update all packages; pnpm format

* remove tsup from prisma

* add zod to prisma files

* delete tsup config

* improve pnpm go script, downgrade prettier

* update pnpm lock
2023-07-17 18:12:40 +02:00
Matti Nannt
cd49d687ad fix weekly summary cron workflow (#568) 2023-07-17 09:30:03 +02:00
Johannes
2852bf617a Fix ordering dropdown + Other input field
Fix ordering dropdown + Other input field
2023-07-16 09:04:22 -05:00
Johannes
30623d5fd2 fix order dropdown, other field and UX tweaks 2023-07-16 15:51:46 +02:00
Matthias Nannt
c6a4b7731f merge latest changes, resolve conflict & build error 2023-07-14 11:05:00 +02:00
Ankur Datta
2ea3d42ff6 [RM] logs 2023-07-14 07:43:24 +00:00
Ankur Datta
e275553425 [ADD] loading-state, [HANDLE] imports 2023-07-14 07:24:43 +00:00
Ankur Datta
701a7d9786 [REFACTOR] survey-overview-page 2023-07-13 17:54:21 +00:00
Ankur Datta
4410b14d0c [ADD] services 2023-07-13 17:53:48 +00:00
Ankur Datta
cddbbc2be2 [ADD] server-actions 2023-07-13 17:53:09 +00:00
Ankur Datta
4fefa09ee8 [ADD] product-with-environment-type 2023-07-13 17:52:55 +00:00
Ankur Datta
09106188ba [FIX] zod-type 2023-07-13 17:52:18 +00:00
338 changed files with 6095 additions and 3559 deletions

View File

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

View File

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

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

View File

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

View File

@@ -1,4 +1,4 @@
Copyright (c) 2023 Matthias Nannt, Johannes Dancker
Copyright (c) 2023 Formbricks GmbH
Portions of this software are licensed as follows:

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -1,5 +1,4 @@
import clsx from "clsx";
import { Icon } from "@/components/shared/Icon";
const styles = {

View File

@@ -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">
&copy; 2022. All rights reserved.
Formbricks GmbH &copy; 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>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

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

View File

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

View 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:
"BoxyHQs 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.`);
}
}

View File

@@ -6,6 +6,7 @@ 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)",
@@ -14,6 +15,8 @@ export const meta = {
date: "2023-04-12",
};
<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 form and survey tools are still alive and kicking in 2023._
<Image

View File

@@ -5,6 +5,7 @@ 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 👀",
@@ -13,6 +14,8 @@ export const meta = {
date: "2023-04-13",
};
<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

View File

@@ -2,6 +2,7 @@ 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 💃",
@@ -10,6 +11,8 @@ export const meta = {
date: "2023-04-13",
};
<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

View File

@@ -4,6 +4,7 @@ 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.",
@@ -11,6 +12,8 @@ export const meta = {
date: "2022-08-26",
};
<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?_

View File

@@ -9,6 +9,7 @@ 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",
@@ -17,6 +18,8 @@ export const meta = {
date: "2023-03-24",
};
<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?_

View File

@@ -2,6 +2,7 @@ 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 🎉",
@@ -9,6 +10,8 @@ export const meta = {
date: "2022-11-07",
};
<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._

View File

@@ -1,6 +1,7 @@
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";
@@ -17,6 +18,8 @@ export const meta = {
date: "2023-07-14",
};
<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:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 KiB

View File

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

View File

@@ -6,6 +6,7 @@ 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",
@@ -14,6 +15,8 @@ export const meta = {
date: "2022-06-03",
};
<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._

View File

@@ -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&apos;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={{

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ export const meta = {
},
]}
example={`{
"personId: "clfqjny0v000ayzgsycx54a2c",
"personId": "clfqjny0v000ayzgsycx54a2c",
"surveyId": "clfqz1esd0000yzah51trddn8",
"finished": true,
"data": {

View File

@@ -45,18 +45,18 @@ import formbricks from "@formbricks/js";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
if (typeof window !== "undefined") {
formbricks.init({
environmentId: "clj66eqzu00m5qu0g8leglrns",
apiHost: "https://app.formbricks.com", // e.g. https://app.formbricks.com
debug: true, // remove when in production
});
}
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]);

View File

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

View File

@@ -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
## (Production: Ubuntu) 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!
## (Most users: Local Setup) 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**
@@ -89,6 +121,12 @@ 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
@@ -164,4 +202,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>;

View File

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

View File

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

View File

@@ -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:
"BoxyHQs 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,
},
};
}

View File

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

View File

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

@@ -34,3 +34,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# Sentry Auth Token
.sentryclirc

15
apps/web/CHANGELOG.md Normal file
View File

@@ -0,0 +1,15 @@
# @formbricks/web
## 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,13 +24,7 @@ export default function AttributeDetailModal({
},
{
title: "Settings",
children: (
<AttributeSettingsTab
attributeClass={attributeClass}
environmentId={environmentId}
setOpen={setOpen}
/>
),
children: <AttributeSettingsTab attributeClass={attributeClass} setOpen={setOpen} />,
},
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -117,9 +117,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 +166,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,7 +221,7 @@ 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&apos;re in development mode. Use it to test surveys, events and attributes.
You&apos;re in development mode. Use it to test surveys, actions and attributes.
</div>
)}

View File

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

View File

@@ -1,9 +1,12 @@
"use server";
import { prisma } from "@formbricks/database";
import { Team } from "@prisma/client";
import { ResourceNotFoundError } from "@formbricks/errors";
import { deleteSurvey, getSurvey } from "@formbricks/lib/services/survey";
import { QuestionType } from "@formbricks/types/questions";
import { createId } from "@paralleldrive/cuid2";
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({
@@ -1372,3 +1375,176 @@ export async function addDemoData(teamId: string): Promise<void> {
InterviewPromptResults.displays
);
}
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);
};

View File

@@ -1,12 +1,10 @@
import EnvironmentsNavbar from "@/app/environments/[environmentId]/EnvironmentsNavbar";
import EnvironmentsNavbar from "@/app/(app)/environments/[environmentId]/EnvironmentsNavbar";
import ToasterClient from "@/components/ToasterClient";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import PosthogIdentify from "./PosthogIdentify";
import FormbricksClient from "../../FormbricksClient";
import { PosthogClientWrapper } from "../../PosthogClientWrapper";
import { ResponseFilterProvider } from "@/app/environments/[environmentId]/ResponseFilterContext";
import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]/ResponseFilterContext";
import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper";
export default async function EnvironmentLayout({ children, params }) {
@@ -22,16 +20,13 @@ export default async function EnvironmentLayout({ children, params }) {
return (
<>
<ResponseFilterProvider>
<PosthogIdentify session={session} />
<FormbricksClient session={session} />
<ToasterClient />
<EnvironmentsNavbar environmentId={params.environmentId} session={session} />
<PosthogClientWrapper>
<main className="h-full flex-1 overflow-y-auto bg-slate-50">
{children}
<main />
</main>
</PosthogClientWrapper>
<main className="h-full flex-1 overflow-y-auto bg-slate-50">
{children}
<main />
</main>
</ResponseFilterProvider>
</>
);

View File

@@ -7,7 +7,6 @@ export default function SettingsCard({
children,
soon = false,
noPadding = false,
dangerZone,
beta,
}: {
title: string;
@@ -15,16 +14,13 @@ export default function SettingsCard({
children: any;
soon?: boolean;
noPadding?: boolean;
dangerZone?: boolean;
beta?: boolean;
}) {
return (
<div className="my-4 w-full bg-white shadow sm:rounded-lg">
<div className="rounded-t-lg border-b border-slate-200 bg-slate-100 px-6 py-5">
<div className="flex">
<h3 className={`${dangerZone ? "text-red-600" : "text-slate-900"} "text-lg font-medium leading-6 `}>
{title}
</h3>
<h3 className="text-lg font-medium leading-6 text-slate-900">{title}</h3>
<div className="ml-2">
{beta && <Badge text="Beta" size="normal" type="warning" />}
{soon && <Badge text="coming soon" size="normal" type="success" />}

View File

@@ -7,10 +7,11 @@ import type { Session } from "next-auth";
import { useRouter } from "next/navigation";
import { useState } from "react";
// upated on 20th of July 2023
const stripeURl =
process.env.NODE_ENV === "production"
? "https://buy.stripe.com/28o00R4GDf9qdfa5kp"
: "https://buy.stripe.com/test_9AQfZw5CL9hmcXSdQQ";
? "https://buy.stripe.com/5kA9ABal07ZjgEw3cc"
: "https://buy.stripe.com/test_8wMaHA3UWcACfuM3cc";
interface PricingTableProps {
environmentId: string;

View File

@@ -0,0 +1,14 @@
import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions";
import { getServerSession } from "next-auth";
import SettingsTitle from "../SettingsTitle";
import PricingTable from "./PricingTable";
export default async function ProfileSettingsPage({ params }) {
const session = await getServerSession(authOptions);
return (
<div>
<SettingsTitle title="Billing & Plan" />
<PricingTable environmentId={params.environmentId} session={session} />
</div>
);
}

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