Compare commits

...

210 Commits

Author SHA1 Message Date
github-actions[bot]
315467ef3f Version Packages (#682)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-12 12:43:50 +02:00
Matti Nannt
3dde021cd0 Prepare release of Formbricks v1.0.2 (#681) 2023-08-12 12:39:18 +02:00
Matti Nannt
ebbde2b531 Fix WEBAPP_URL cannot be set with prebuilt Docker image (#680)
* Fix WEBAPP_URL cannot be set with prebuilt Docker image

* Extend default docker-compose file and production script

* Update docs
2023-08-12 11:53:23 +02:00
Dhruwang Jariwala
47a8fd6b62 Rewrite Api Key Settings to React Server Components (#654)
* moved apikey settings to server component

* rename ZApiKeyData to ZApiKeyCreateInput

* Make smaller improvements

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-08-11 17:23:59 +02:00
Shubham Palriwala
c6686209be Move Look & Feel Settings to React Server Components (#672)
* feat: migrate look and feel to serverside component with loading screen

* fix: use existing product type instead of creating a custom type

* fix: make improvements as Matti suggested

* change attributes order in updateProduct function

* run pnpm format

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-08-11 17:04:31 +02:00
Shubham Palriwala
09436c78fc Validate for E-Mail Address on Verification Page (#666)
* validate: for email in the user verification modal

* feat: email auth is now a server page, uses server-side zod lib for email input validation, removes validator lib

* Add FormWrapper to Error Message

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-08-11 16:36:26 +02:00
Dhruwang Jariwala
98cdf941e6 Improve Preview in Survey Editor with Mobile & Desktop View (#573)
* made modal component responsive

* added tab switch

* added mobile preview mode for surveys

* did some refactors

* did some refactors

* added type defs

* ran pnpm format

* removed an unused comment

* fixed variable name typo

* fixed UI bugs and added mobile mockup to link surveys

* restored changes from fix long description PR

* fixed scroll to top issue and toggle hide bug

* fixed minor animation bug

* fixed placement issue

* re-embed restart button, make phone preview more responsive

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-08-11 10:55:49 +02:00
Johannes
52a09aa3ae Introduce Restart Survey Button on Preview of Survey
Introduce Restart Survey Button on Preview of Survey
2023-08-11 09:13:02 +02:00
Johannes
bf028a5f64 Merge branch 'main' of github.com:formbricks/formbricks into shubham/for-1120-add-restart-button-to-survey-preview-ui-tweak 2023-08-11 09:12:15 +02:00
Johannes
5c60694117 Merge branch 'main' of github.com:formbricks/formbricks into shubham/for-1120-add-restart-button-to-survey-preview-ui-tweak 2023-08-11 08:52:52 +02:00
Matti Nannt
179f92077b Apply prettier formatting (#678) 2023-08-10 17:25:40 +02:00
Matti Nannt
9cdf446f65 Remove lodash dependency from formbricks-js (#677)
* Remove lodash dependency from formbricks-js

* add array utils file
2023-08-10 16:22:10 +02:00
ShubhamPalriwala
f98d4f5c11 fix: make button exactly like mock 2023-08-10 17:21:44 +05:30
Matti Nannt
142c1bd35b Fix saved changes are not visible in Survey Editor (#674) 2023-08-10 13:14:17 +02:00
ShubhamPalriwala
5e3ec7e4f0 feat: Restart Survey when Previewing a Survey & truncate logic values 2023-08-10 15:09:17 +05:30
Johannes
3bbb4170e2 Set Response Limit to 50 when an In-App Survey is Created and Fix Doc Feedback survey
Set Response Limit to 50 when an In-App Survey is Created and Fix Doc Feedback survey
2023-08-09 18:14:34 +02:00
Johannes
dc085c41c0 Merge branch 'main' of github.com:formbricks/formbricks into shubham/for-1119-tweak-set-default-for-in-app-surveys-to-limit-to-50 2023-08-09 18:12:38 +02:00
Matti Nannt
33b3887b84 Use .env.docker in advanced docker-compose setup to simplify file structure (#671) 2023-08-09 17:57:35 +02:00
Johannes
6a8805de0b Merge branch 'main' of github.com:formbricks/formbricks into shubham/for-1119-tweak-set-default-for-in-app-surveys-to-limit-to-50 2023-08-09 17:52:29 +02:00
Johannes
10e149bb02 Fix formbricks-js labels not showing correctly with custom label styles, add dark mode to demo app
Fix formbricks-js labels not showing correctly with custom label styles, add dark mode to demo app
2023-08-09 17:51:20 +02:00
Johannes
9f944249fc update formatting 2023-08-09 17:50:40 +02:00
Johannes
b5765fed74 add dark mode to widget 2023-08-09 17:48:20 +02:00
Johannes
eee9b29723 Merge branch 'main' of github.com:formbricks/formbricks into feature/FOR-1081 2023-08-09 17:02:51 +02:00
Johannes
a3aae4ab95 fix: fixes survey link share modal and adds a mobile nav menu
fix: fixes survey link share modal and adds a mobile nav menu
2023-08-09 16:12:07 +02:00
Johannes
5b9db8f353 Merge branch 'main' of github.com:formbricks/formbricks into fix/safari-mobile 2023-08-09 16:08:52 +02:00
Matti Nannt
9b98ca4f64 Fix thank you screen is disabled after completing a survey (#670) 2023-08-09 16:05:15 +02:00
Johannes
6572d5395b Merge branch 'main' of github.com:formbricks/formbricks into fix/safari-mobile 2023-08-09 15:57:21 +02:00
Johannes
cd753f1a67 Merge branch 'main' of github.com:formbricks/formbricks into shubham/for-1119-tweak-set-default-for-in-app-surveys-to-limit-to-50 2023-08-09 15:37:08 +02:00
Johannes
6f0a26904f Fix back button in link surveys with logic jumps
Fix back button in link surveys with logic jumps
2023-08-09 15:35:32 +02:00
Johannes
e1c8a715d1 Add UI to create webhooks on integrations page
Add UI to create webhooks on integrations page
2023-08-09 15:31:14 +02:00
Johannes
96e54dbb46 double check migration 2023-08-09 15:25:55 +02:00
Johannes
b71fdf3205 Merge branch 'main' of github.com:formbricks/formbricks into shubham/for-1035-add-webhooks-ui-on-integrations-page 2023-08-09 15:22:11 +02:00
ShubhamPalriwala
5520edb2c5 feat: set response limit to 50 if the created survey is in-app 2023-08-09 18:43:50 +05:30
Johannes
d824da610d Fix: layout issue on peoples page
Fix: layout issue on peoples page
2023-08-09 14:40:35 +02:00
Dhruwang Jariwala
2bebc9598c Rewrite profile settings page to server component (#642)
* Chore: moved profile settings to server component

* ran pnpm format

* fisxed a build issue

* made requested changes

* made some refactors

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-08-09 13:13:58 +02:00
Dhruwang Jariwala
580e51dcea Fix Logic Jumps issues in in-product survey (#667) 2023-08-09 12:10:07 +02:00
Matthias Nannt
b570f3c79d fix pnpm lock file 2023-08-09 09:36:16 +02:00
Matthias Nannt
cf94c1a6d1 Make next & back button follow logic jumps 2023-08-09 09:27:12 +02:00
Johannes
71832c590f ui/ux tweaks 2023-08-08 22:12:40 +02:00
ShubhamPalriwala
15050525fd fix: make components out of survey and trigger checklists and minor ux factoring 2023-08-08 21:21:15 +05:30
ShubhamPalriwala
dcc198b151 cleanup: addWebhookModal component 2023-08-08 20:29:28 +05:30
ShubhamPalriwala
3793f29d0a fix: correct typo in htmlFor label 2023-08-08 20:06:21 +05:30
ShubhamPalriwala
b6da482e3f feat: webhooks now have a name across the UI 2023-08-08 20:01:48 +05:30
ShubhamPalriwala
cd1d9196fc feat: add name in webhook model db in prisme 2023-08-08 19:41:52 +05:30
ShubhamPalriwala
e3c09ebec3 fix: loader for add webhook, test nedpoint check on webhook creation, survey check, updated webhook logo 2023-08-08 19:22:01 +05:30
Dhruwang
2bda12d4fc Fix: layout issue on peoples page 2023-08-08 17:56:39 +05:30
Matti Nannt
b072d3b549 Update pnpm lock file (#664) 2023-08-08 13:38:22 +02:00
Johannes
758fc9af4d tweaks 2023-08-08 13:29:34 +02:00
ShubhamPalriwala
d4a4b4ec41 fix: dropdown replaced with checkbox list and loader is implemented 2023-08-08 15:55:48 +05:30
Johannes
5aa38a6e39 Delete Team, Transfer Ownership and Leave Team functionality
Delete Team, Transfer Ownership and Leave Team functionality
2023-08-08 10:51:44 +02:00
Johannes
0ebef13805 fix build error 2023-08-08 10:46:41 +02:00
Johannes
a81ceff09e update formatting 2023-08-08 10:33:26 +02:00
Piyush Gupta
7ebdf9939e fix: added loading states in CTA 2023-08-08 10:16:48 +05:30
ShubhamPalriwala
7631783e7d fix: all webhook icons now use lucid 2023-08-08 09:46:08 +05:30
ShubhamPalriwala
c92b2b00e0 fix: webhook table wrapping & add webhook modal text and dropdown id 2023-08-08 09:38:15 +05:30
ShubhamPalriwala
e68a7fe763 fix: add webhook logo and card component flexibility 2023-08-08 09:21:03 +05:30
Piyush Gupta
a3b46ee532 Merge branch 'main' of https://github.com/formbricks/formbricks into feature/delete-team 2023-08-08 00:59:37 +05:30
Matti Nannt
a1a66ef6be Fix close-on-date pipeline not executed properly (#662)
* Fix github action closeOnDate pipeline

* remove prebuild turbo build
2023-08-07 19:54:23 +02:00
Matti Nannt
6a1b8106b7 Add prebuild as a build dependency (#661) 2023-08-07 18:15:28 +02:00
Matti Nannt
8b1a074e2c Update npm packages to latest minor version (#660) 2023-08-07 17:46:58 +02:00
Matti Nannt
57733a75fc Fix build error (#659) 2023-08-07 17:13:50 +02:00
Matti Nannt
e5ef71ae87 Fix product service throwing validation error (#658) 2023-08-07 17:02:20 +02:00
Matti Nannt
89dae8f1d8 Simplify highlightBorderColor in product type (#657) 2023-08-07 16:50:24 +02:00
Anshuman Pandey
370041b0ae A highlight border can now be added to the in-product modal using the product settings (#610)
* feat: added logic for adding highlight border

* feat: adds highlight border color to js widget

* fix: fixes class issue

* fix: removes log

* fix: fixes db fields

* fix: fixes border color edit

* fix: fixes highlight border styles in demo app and preview

* fix migrations

* remove console.log

* fix build issues

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-08-07 16:14:55 +02:00
Johannes
34ff14d43b Fix: In app survey preview container size too high
Fix: In app survey preview container size too high
2023-08-07 16:00:25 +02:00
Pradumn Kumar
b6c0dbf5d3 Close survey after x responses now needs to be set to a higher number than the number of current responses (#606)
* fix: fixes close survey on x response issue

* feat: updates

* chore: don't update _count

* chore: optimizations

* fix: fixes issue with not being able to enter a lower value at all

* update toast message

* add response count to toast

* only count completed responses

---------

Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-08-07 15:22:37 +02:00
Johannes
488e2801f0 Make template library searchable
Make template library searchable
2023-08-07 14:21:43 +02:00
Johannes
ae7d0a4846 Add input field validation to rename product, team and name
Add input field validation to rename product, team and name
2023-08-07 14:17:59 +02:00
Dhruwang
7d3fa70fe2 Fix: broken preview 2023-08-07 17:09:29 +05:30
ShubhamPalriwala
bb4052690e feat: link integrations page to webhook 2023-08-07 16:29:54 +05:30
ShubhamPalriwala
e8a286bd4e feat: webhooks UI 2023-08-07 16:10:34 +05:30
Piyush Gupta
205593d8d3 added loading in transfer ownership CTA 2023-08-06 15:46:11 +05:30
Piyush Gupta
37afd004af refactor: used isOwner in delete team 2023-08-06 15:30:54 +05:30
Piyush Gupta
ad86c4dbf4 fix: independent queries -> Txn 2023-08-06 15:17:08 +05:30
Piyush Gupta
fb64eb50a2 Merge branch 'main' of https://github.com/formbricks/formbricks into feature/delete-team 2023-08-06 14:58:59 +05:30
Piyush Gupta
8d7eeb045b feat: added transfer ownership 2023-08-06 14:58:52 +05:30
Shubham Palriwala
fdb1aa2299 Rewrite Person Detail Page to Server Components (#609)
* feat: migration /[personId] page to server side

* feat: decouple components in person page

* fix: ZDisplaysWithSurveyName now extends the ZDisplay type

* feat: drop custom service and use existing service for survey and response

* run pnpm format

* shift data fetching to component level but still server side

* rename event to action

* move special person services to activity service

* remove activityFeedItem type in ActivityFeed

* simplify TResponseWithSurvey

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
2023-08-06 09:53:37 +02:00
Shubham Khunt
df9ff011f7 fix:removed toast 2023-08-06 11:51:31 +05:30
Shubham Khunt
e85d95a4eb fix:removed toast 2023-08-06 11:51:01 +05:30
Johannes
5c9605f4af Feature : Toggle Multi-Select and Single-Select Question Types
Feature : Toggle Multi-Select and Single-Select Question Types
2023-08-05 16:23:39 +02:00
Johannes
de3d580614 Merge branch 'main' of github.com:formbricks/formbricks into toggle-multi-and-single-select 2023-08-05 15:29:39 +02:00
Piyush Gupta
ccb89548f0 feat: added leave team 2023-08-05 17:49:06 +05:30
Anshuman Pandey
ad42f4cc55 Dramatically improve load times when creating a new team(#614)
* fix: attempts to reduce time taken to create team

* fix: fixes long time taken in team creation

* fix: refactors prisma logic

* feat: added logic for adding demo data while signing up

* fix: adds comment

* fix: adds another logic for adding demo data

* fix: adds service for adding demo data

* fix: fixes

* fix: adds demo product creation logic in next auth options

* fix: fixes next auth options

* fix: fixes team creation logic

* refactor: clean up

* fix: moves the logic for adding demo product while creating team in bg

* fix: moves individual queries in a transaction

* refactor: service

* fix: moves api route to app-dir

* fix: fixes api calls

* fix: fixes cache

* fix: removes unused code
2023-08-05 13:59:06 +02:00
Dhruwang
51dda67992 fixed invalid storedResponseValue issue 2023-08-05 15:08:34 +05:30
Dhruwang
0598ad2eaa Feat:Toggle Multi-Select and Single-Select Question Types 2023-08-05 12:39:48 +05:30
Meet Patel
44e48e3c3f Hide the clear button for input type search 2023-08-05 10:56:50 +05:30
Piyush Gupta
1551baeca7 merged with main 2023-08-05 09:33:58 +05:30
Piyush Gupta
c3f26f7ab8 feat: added delete team functionality 2023-08-05 09:32:23 +05:30
Meet Patel
e9e3de2ce8 Added 'type=search' and 'name=search' attributes to SearchBox in TemplateContainer 2023-08-04 22:22:42 +05:30
Meet Patel
34c4e9bc1a lucide search icon 2023-08-04 22:10:39 +05:30
Johannes
c707896eb6 Fix Weekly: Remove N/A completion rate, exclude "completed" survey if has no submission in last 7 days
Fix Weekly: Remove N/A completion rate, exclude "completed" survey if has no submission in last 7 days
2023-08-04 04:19:40 -05:00
Johannes
ee545b7ade remove N/A CR, exclude completed if has no submission 2023-08-04 09:51:49 +02:00
Johannes
7bf0fa450a Add structured data to blog articles, tweaked the SEO score of existing ones
Add structured data to blog articles, tweaked the SEO score of existing ones
2023-08-04 02:19:13 -05:00
Johannes
1a8618692a more metadata for articles 2023-08-03 20:28:38 +02:00
Matti Nannt
e5f371476c Fix formatting of preview components (#646)
* Fix formatting of preview components

* update npm packages
2023-08-03 17:03:53 +02:00
Shubham Palriwala
dba3677633 fix: documentation on create webhook had invalid body key and headers were not being showed as required (#643) 2023-08-03 13:48:31 +02:00
Anshuman Pandey
369c9ed7b2 fix: fixes survey link share modal and moble nav menu 2023-08-03 17:13:51 +05:30
Piyush Gupta
0776138c1c Merge branch 'main' of https://github.com/formbricks/formbricks into feature/delete-team 2023-08-02 23:40:30 +05:30
Matthias Nannt
235c1afe28 Fix formbricks-js labels not showing correctly with custom label styles 2023-08-02 16:59:57 +02:00
Meet Patel
17b9d686bd category button text & gap 2023-08-02 20:04:18 +05:30
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
Meet Patel
c423e43aee refined search for who completed onboarding 2023-08-02 19:39:38 +05:30
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
Meet Patel
712431e842 search box improved & default category set 2023-08-01 23:32:00 +05:30
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
Meet Patel
86da5ff2f4 search improved & bg white & category disabled on search 2023-07-31 23:03:37 +05:30
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
Meet Patel
eed9b6635d two word case handled 2023-07-31 15:06:12 +05:30
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
Meet Patel
892a58c45e Merge branch 'main' into search-template-library 2023-07-30 23:00:33 +05:30
Meet Patel
4fb9851a6d searchbox component 2023-07-30 22:49:48 +05:30
Meet Patel
09c37d78a2 filter logic moved out side return 2023-07-29 11:21:56 +05:30
Meet Patel
6335565bf9 search works 2023-07-29 11:20:14 +05:30
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
Piyush Gupta
1e816eb6d9 feat: added delete card and modal 2023-07-23 01:01:51 +05:30
Shubham Khunt
8c31c71251 fix: removed error toast 2023-07-22 19:27:53 +05:30
Shubham Khunt
62a08f304b padding issue fixed in delete dialog 2023-07-22 19:25:15 +05:30
Shubham Khunt
35057322a4 padding issue fixed in alert dialog 2023-07-22 19:25:14 +05:30
Shubham Khunt
b1a93de8db fix: toast error message updated 2023-07-22 19:25:14 +05:30
Shubham Khunt
aca32655cd fix: edit profile page input validation added 2023-07-22 19:25:14 +05:30
Shubham Khunt
8b14559d5f fix: Edit product page input validation completed 2023-07-22 19:25:14 +05:30
Shubham Khunt
43a623a61e fix: edit team page input validation added 2023-07-22 19:25:14 +05:30
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
Johannes
b673044890 Add Blog for v1 Release - How we got here
Add Blog for v1 Release - How we got here
2023-07-14 06:05:44 -05:00
Shubham Palriwala
bf9189c1af fix: docker compose url for curl (#557) 2023-07-14 13:00:57 +02:00
Moritz Rengert
e262006f7e Add Demo-Data to newly created teams (#521)
* create people and survey on team create

* create people and responses / displays

* drafting GPT input

* add attributeClass, eventClass and people

* fix link in person detail page

* fix email instead of uid

* added two surveys

* add events, 3 more surveys

---------

Co-authored-by: Johannes <johannes@formbricks.com>
2023-07-14 12:55:41 +02:00
Johannes
d44ea1f32d add v1 blog 2023-07-14 12:51:44 +02:00
Matti Nannt
e24f6cd017 Improve documentation (#552)
* update nextjs app docs

* remove prisma extendedWhereUnique from schema

* change button titles in pricing table

* fix smaller bugs
2023-07-14 12:48:57 +02:00
Matthias Nannt
c6a4b7731f merge latest changes, resolve conflict & build error 2023-07-14 11:05:00 +02:00
Johannes
62d2c1af18 improve filter ux, update summary header (#555) 2023-07-14 10:37:42 +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
Nitesh Seram
1a83373099 Fix random 0 can appear in survey menubar (#553)
Co-authored-by: Seram Nitesh Singh <nitesh.s@auzmor.com>
2023-07-14 09:08:15 +02: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
447 changed files with 12891 additions and 5205 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

@@ -1,4 +1,4 @@
name: Cron - weeklySummary
name: Cron - closeOnDate
on:
# "Scheduled workflows run on the latest commit on the default or base branch."
@@ -10,14 +10,14 @@ jobs:
cron-weeklySummary:
env:
APP_URL: ${{ secrets.APP_URL }}
CRON_API_KEY: ${{ secrets.CRON_SECRET }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
runs-on: ubuntu-latest
steps:
- name: cURL request
if: ${{ env.APP_URL && env.CRON_SECRET }}
run: |
curl ${{ secrets.APP_URL }}/api/cron/close_surveys \
curl ${{ env.APP_URL }}/api/cron/close_surveys \
-X POST \
-H 'content-type: application/json' \
-H 'authorization: ${{ secrets.CRON_SECRET }}' \
-H 'x-api-key: ${{ env.CRON_SECRET }}' \
--fail

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

@@ -1,53 +1,76 @@
import fbsetup from "../../public/fb-setup.png";
import formbricks from "@formbricks/js";
import Image from "next/image";
import { useEffect, useState } from "react";
import fbsetup from "../../public/fb-setup.png";
export default function AppPage({}) {
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
if (darkMode) {
document.body.classList.add("dark");
} else {
document.body.classList.remove("dark");
}
}, [darkMode]);
return (
<div className="px-12 py-6">
<div>
<h1 className="text-2xl font-bold">Formbricks In-product Survey Demo App</h1>
<p className="text-slate-700">
This app helps you test your in-app surveys. You can create an test user actions, create and update
user attributes, etc.
</p>
<div className="h-screen bg-white px-12 py-6 dark:bg-slate-800">
<div className="flex justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">
Formbricks In-product Survey Demo App
</h1>
<p className="text-slate-700 dark:text-slate-300">
This app helps you test your in-app surveys. You can create and test user actions, create and
update user attributes, etc.
</p>
</div>
<button
className="rounded-lg bg-slate-200 px-6 py-1 dark:bg-slate-700 dark:text-slate-100"
onClick={() => setDarkMode(!darkMode)}>
Toggle Dark Mode
</button>
</div>
<div className="my-4 grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<div className="rounded-lg border border-slate-300 bg-slate-100 p-6">
<h3 className="text-lg font-semibold">Setup .env</h3>
<p className="text-slate-700">
<div className="rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">Setup .env</h3>
<p className="text-slate-700 dark:text-slate-300">
Copy the environment ID of your Formbricks app to the env variable in demo/.env
</p>
<Image src={fbsetup} alt="fb setup" className="mt-4 rounded" priority />
</div>
<div className="mt-4 rounded-lg border border-slate-300 bg-slate-100 p-6">
<h3 className="text-lg font-semibold">Widget Logs</h3>
<p className="text-slate-700">
Look at the logs to understand how the widget works. <strong>Open your browser console</strong>{" "}
to see the logs.
<div className="mt-4 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">Widget Logs</h3>
<p className="text-slate-700 dark:text-slate-300">
Look at the logs to understand how the widget works.{" "}
<strong className="dark:text-white">Open your browser console</strong> to see the logs.
</p>
{/* <div className="max-h-[40vh] overflow-y-auto py-4">
{/* <div className="max-h-[40vh] overflow-y-auto py-4">
<LogsContainer />
</div> */}
</div>
</div>
<div className="md:grid md:grid-cols-3">
<div className="col-span-3 rounded-lg border border-slate-300 bg-slate-100 p-6">
<h3 className="text-lg font-semibold">Reset person / pull data from Formbricks app</h3>
<p className="text-slate-700">
<div className="col-span-3 rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-gray-600 dark:bg-gray-800">
<h3 className="text-lg font-semibold dark:text-white">
Reset person / pull data from Formbricks app
</h3>
<p className="text-slate-700 dark:text-gray-300">
On formbricks.logout() a few things happen: <strong>New person is created</strong> and{" "}
<strong>surveys & no-code actions are pulled from Formbricks:</strong>.
</p>
<button
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700"
className="my-4 rounded-lg bg-slate-500 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
onClick={() => {
formbricks.logout();
}}>
Logout
</button>
<p className="text-xs text-slate-700">
<p className="text-xs text-slate-700 dark:text-gray-300">
If you made a change in Formbricks app and it does not seem to work, hit &apos;Logout&apos; and
try again.
</p>
@@ -56,7 +79,7 @@ export default function AppPage({}) {
<div className="p-6">
<div>
<button
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700"
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600"
onClick={() => {
formbricks.track("Code Action");
}}>
@@ -64,7 +87,7 @@ export default function AppPage({}) {
</button>
</div>
<div>
<p className="text-xs text-slate-700">
<p className="text-xs text-slate-700 dark:text-gray-300">
This button sends a{" "}
<a href="https://formbricks.com/docs/actions/code" className="underline" target="_blank">
Code Action
@@ -75,18 +98,24 @@ export default function AppPage({}) {
</div>
<div className="p-6">
<div>
<button className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700">
<button className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
No-Code Action
</button>
</div>
<div>
<p className="text-xs text-slate-700">
<p className="text-xs text-slate-700 dark:text-gray-300">
This button sends a{" "}
<a href="https://formbricks.com/docs/actions/no-code" className="underline" target="_blank">
<a
href="https://formbricks.com/docs/actions/no-code"
className="underline dark:text-blue-500"
target="_blank">
No Code Action
</a>{" "}
as long as you created it beforehand in the Formbricks App.{" "}
<a href="https://formbricks.com/docs/actions/no-code" target="_blank" className="underline">
<a
href="https://formbricks.com/docs/actions/no-code"
target="_blank"
className="underline dark:text-blue-500">
Here are instructions on how to do it.
</a>
</p>
@@ -98,17 +127,17 @@ export default function AppPage({}) {
onClick={() => {
formbricks.setAttribute("Plan", "Free");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700">
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
Set Plan to &apos;Free&apos;
</button>
</div>
<div>
<p className="text-xs text-slate-700">
<p className="text-xs text-slate-700 dark:text-gray-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/attributes/custom-attributes"
target="_blank"
className="underline">
className="underline dark:text-blue-500">
attribute
</a>{" "}
&apos;Plan&apos; to &apos;Free&apos;. If the attribute does not exist, it creates it.
@@ -121,17 +150,17 @@ export default function AppPage({}) {
onClick={() => {
formbricks.setAttribute("Plan", "Paid");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700">
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
Set Plan to &apos;Paid&apos;
</button>
</div>
<div>
<p className="text-xs text-slate-700">
<p className="text-xs text-slate-700 dark:text-gray-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/attributes/custom-attributes"
target="_blank"
className="underline">
className="underline dark:text-blue-500">
attribute
</a>{" "}
&apos;Plan&apos; to &apos;Paid&apos;. If the attribute does not exist, it creates it.
@@ -144,17 +173,17 @@ export default function AppPage({}) {
onClick={() => {
formbricks.setEmail("test@web.com");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700">
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
Set Email
</button>
</div>
<div>
<p className="text-xs text-slate-700">
<p className="text-xs text-slate-700 dark:text-gray-300">
This button sets the{" "}
<a
href="https://formbricks.com/docs/attributes/identify-users"
target="_blank"
className="underline">
className="underline dark:text-blue-500">
user email
</a>{" "}
&apos;test@web.com&apos;
@@ -167,17 +196,17 @@ export default function AppPage({}) {
onClick={() => {
formbricks.setUserId("THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING");
}}
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700">
className="mb-4 rounded-lg bg-slate-800 px-6 py-3 text-white hover:bg-slate-700 dark:bg-gray-700 dark:hover:bg-gray-600">
Set User ID
</button>
</div>
<div>
<p className="text-xs text-slate-700">
<p className="text-xs text-slate-700 dark:text-gray-300">
This button sets an external{" "}
<a
href="https://formbricks.com/docs/attributes/identify-users"
target="_blank"
className="underline">
className="underline dark:text-blue-500">
user ID
</a>{" "}
to &apos;THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING&apos;

View File

@@ -5,6 +5,7 @@ module.exports = {
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
darkMode: "class",
theme: {
extend: {},
},

View File

@@ -1,8 +1,7 @@
import { Button, Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui";
import { useRouter } from "next/router";
import { useState } from "react";
import { handleFeedbackSubmit, updateFeedback } from "../../lib/handleFeedbackSubmit";
import { Popover, PopoverTrigger, PopoverContent } from "@formbricks/ui";
import { Button } from "@formbricks/ui";
import { useRouter } from "next/router";
export const DocsFeedback: React.FC = () => {
const router = useRouter();
@@ -26,7 +25,7 @@ export const DocsFeedback: React.FC = () => {
Is everything on this page clear?
<Popover open={isOpen} onOpenChange={setIsOpen}>
<div className="mt-2 inline-flex space-x-3 md:ml-4 md:mt-0">
{["Yes 👍", " No 👎"].map((option) => (
{["Yes 👍", "No 👎"].map((option) => (
<PopoverTrigger
key={option}
className="rounded border border-slate-200 bg-slate-50 px-4 py-2 text-slate-900 hover:bg-slate-100 hover:text-slate-600 focus:outline-none focus:ring-2 focus:ring-neutral-900 focus:ring-offset-1 dark:border-slate-700 dark:bg-slate-700 dark:text-slate-300 dark:hover:bg-slate-600 dark:hover:text-slate-300"

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

@@ -10,6 +10,7 @@ interface APICallProps {
label: string;
type: string;
description: string;
required?: boolean;
}[];
bodies: {
label: string;
@@ -69,7 +70,13 @@ export function APILayout({ method, url, description, headers, bodies, responses
<p className="not-prose -mb-1 pt-2 font-bold">Headers</p>
<div>
{headers.map((q) => (
<Parameter key={q.label} label={q.label} type={q.type} description={q.description} />
<Parameter
key={q.label}
label={q.label}
type={q.type}
description={q.description}
required={q.required}
/>
))}
</div>
</div>

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,6 +26,10 @@ interface Props {
meta: {
title: string;
description: string;
publishedTime: string;
authors: string[];
section: string;
tags: string[];
};
children: JSX.Element;
}
@@ -34,7 +38,14 @@ export default function LayoutMdx({ meta, children }: Props) {
useExternalLinks(".prose a");
return (
<div className="flex h-screen flex-col justify-between">
<MetaInformation title={meta.title} description={meta.description} />
<MetaInformation
title={meta.title}
description={meta.description}
publishedTime={meta.publishedTime}
authors={meta.authors}
section={meta.section}
tags={meta.tags}
/>
<Header />
<main className="min-w-0 max-w-2xl flex-auto px-4 lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16">
<article className="mx-auto my-16 max-w-3xl px-2">

View File

@@ -3,10 +3,21 @@ import Head from "next/head";
interface Props {
title: string;
description: string;
publishedTime?: string;
authors?: string[];
section?: string;
tags?: string[];
}
export default function MetaInformation({ title, description }: Props) {
const pageTitle = `${title} | Open-Source Survey Software`;
export default function MetaInformation({
title,
description,
publishedTime,
authors,
section,
tags,
}: Props) {
const pageTitle = `${title} | Open-Source Experience Management, Privacy-first`;
return (
<Head>
<title>{pageTitle}</title>
@@ -14,13 +25,22 @@ export default function MetaInformation({ title, description }: Props) {
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={`https://${process.env.VERCEL_URL}/social-image.png`} />
<meta property="og:image:alt" content="Formbricks - Open Source Form and Survey Infrastructure" />
<meta property="og:image:alt" content="Formbricks - Open Source Experience Management, Privacy-first" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="en_US" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Open Source Forms and Surveys by Formbricks" />
<meta property="og:site_name" content="Open Source Experience Management, Privacy-first" />
<meta property="article:publisher" content="Formbricks" />
{publishedTime && <meta property="article:published_time" content={publishedTime} />}
{authors && <meta property="article:author" content={authors.join(", ")} />}
{section && <meta property="article:section" content={section} />}
{tags && <meta property="article:tag" content={tags.join(", ")} />}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@formbricks" />
<meta name="twitter:creator" content="@formbricks" />
<meta name="theme-color" content="#00C4B8" />
</Head>
);
}

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

@@ -91,12 +91,16 @@ const nextConfig = {
destination: "/docs/actions/code",
permanent: true,
},
{
source: "/pmf",
destination: "/",
permanent: true,
},
{
source: "/blog/v1-and-how-we-got-here",
destination: "/blog/experience-management-open-source",
permanent: true,
},
];
},
};

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,35 +6,42 @@ import LimeSurvey from "./free-survey-tool-limesurvey-open-source-software-opens
import OpnForm from "./opnform-free-open-source-form-survey-tools-builder-2023-self-hostign.jpg";
import HeaderImage from "./2023-title-best-open-source-survey-software-tools-and-alternatives.png";
import SurveyJS from "./surveyjs-free-opensource-form-survey-tool-software-to-make-surveys-2023.png";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "Best Open-source Form & Survey Tools (still maintained in 2023)",
title: "5 Open Source Survey and Form Tools maintained in 2023",
description:
"Most open-source projects get abandoned after a while. But these 5 open-source form and survey tools are still alive and kicking in 2023.",
"Most open source projects get abandoned after a while. But these 5 open source survey tools are still alive and kicking in 2023.",
date: "2023-04-12",
publishedTime: "2023-04-12T12:00:00",
authors: ["Johannes"],
section: "Open Source Surveys",
tags: ["Open Source Surveys", "Formbricks", "Typeform", "SurveyJS", "Typebot", "OpnForm", "LimeSurvey"],
};
_Most open-source projects get abandoned after a while. But these 5 open-source form and survey tools are still alive and kicking in 2023._
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
_Most open source projects get abandoned after a while. But these 5 open source survey tools are still alive and kicking in 2023._
<Image
src={HeaderImage}
alt="Free and self-hostable: Find the 5 best (and maintained) open-source survey tools 2023."
alt="Open source survey tool self-hostable: Find the 5 best (and maintained) open source survey tool 2023."
className="rounded-lg"
/>
Looking for the perfect open-source form and survey tool to help you gather valuable insights and improve your business? Look no further!
Looking for the perfect open source survey tool to help you gather valuable insights and improve your business? Look no further!
We've compiled a list of the top 5 open-source form and survey tools that are still maintained in 2023. In-product surveys, conversational bots, AI-generated surveys: These tools offer various features that cater to different needs.
We've compiled a list of the top 5 open source form and survey tools that are still maintained in 2023. In app surveys, conversational bots, AI-generated surveys: These open source tools offer various features that cater to different needs.
## 1. Formbricks - In-product micro-surveys
## 1. Formbricks - In app micro surveys
<Image
src={Formbricks}
alt="Formbricks is a free and open-source survey software for in-product micro-surveys. Ask any segment at any point in time."
alt="Formbricks is a free and open source survey software for in app micro surveys. Ask any user segment at any point in the user journey."
className="rounded-lg"
/>
Formbricks is a powerful open-source form and survey solution designed to help you get better experience data for your business. This tool allows you to survey specific customer segments at any point in the user journey, providing you with invaluable insights into what your customers think and feel.
Formbricks is a powerful open source survey tool designed to help you get better experience data for your business. This tool allows you to survey specific customer segments at any point in the user journey, providing you with invaluable insights into what your customers think and feel about your product.
- 👍 **Pre-segment users:** Don't ask everyone, all the time. Granularly segment your user base to get deep insights
- 👍 **Event-based surveys:** Trigger surveys based on user behavior, such as page views, clicks, and more
@@ -42,11 +49,15 @@ Formbricks is a powerful open-source form and survey solution designed to help y
- 👍 **Easy self-hosting:** Docker makes it possible to self-host Formbricks in minutes.
- ⚠️ **It's early for Formbricks.** Product developes rapidly but might encounter a bug here and there.
[Try it out](https://app.formbricks.com), [read more](https://formbricks.com) or dive into the [code base.](https://formbricks.com/github)
[Try it out](https://app.formbricks.com), [read more](https://formbricks.com) or dive into the [code base](https://formbricks.com/github) or comprehensive [docs](https://formbricks.com/docs).
## 2. SurveyJS - Build-it-yourself libraries
## 2. SurveyJS - Build-it-yourself library
<Image src={SurveyJS} alt="SurveyJS is a comprehensive" className="rounded-lg" />
<Image
src={SurveyJS}
alt="SurveyJS is a comprehensive JS library to build your own form or survey application."
className="rounded-lg"
/>
SurveyJS is a collection of JavaScript Librarys to build forms. Building your own form management system has never been easier than with SurveyJS. It packs:
@@ -58,9 +69,13 @@ SurveyJS is a collection of JavaScript Librarys to build forms. Building your ow
[Dive into the code on GitHub](https://github.com/surveyjs)
## 3. Typebot - Truly conversational Forms
## 3. Typebot - Truly conversational forms
<Image src={Typebot} alt="SurveyJS is a comprehensive" className="rounded-lg" />
<Image
src={Typebot}
alt="Open source survey and form builder SurveyJS lets you build surveys fast"
className="rounded-lg"
/>
Coming in at number three on our list is Typebot, that makes it really easy to create conversational forms and surveys. Typebot helps you engage with your audience in a more interactive way, leading to higher response rates and better data. It comes with:
@@ -74,9 +89,13 @@ Coming in at number three on our list is Typebot, that makes it really easy to c
## 4. OpnForm - Straight-forward survey builder
<Image src={OpnForm} alt="SurveyJS is a comprehensive" className="rounded-lg" />
<Image
src={OpnForm}
alt="OpnForm is an open source form builder for experience management"
className="rounded-lg"
/>
OpnForms is a flexible and powerful open-source form and survey tool designed to make data collection easy and efficient. OpnForm packs lots of features, especially for a Beta:
OpnForms is a flexible and powerful open source form and survey tool designed to make data collection easy and efficient. OpnForm packs lots of features, especially for a Beta:
- 👍 **Multiple Question Types:** Choose from a wide variety of question types to create highly customizable forms and surveys.
- 👍 **Conditional Logic:** Show or hide questions based on previous responses to create personalized surveys.
@@ -87,7 +106,11 @@ OpnForms is a flexible and powerful open-source form and survey tool designed to
## 5. LimeSurvey - Old (but gold?)
<Image src={LimeSurvey} alt="SurveyJS is a comprehensive" className="rounded-lg" />
<Image
src={LimeSurvey}
alt="LimeSurvey is open source survey builder to manage experiences with forms"
className="rounded-lg"
/>
LimeSurvey has been around for at least a decade. It's a powerful survey tool made for more classical, scientific surveying. It packs:
@@ -101,9 +124,9 @@ LimeSurvey has been around for at least a decade. It's a powerful survey tool ma
## Summary ☟
In this article, we've rounded up the top 5 open-source form and survey tools that are still rocking it in 2023. Perfect for devs who are always on the lookout for the latest and greatest!
In this article, we've rounded up the top 5 open source form and survey tools that are still rocking it in 2023. Perfect for devs who are always on the lookout for the latest and greatest!
1. Formbricks: A game-changer for in-product micro-surveys, letting you target specific customer segments at any point in their journey. It's still early days, but this bad boy is worth keeping an eye on.
1. Formbricks: A game-changer for in app micro surveys, letting you target specific customer segments at any point in their journey. It's still early days, but this bad boy is worth keeping an eye on.
2. SurveyJS: A must-have for DIY enthusiasts, this collection of JavaScript libraries makes building your own form management system a breeze. Just remember, the starting price is $499/year.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,178 @@
import Image from "next/image";
import LayoutMdx from "@/components/shared/LayoutMdx";
import { Callout } from "@/components/shared/Callout";
import AuthorBox from "@/components/shared/AuthorBox";
import TweetPeer from "./peer-tweet-typeform-open-source.png";
import SnoopForms from "./snoopforms-open-source-typeform-alternative.png";
import TwitterResult from "./twitter-results-PMF-cal.png";
import EmailResult from "./email-results-PMF-cal.png";
import TitleImage from "../github-accelerator-experience/formbricks-sponsored-by-github-accelerator-2023.webp";
import PMFDashboard from "./pmf-survey-dashboard.png";
import MattiJojo from "./matti-jojo.jpg";
export const meta = {
title: "Formbricks v1 - How we got here 🤸",
description:
"A lot has happened since Matti and I had a chat about open-source surveys in May last year. The release of Formbricks v1.0 is a perfect opportunity to look back on how it all started.",
date: "2023-07-14",
publishedTime: "2023-07-14T08:10:33",
authors: ["Johannes"],
section: "Open Source Experience Management",
tags: ["Open Source", "Experience Management", "Formbricks"],
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
_A lot has happened since Matti and I had a chat about open-source surveys in May last year. The release of Formbricks v1.0 is a perfect opportunity to look back on how it all started._
Funnily enough, it started with a tweet:
<Image
src={TweetPeer}
alt="Peer tweets about the best open-source Typeform alternative"
className="rounded-lg"
/>
When I read this tweet, I was sitting in a WeWork in Mexico City, feeling a bit guilty for being such a cliché digital nomad. Sipping on my decaffeinated matcha with lactose-free goat milk, I slid into Matti's DMs. I knew he had built an open-source survey tool a few months ago and suggested he comment on the tweet. We started chatting.
Both Matti and I had been freelancing for several months. While the money was good, we missed working on a project of our own. We met a couple of years ago while working on our startups. I had an app and wanted to be GDPR compliant; Matti offered to help out. We've stayed in contact ever since, giving feedback on projects and ideas.
So we chatted about the opportunity of building a commercial open-source alternative to Typeform. After reading up on the advantages - and challenges - of open-source, we decided to build a side project together: **[snoopForms: The Open-Source Typeform Alternative](https://snoopforms.com/)** 🦝
<Image
src={SnoopForms}
alt="SnoopForms was the OS Typeform alternative with a twist"
className="rounded-lg"
/>
### **snoopForms and why OS Typeform isnt a good business**
We shipped a first version of the product and the landing page and shared it on ProductHunt and HackerNews. It stirred up quite some interest—1.5k people signed up over a few weeks. During the launch, we sold 10 early bird deals for $179 each. We got excited! Is it really that easy?
Well no, it is not. Apart from the Early Birds (🤍) we attracted a looooooooot of people who liked Typeform, but didnt want to pay the high price tag. Generally, it's not the best idea to build a product with high competition (100s of survey tools) for a price-sensitive target group. Even data privacy isnt a good selling point when Jotform offers self-hosting and Typeform is an EU-based company (GDPR). Weve written **[in-depth](https://formbricks.com/blog/open-source-qualtrics-beats-typeform)** about why we eventually decided to shift our focus away from open-source Typeform.
Nonetheless, we were hooked on open-source surveys; we just hadn't found the right application yet.
### **snoopForms → Formbricks, a detour, and going full-time**
We decided to wrap up our freelance gigs and go in full-time, to find the right angle quicker. We built a MVP for _Building Bricks for Forms and Surveys_, but while the idea made sense, we struggled to build an easy, opionated solution which can be used for several use cases. To help engineers build form and survey applications faster, it needed to pack form creation, form analytics, graphs, data handling, data storage, analysis, integrations, and ways to act on the insights for a variety of use cases 🤷 There's a reason why successful dev tools focus on one of these aspects and do them well. So we kept looking…
### **Data Processing vs. Experience Management**
We had talked to and surveyed a lot of people. Zooming out, we saw that there are two applications for forms: data processing and experience management.
**Data processing** is essentially getting information out of a human brain into a digital system. The focus here lies on high integrability with (legacy) systems, versatility, and a great developer experience. Libraries like React Hook Forms or the more productized form.io fall into this category. While there certainly is a need here, it was a less inspiring field for us, especially after reading how [tedious](https://medium.com/@jgee/what-i-learned-in-two-years-of-moving-government-forms-online-1edc4c2aa089) it can be to bring [legacy](https://medium.com/san-francisco-digital-services/how-to-make-a-form-d1d1b67d95d7) form systems up to speed.
**Experience Management** on the other hand is a lot more exciting! Helping teams build better products, services, and organizations is a mission both Matti and I could get behind ❤️
<Callout title="In a nutshell" type="note">
Experience Management, in a nutshell, is enabling teams to gather, analyze, and act on qualitative data
collected from their users, customers, and employees.
</Callout>
We researched what was out there and talked to more founders. Initially, we wanted to build a solution for a problem we had ourselves: Gathering qualitative insights along the user journey of early adopters. Its common sense to never launch without analytics, but why does everyone launch without a system to gather qualitative user insights continuously? Mostly, because there's no easy, cheap and quick way to do it (yet).
### **The Product-Market Fit Survey Tool**
Following Paul Graham's advice to start with a **[narrow but deep hole](http://paulgraham.com/startupideas.html)**, we narrowed it down to one specific problem every founder has: Measuring Product-Market Fit. Many know the **[article](https://review.firstround.com/how-superhuman-built-an-engine-to-find-product-market-fit)** on how Superhuman built an engine to measure and optimize PMF. However, to run it correctly within your product, you do have to do quite some custom coding. So we decided to offer a solution which works out of the box.
<Image
src={PMFDashboard}
alt="Peer tweets about the best open-source Typeform alternative"
className="rounded-lg"
/>
_We built a custom dashboard just to visualize the PMF survey results_
We shipped an MVP, launched it, and picked up the conversations with the founders who had tried to measure PMF
before. We quickly learned this:
- Our MVP was scoped so rigidly that we had to do quite some custom coding ourselves to get it implemented. Setting it up and maintaining it stood in no relation to the value it provided because…
- The PMF survey is for teams that "kinda" have PMF, not for teams which are just starting out. You need a few hundred recurring users in your app to be able to reach statistical significance and run the survey every 3 months without asking anyone twice. For teams who dont have that, talking to users 1:1 will remain the best way to go.
- Founders were reluctant to add another tool just for one survey and purpose.
After the rather disappointing launch of the PMF survey tool, we hit a low. The excitement about the initial snoopForm traction had passed. Since November, we had been working full-time for four months iterating on different products in the forms and survey space, and it didnt feel like we made much progress. But that wasnt really true.
### **"The essence of strategy is choosing what not to do.”**
It was quite a journey since Matti wrote the first line of code in July last year. Even though it didnt _feel_ like much progress, we had learned a lot. Most importantly, we knew what **not** to build.
With snoopForms, we learned that there is a latent need for an open-source standalone survey tool, with a Typeform-like feature set: Its a great opportunity to build a community and **not** a great business opportunity. The PMF survey tool taught us about the needs of early-stage teams and that surveys are **not** the best solution to their problems. However, they do need some form to gather qualitative feedback and a smooth way to work with it 😏
We took everything we knew and wrote a Masterplan. And then we sat down coded.
### **Open-source in-product surveys**
March and the first half of April flew by as we hacked together the MVP of what Formbricks is today: A tool which enables teams to target specific segments of their user base with versatile micro-surveys. Matti solved some tricky technical challenges around the Formbricks Widget and shipped the backend. ChatGPT and I warmed up with next.js and built frontend. Seeing the product come together was fun and felt like progress - exactly what we needed!
<Image
src={TitleImage}
alt="GitHub sponsors Formbricks to join their open-source accelerator program"
className="rounded-lg"
/>
A welcomed motivator came in the form of an email from GitHub: We we were chosen to take part in the first-ever batch of the GitHub Open-Source Accelerator! It was a really fun programme and we learned a lot! You can read about our experience [here.](https://formbricks.com/blog/github-accelerator-experience)
### **On the right track 🚆**
Once we were ready to share our MVP with the world, we knew we were on the right track! The feedback was really good and usage picked up right from the start. One morning we woke up to our analytics dashboard showing thousands of survey displays with 500+ responses in the past couple of hours! This is when we knew were onto something 🚀
A couple of weeks later, Peer from [Cal.com](http://Cal.com) came back to northern Germany to visit his family and friends. We hung out in the co-working space, where he did his first steps as an entrepreneur. As we were chatting about how to separate opinion from experience, Peer decided to shoot an email with a survey to the 12k most active users of Cal.com. A few days before, he asked his Twitter following the PMF question:
<Image
src={TwitterResult}
alt="SnoopForms was the OS Typeform alternative with a twist"
className="rounded-lg"
/>
We wanted to compare how the publicly asked survey (Twitter) compared to a survey among the community of users. The results were eye-opening:
<Image
src={EmailResult}
alt="SnoopForms was the OS Typeform alternative with a twist"
className="rounded-lg"
/>
_Anything above 40% is considered PMF, 60% is 🔥🔥🔥_
For us, this was not only a good test for our system but it was a great example that Experience Management does not only happen within apps, but on several touch points right from the start. If we want to offer a solution which can comprehensively measure and evaluate experiences, it cannot be confined to in-product surveys for too long.
### **Typeform sneaking back in?**
Given our early traction with snoopForms, its not a big surprise that community requests for a Typeform-like standalone survey popped up more and more. Since we had all the pieces in place, we shipped standalone surveys in a day. However, its not our focus right now, so we hand over most of the feature requests to the community. And the community ships! Just in the past couple of weeks, we got:
- **Prefill Data in Link Surveys** by Piyush - [Docs](https://formbricks.com/docs/link-surveys/data-prefilling)
- **Identify Users Link identification** by Zorig - [Docs](https://formbricks.com/docs/link-surveys/user-identification)
- **Close Survey on Date** by Piyush
- **Close Survey after x Responses** by Pradumn
- **Redirect on Link Survey Completion** by Dhruwang
- **Embed Link Survey via iframe** by Ankur
- **Set Custom Survey Closed Message** by Pradumn
- **Randomize Answer Options** by Shubdeep
We absolutely love the idea of being the platform for a community-developed standalone survey experience! Its a win-win: The community gets a free, self-hostable standalone survey product and we get a growing upper funnel for our Experience Management solution 🤸
### **Its coming together 🔥**
Looking back at the amount of value Formbricks can deliver after not even 5 months fills us with excitement. Matti and I have been hacking it together for the most part but we wouldnt be anywhere nearly as close without the support of our community and friends - thank you!
And were only getting started: We were able to hire two engineers from our community of contributors to build up more shipping velocity. We also started working with Kristian who runs our [open-source Design repo](https://github.com/formbricks/design) more closely and are super excited about what were cooking up. Well make sure Formbricks stays easy-to-use, accessible and simply beautiful as it grows more powerful! We can't tell you how excited we are about what's coming 😁
If youre curious to learn about all the things you can do with Formbricks today, weve written and keep updating a list of Best Practices incl. survey templates in our [Docs](https://formbricks.com/docs/best-practices/cancel-subscription).
### Sounds good? **Build with us!**
- **Eager to contribute?** [Join our Discord](https://formbricks.com/discord) and say Hi! Were more than happy to find the perfect issue for your level of experience 🙌 😊
- **Got an experience to measure?** Open-source is the way to go. [Lets have a chat 🤙](https://cal.com/johannes/15)
<Image
src={MattiJojo}
alt="SnoopForms was the OS Typeform alternative with a twist"
className="rounded-lg w-1/2"
/>
### All the best!
Johannes & Matti
export default ({ children }) => <LayoutMdx meta={meta}>{children}</LayoutMdx>;

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -5,14 +5,21 @@ import Demo from "./our-experience-github-acc-demo-screenshot.png";
import Mail from "./github-accelerator-selection-mail.png";
import Teams from "./github-accelerator-2022-teams.png";
import NewsletterSignup from "@/components/shared/NewsletterSignup";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "Our GitHub Accelerator Experience 👀",
description:
"What we learned during the first GitHub Open-Source Accelerator Programm - our experience and if we would do it again.",
date: "2023-04-13",
date: "2023-06-06",
publishedTime: "2023-06-06T05:12:25",
authors: ["Johannes"],
section: "GitHub Accelerator",
tags: ["GitHub Accelerator", "Open-Source", "Startup"],
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
_We were among the first 20 teams ever to run through the Open-Source Accelerator by Github. Read about our experience and if we would do it again:_
<Image
@@ -37,7 +44,7 @@ January and February came and went. On the 22nd of March, we received an email f
Needless to say, we were thrilled! We were selected from over 1000 open-source projects, alongside renowned and popular projects like [Nuxt](https://github.com/nuxt/nuxt), [TRPC](https://github.com/trpc/trpc), and [Responsively App](https://github.com/responsively-org/responsively-app). Here is a summary of what we got:
### What we got on paper
### What the GitHub Accelerator offers on paper
✅ Ten sessions with **well-known** figures from the open-source community (Wednesdays)
@@ -105,7 +112,7 @@ Ericas talk was really impressive and so is she: Founder of Bitnami, COO of GitH
**GitHub Demo Day:** All teams presented what they achieved during the 10 week programm. It was great fun to present Formbricks, [you can watch it on Youtube.](https://www.youtube.com/live/Gj6Bez2182k?feature=share&t=1448)
## Would we do it again? And should you?
## Would we do the GitHub Accelerator again? And should you?
Yes, absolutely. The sessions were excellent, we met a handful of inspiring builders, and the 20k USD was a helpful financial boost. The application process might evolve, but since all of your code is open-source anyway, you might as well throw your hat in the ring.

View File

@@ -2,14 +2,21 @@ import Image from "next/image";
import LayoutMdx from "@/components/shared/LayoutMdx";
import TitleImage from "./formbricks-sponsored-by-github-accelerator-2023.webp";
import NewsletterSignup from "@/components/shared/NewsletterSignup";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "Formbricks Joins GitHub Accelerator's Inaugural Cohort 💃",
description:
"We're getting ready to take our open-source experience management platform to new heights, thanks to being part of the first-ever GitHub Accelerator program!",
date: "2023-04-13",
publishedTime: "2023-04-13T05:12:25",
authors: ["Johannes"],
section: "GitHub Accelerator",
tags: ["GitHub Accelerator", "Open-Source"],
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
_We're getting ready to take our open-source experience management platform to new heights, thanks to being part of the first-ever GitHub Accelerator program!_
<Image

View File

@@ -4,13 +4,20 @@ import RobinHoodMeme from "./robin-hood-meme.png";
import WhyWeDoIt from "./why-we-do-it.png";
import EverythinEverywhereAllAtOnce from "./everything_everywhere_all_at_once.png";
import ResponsiveEmbed from "react-responsive-embed";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "Open source forms will save the world.",
description: "What motivates us to build open source tech in such a crowded space?",
date: "2022-08-26",
publishedTime: "2022-08-26T12:12:25",
authors: ["Johannes"],
section: "Open Source Survey Tool",
tags: ["Open Source", "Survey Tool", "Forms"],
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
<Image src={RobinHoodMeme} alt="Robin Hood Meme" className="rounded-lg" />
_What motivates us to build open source tech in such a crowded space? What do we see what others might not? And how do we understand the relationship between free open source tech and a commercial complement?_

View File

@@ -9,14 +9,21 @@ import Wrestling from "./wrestling.jpg";
import TypeformValue from "./typeform-value-prop.png";
import ResponsiveEmbed from "react-responsive-embed";
import { Callout } from "@/components/shared/Callout";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "Why Qualtrics beats Typeform, especially Open-Source",
description:
"We kicked it off with a Typeform open-source alternative. As we build and learn, our focus is shifting. Read why:",
date: "2023-03-24",
publishedTime: "2023-03-23T08:20:15",
authors: ["Johannes"],
section: "Open Source Experience Management",
tags: ["Open Source", "Experience Management", "Typeform", "Qualtrics"],
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
<Image src={Wrestling} alt="Why we do it" className="rounded-lg" />
_In September, we kicked it off with a Typeform open-source alternative. As we build and learn, our focus is shifting. We talk about how we look at form and survey tools today, why experience management not only matters for enterprise and why the endgame looks a lot more like open-source Qualtrics. Qualtrics? What happened to Typeform?_

View File

@@ -2,13 +2,20 @@ import Image from "next/image";
import LayoutMdx from "@/components/shared/LayoutMdx";
import HeaderImage from "./formbricks-logo-header-open-source-form-infrastructure.svg";
import HeroAnimation from "../../../components/shared/HeroAnimation.tsx";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "snoopForms → Formbricks 🎉",
description: "A new name, a new look, key learnings and what's next.",
date: "2022-11-07",
publishedTime: "2023-07-11T12:00:06",
authors: ["Johannes"],
section: "Formbricks",
tags: ["Formbricks", "snoopForms", "Open Source"],
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
<Image src={HeaderImage} alt="Formbricks - Open Source Forms and Surveys" className="rounded-lg" />
_It has been quiet in the past weeks, but we didn't spend our days sitting around. Find out what we were up to and where we are taking Formbricks from here._

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,14 +6,21 @@ import TitleImage from "./title-image.png";
import HeaderImage from "./formbricks-logo.svg";
import ProprietaryDependence from "./propietary-dependence.jpeg";
import ResponsiveEmbed from "react-responsive-embed";
import AuthorBox from "@/components/shared/AuthorBox";
export const meta = {
title: "Why Open-Source + No-Code is the Future of Enterprise & Goverment Software",
description:
"Here is why a no-code interface is cheatcode for OSS and why particularly large corporations and governments are to benefit the most",
date: "2022-06-03",
publishedTime: "2022-06-03T06:04:08",
authors: ["Johannes"],
section: "Open-Source",
tags: ["Open-Source", "No-Code", "Enterprise", "Government"],
};
<AuthorBox name="Johannes" title="Co-Founder" date="April 7th, 2023" duration="4" />
<Image src={TitleImage} alt="Title Image" className="rounded-lg" />
_Open source software (OSS) beats out proprietary software in every regard - except for value capturing. No-Code tools shorten the feedback loop between builders and consumers, kicking productivity through the roof. Here is why a no-code interface is cheatcode for OSS and why particularly large corporations and governments are to benefit the most._

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

@@ -36,25 +36,27 @@ pnpm add @formbricks/js
## Integrating with Next.js 13 App Directory
The Next.js 13 app directory requires us to initialize Formbricks differently than the pages directory. Specifically, the app directory server-side renders components by default, and the formbricks-js library is a client-side library. To make these work together, create a `formbricks.js` file (or formbricks.ts if you are using Typescript) and set up the FormbricksProvider with the 'use client' directive:
The Next.js 13 app directory requires us to initialize Formbricks differently than the pages directory. Specifically, the app directory server-side renders components by default, and the formbricks-js library is a client-side library. To make these work together, create a `formbricks.tsx` file (or `formbricks.js` if you don't use Typescript) and set up the FormbricksProvider with the 'use client' directive:
```tsx
// app/formbricks.js
// app/formbricks.tsx
"use client";
import formbricks from "@formbricks/js";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
if (typeof window !== "undefined") {
formbricks.init({
environmentId: "your-environment-id",
apiHost: "your-api-host", // e.g. https://app.formbricks.com
debug: true, // remove when in production
});
}
export default function Providers({ Component, pageProps }: AppProps) {
export default function FormbricksProvider() {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
formbricks.init({
environmentId: "clj66eqzu00m5qu0g8leglrns",
apiHost: "https://app.formbricks.com", // e.g. https://app.formbricks.com
debug: true, // remove when in production
});
}, []);
useEffect(() => {
formbricks?.registerRouteChange();
}, [pathname, searchParams]);
@@ -63,21 +65,17 @@ export default function Providers({ Component, pageProps }: AppProps) {
}
```
Once we do this, we can then import the `provider.js` file in our `app/layout.js` file, and wrap our app in the Formbricks provider.
Once we do this, we can then import the `formbricks.tsx` file in our `app/layout.tsx` file, and wrap our app in the Formbricks provider.
```tsx
// app/layout.js
import Providers from "./formbricks";
// app/layout.tsx
import FormbricksProvider from "./formbricks";
import "./globals.css";
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({ children }) {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<Providers />
<FormbricksProvider />
<body>{children}</body>
</html>
);

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
## (Most users: One-click Ubuntu setup) Running the Shell Script
This is the quickest way to get Formbricks up and running on an Ubuntu server. The shell script will automatically install all the required dependencies and configure your server, making the process a breeze.
### Requirements
Before you proceed, make sure you have the following:
- A Linux Ubuntu Virtual Machine deployed with SSH access.
- An A record set up to connect a custom domain to your instance. Formbricks will automatically create an SSL certificate for your domain using Let's Encrypt.
## Single Command Setup
Copy and paste the following command into your terminal:
```bash
/bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/formbricks/formbricks/main/docker/production.sh)"
```
The script will prompt you for the following information:
1. **Overwriting Docker GPG Keys**: If Docker GPG keys already exist, the script will ask if you want to overwrite them.
2. **Email Address**: Provide your email address for SSL certificate registration with Let's Encrypt.
3. **Domain Name**: Enter the domain name that Traefik will use to create the SSL certificate and forward requests to Formbricks.
That's it! After running the command and providing the required information, visit the domain name you entered, and you should see the Formbricks home wizard!
## (Manual Deployment) Running the pre-built Docker Image
### Requirements
Ensure `docker` & `docker compose` are installed on your server/system. Both are typically included with Docker utilities, like Docker Desktop and Rancher Desktop.
**Note**: `docker compose` without the hyphen is now the primary method of using docker-compose, according to the Docker documentation.
## (Most users) Running the pre-built Docker Image
This is suitable for those who are testing Formbricks or running it with minimal to no modifications. For this we use the [public Docker image](https://hub.docker.com/r/formbricks/formbricks) and a simple docker-compose file.
1. **Create a New Directory for Formbricks**
@@ -41,7 +73,7 @@ This is suitable for those who are testing Formbricks or running it with minimal
Download the docker-compose file directly from the Formbricks repository:
```bash
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/docker/main/docker-compose.yml
curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/main/docker/docker-compose.yml
```
3. **Generate NextAuth Secret**
@@ -89,21 +121,21 @@ This is suitable for those who are testing Formbricks or running it with minimal
## (Advanced users) Build and Run Formbricks
### Requirements
Ensure `docker` & `docker compose` are installed on your server/system. Both are typically included with Docker utilities, like Docker Desktop and Rancher Desktop.
**Note**: `docker compose` without the hyphen is now the primary method of using docker-compose, according to the Docker documentation.
1. Clone the repository:
```bash
git clone https://github.com/formbricks/formbricks.git && cd formbricks
```
Create a `.env` file based on `.env.docker` and change all fields according to your setup. This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings in the `.env` file. If you configured your email credentials, you can also comment the following lines to enable email verification (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1`)
2. Modify the `.env.docker` file as required by your setup. This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings. If you configured your email credentials, you can also comment the following lines to enable email verification (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1`)
2. Copy the `.env.docker` file to `.env` and edit it with an editor of your choice if needed.
```bash
cp .env.docker .env
```
Note: The environment variables are used at build time. When you change environment variables later, you need to rebuild the image with `docker compose build` for the changes to take effect.
Note: All environment variables starting with `NEXT_PUBLIC_` are used at build time. When you change environment variables later, you need to rebuild the image with `docker compose build` for the changes to take effect.
3. Finally start the docker compose process to build and spin up the Formbricks container as well as the PostgreSQL database.
@@ -120,12 +152,12 @@ Certainly, here is the reformatted version for Formbricks environment variables.
These variables must also be provided at runtime.
| Variable | Description | Required | Default |
| ---------------------- | --------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------- |
| NEXT_PUBLIC_WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
| DATABASE_URL | Database URL with credentials. | required | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` |
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` |
| Variable | Description | Required | Default |
| --------------- | --------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------- |
| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
| DATABASE_URL | Database URL with credentials. | required | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` |
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` |
### Build-time Variables
@@ -164,4 +196,16 @@ docker compose logs -f
You can close the logs again with `CTRL + C`.
## Troubleshooting for the Shell Script Setup
If you encounter any issues, consider the following steps:
- **Inbound Rules**: Make sure you have added inbound rules for Port 80 and 443 in your VM's Security Group.
- **A Record**: Verify that you have set up an A record for your domain, pointing to your VM's IP address.
- **Check Docker Instances**: Run `docker ps` to check the status of the Docker instances.
- **Check Formbricks Logs**: Run `cd formbricks && docker compose logs` to check the logs of the Formbricks stack.
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;

View File

@@ -41,7 +41,7 @@ export const meta = {
]}
example={`{
"url": "https://mysystem.com/myendpoint",
"trigger": "responseFinished"
"triggers": ["responseFinished"]
}`}
responses={[
{

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

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

@@ -0,0 +1,23 @@
# @formbricks/web
## 1.0.2
### Patch Changes
- 3dde021c: Release version 1.0.2
- Updated dependencies [3dde021c]
- @formbricks/js@1.0.3
## 0.1.2
### Patch Changes
- Updated dependencies [a1b447ca]
- @formbricks/js@1.0.2
## 0.1.1
### Patch Changes
- Updated dependencies [3d0d633b]
- @formbricks/js@1.0.1

View File

@@ -10,7 +10,7 @@ COPY .env.docker /app/apps/web/.env
RUN pnpm install
# Build the project
RUN pnpm prebuild --filter=web...
RUN pnpm post-install --filter=web...
RUN pnpm turbo run build --filter=web...
FROM node:18-alpine AS runner
@@ -36,6 +36,8 @@ COPY --from=installer --chown=nextjs:nodejs /app/packages/database/migrations ./
ENV NEXTAUTH_SECRET=$NEXTAUTH_SECRET
EXPOSE 3000
CMD if [ "$NEXTAUTH_SECRET" != "RANDOM_STRING" ]; then \
pnpm dlx prisma migrate deploy && node apps/web/server.js; \
else \

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

@@ -16,6 +16,7 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/shared/DropdownMenu";
import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import CreateTeamModal from "@/components/team/CreateTeamModal";
import {
@@ -65,6 +66,8 @@ import AddProductModal from "./AddProductModal";
import { formbricksLogout } from "@/lib/formbricks";
import formbricks from "@formbricks/js";
import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants";
import { MenuIcon } from "lucide-react";
import { cn } from "@formbricks/lib/cn";
interface EnvironmentsNavbarProps {
environmentId: string;
@@ -86,6 +89,8 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
const [showAddProductModal, setShowAddProductModal] = useState(false);
const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false);
useEffect(() => {
if (environment && environment.widgetSetupCompleted) {
setWidgetSetupCompleted(true);
@@ -117,9 +122,9 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
},
{
name: "Actions & Attributes",
href: `/environments/${environmentId}/events`,
href: `/environments/${environmentId}/actions`,
icon: FilterIcon,
current: pathname?.includes("/events") || pathname?.includes("/attributes"),
current: pathname?.includes("/actions") || pathname?.includes("/attributes"),
},
{
name: "Integrations",
@@ -166,7 +171,7 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
icon: CreditCardIcon,
label: "Billing & Plan",
href: `/environments/${environmentId}/settings/billing`,
hidden: IS_FORMBRICKS_CLOUD,
hidden: !IS_FORMBRICKS_CLOUD,
},
],
},
@@ -221,19 +226,20 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
<nav className="top-0 z-10 w-full border-b border-slate-200 bg-white">
{environment?.type === "development" && (
<div className="h-6 w-full bg-[#A33700] p-0.5 text-center text-sm text-white">
You&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>
)}
<div className="w-full px-4 sm:px-6">
<div className="flex h-14 justify-between">
<div className="flex space-x-4 py-2">
<div className="flex space-x-4 py-2">
<Link
href={`/environments/${environmentId}/surveys/`}
className=" flex items-center justify-center rounded-md bg-gradient-to-b text-white transition-all ease-in-out hover:scale-105">
{/* <PlusIcon className="h-6 w-6" /> */}
<Image src={FaveIcon} width={30} height={30} alt="faveicon" />
</Link>
{navigation.map((item) => {
const IconComponent: React.ElementType = item.icon;
@@ -245,7 +251,7 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
item.current
? "bg-slate-100 text-slate-900"
: "text-slate-900 hover:bg-slate-50 hover:text-slate-900",
"inline-flex items-center rounded-md px-2 py-1 text-sm font-medium"
"hidden items-center rounded-md px-2 py-1 text-sm font-medium sm:inline-flex"
)}
aria-current={item.current ? "page" : undefined}>
<IconComponent className="mr-3 h-5 w-5" />
@@ -254,6 +260,34 @@ export default function EnvironmentsNavbar({ environmentId, session }: Environme
);
})}
</div>
<div className="flex items-center sm:hidden">
<Popover open={mobileNavMenuOpen} onOpenChange={setMobileNavMenuOpen}>
<PopoverTrigger onClick={() => setMobileNavMenuOpen(!mobileNavMenuOpen)}>
<span>
<MenuIcon className="h-6 w-6 rounded-md bg-slate-200 p-1 text-slate-600" />
</span>
</PopoverTrigger>
<PopoverContent className="mr-4 bg-slate-200">
<div className="flex flex-col">
{navigation.map((navItem) => (
<Link key={navItem.name} href={navItem.href}>
<div
onClick={() => setMobileNavMenuOpen(false)}
className={cn(
"flex items-center space-x-2 rounded-md p-2",
navItem.current && "bg-slate-300"
)}>
<navItem.icon className="h-5 w-5" />
<span className="font-medium text-slate-600">{navItem.name}</span>
</div>
</Link>
))}
</div>
</PopoverContent>
</Popover>
</div>
<div className="hidden sm:ml-6 sm:flex sm:items-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>

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

@@ -0,0 +1,300 @@
"use server";
import { prisma } from "@formbricks/database";
import { ResourceNotFoundError } from "@formbricks/errors";
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
import { deleteSurvey, getSurvey } from "@formbricks/lib/services/survey";
import { Team } from "@prisma/client";
import { Prisma as prismaClient } from "@prisma/client/";
export async function createTeam(teamName: string, ownerUserId: string): Promise<Team> {
const newTeam = await prisma.team.create({
data: {
name: teamName,
memberships: {
create: {
user: { connect: { id: ownerUserId } },
role: "owner",
accepted: true,
},
},
products: {
create: [
{
name: "My Product",
environments: {
create: [
{
type: "production",
eventClasses: {
create: [
{
name: "New Session",
description: "Gets fired when a new session is created",
type: "automatic",
},
{
name: "Exit Intent (Desktop)",
description: "A user on Desktop leaves the website with the cursor.",
type: "automatic",
},
{
name: "50% Scroll",
description: "A user scrolled 50% of the current page",
type: "automatic",
},
],
},
attributeClasses: {
create: [
{
name: "userId",
description: "The internal ID of the person",
type: "automatic",
},
{
name: "email",
description: "The email of the person",
type: "automatic",
},
],
},
},
{
type: "development",
eventClasses: {
create: [
{
name: "New Session",
description: "Gets fired when a new session is created",
type: "automatic",
},
{
name: "Exit Intent (Desktop)",
description: "A user on Desktop leaves the website with the cursor.",
type: "automatic",
},
{
name: "50% Scroll",
description: "A user scrolled 50% of the current page",
type: "automatic",
},
],
},
attributeClasses: {
create: [
{
name: "userId",
description: "The internal ID of the person",
type: "automatic",
},
{
name: "email",
description: "The email of the person",
type: "automatic",
},
],
},
},
],
},
},
],
},
},
include: {
memberships: true,
products: {
include: {
environments: true,
},
},
},
});
const teamId = newTeam?.id;
if (teamId) {
fetch(`${WEBAPP_URL}/api/v1/teams/${teamId}/add_demo_product`, {
method: "POST",
headers: {
"x-api-key": INTERNAL_SECRET,
},
});
}
return newTeam;
}
export async function duplicateSurveyAction(environmentId: string, surveyId: string) {
const existingSurvey = await getSurvey(surveyId);
if (!existingSurvey) {
throw new ResourceNotFoundError("Survey", surveyId);
}
// create new survey with the data of the existing survey
const newSurvey = await prisma.survey.create({
data: {
...existingSurvey,
id: undefined, // id is auto-generated
environmentId: undefined, // environmentId is set below
name: `${existingSurvey.name} (copy)`,
status: "draft",
questions: JSON.parse(JSON.stringify(existingSurvey.questions)),
thankYouCard: JSON.parse(JSON.stringify(existingSurvey.thankYouCard)),
triggers: {
create: existingSurvey.triggers.map((trigger) => ({
eventClassId: trigger.id,
})),
},
attributeFilters: {
create: existingSurvey.attributeFilters.map((attributeFilter) => ({
attributeClassId: attributeFilter.attributeClassId,
condition: attributeFilter.condition,
value: attributeFilter.value,
})),
},
environment: {
connect: {
id: environmentId,
},
},
surveyClosedMessage: existingSurvey.surveyClosedMessage
? JSON.parse(JSON.stringify(existingSurvey.surveyClosedMessage))
: prismaClient.JsonNull,
},
});
return newSurvey;
}
export async function copyToOtherEnvironmentAction(
environmentId: string,
surveyId: string,
targetEnvironmentId: string
) {
const existingSurvey = await prisma.survey.findFirst({
where: {
id: surveyId,
environmentId,
},
include: {
triggers: {
include: {
eventClass: true,
},
},
attributeFilters: {
include: {
attributeClass: true,
},
},
},
});
if (!existingSurvey) {
throw new ResourceNotFoundError("Survey", surveyId);
}
let targetEnvironmentTriggers: string[] = [];
// map the local triggers to the target environment
for (const trigger of existingSurvey.triggers) {
const targetEnvironmentTrigger = await prisma.eventClass.findFirst({
where: {
name: trigger.eventClass.name,
environment: {
id: targetEnvironmentId,
},
},
});
if (!targetEnvironmentTrigger) {
// if the trigger does not exist in the target environment, create it
const newTrigger = await prisma.eventClass.create({
data: {
name: trigger.eventClass.name,
environment: {
connect: {
id: targetEnvironmentId,
},
},
description: trigger.eventClass.description,
type: trigger.eventClass.type,
noCodeConfig: trigger.eventClass.noCodeConfig
? JSON.parse(JSON.stringify(trigger.eventClass.noCodeConfig))
: undefined,
},
});
targetEnvironmentTriggers.push(newTrigger.id);
} else {
targetEnvironmentTriggers.push(targetEnvironmentTrigger.id);
}
}
let targetEnvironmentAttributeFilters: string[] = [];
// map the local attributeFilters to the target env
for (const attributeFilter of existingSurvey.attributeFilters) {
// check if attributeClass exists in target env.
// if not, create it
const targetEnvironmentAttributeClass = await prisma.attributeClass.findFirst({
where: {
name: attributeFilter.attributeClass.name,
environment: {
id: targetEnvironmentId,
},
},
});
if (!targetEnvironmentAttributeClass) {
const newAttributeClass = await prisma.attributeClass.create({
data: {
name: attributeFilter.attributeClass.name,
description: attributeFilter.attributeClass.description,
type: attributeFilter.attributeClass.type,
environment: {
connect: {
id: targetEnvironmentId,
},
},
},
});
targetEnvironmentAttributeFilters.push(newAttributeClass.id);
} else {
targetEnvironmentAttributeFilters.push(targetEnvironmentAttributeClass.id);
}
}
// create new survey with the data of the existing survey
const newSurvey = await prisma.survey.create({
data: {
...existingSurvey,
id: undefined, // id is auto-generated
environmentId: undefined, // environmentId is set below
name: `${existingSurvey.name} (copy)`,
status: "draft",
questions: JSON.parse(JSON.stringify(existingSurvey.questions)),
thankYouCard: JSON.parse(JSON.stringify(existingSurvey.thankYouCard)),
triggers: {
create: targetEnvironmentTriggers.map((eventClassId) => ({
eventClassId: eventClassId,
})),
},
attributeFilters: {
create: existingSurvey.attributeFilters.map((attributeFilter, idx) => ({
attributeClassId: targetEnvironmentAttributeFilters[idx],
condition: attributeFilter.condition,
value: attributeFilter.value,
})),
},
environment: {
connect: {
id: targetEnvironmentId,
},
},
surveyClosedMessage: existingSurvey.surveyClosedMessage ?? prismaClient.JsonNull,
},
});
return newSurvey;
}
export const deleteSurveyAction = async (surveyId: string) => {
await deleteSurvey(surveyId);
};

View File

@@ -1,9 +1,10 @@
import JsLogo from "@/images/jslogo.png";
import WebhookLogo from "@/images/webhook.png";
import ZapierLogo from "@/images/zapier-small.png";
import { Card } from "@formbricks/ui";
import Image from "next/image";
import JsLogo from "@/images/jslogo.png";
import ZapierLogo from "@/images/zapier-small.png";
export default function IntegrationsPage() {
export default function IntegrationsPage({ params }) {
return (
<div>
<h1 className="my-2 text-3xl font-bold text-slate-800">Integrations</h1>
@@ -11,17 +12,34 @@ export default function IntegrationsPage() {
<div className="grid grid-cols-3 gap-6">
<Card
docsHref="https://formbricks.com/docs/getting-started/nextjs-app"
docsText="Docs"
docsNewTab={true}
label="Javascript Widget"
description="Integrate Formbricks into your Webapp"
icon={<Image src={JsLogo} alt="Javascript Logo" />}
/>
<Card
docsHref="https://formbricks.com/docs/integrations/zapier"
docsText="Docs"
docsNewTab={true}
connectHref="https://zapier.com/apps/formbricks/integrations"
connectText="Connect"
connectNewTab={true}
label="Zapier"
description="Integrate Formbricks with 5000+ apps via Zapier"
icon={<Image src={ZapierLogo} alt="Zapier Logo" />}
/>
<Card
connectHref={`/environments/${params.environmentId}/integrations/webhooks`}
connectText="Manage Webhooks"
connectNewTab={false}
docsHref="https://formbricks.com/docs/webhook-api/overview"
docsText="Docs"
docsNewTab={true}
label="Webhooks"
description="Trigger Webhooks based on actions in your surveys"
icon={<Image src={WebhookLogo} alt="Webhook Logo" />}
/>
</div>
</div>
);

View File

@@ -0,0 +1,231 @@
import SurveyCheckboxGroup from "@/app/(app)/environments/[environmentId]/integrations/webhooks/SurveyCheckboxGroup";
import TriggerCheckboxGroup from "@/app/(app)/environments/[environmentId]/integrations/webhooks/TriggerCheckboxGroup";
import { triggers } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/HardcodedTriggers";
import { testEndpoint } from "@/app/(app)/environments/[environmentId]/integrations/webhooks/testEndpoint";
import Modal from "@/components/shared/Modal";
import { createWebhook } from "@formbricks/lib/services/webhook";
import { TPipelineTrigger } from "@formbricks/types/v1/pipelines";
import { TSurvey } from "@formbricks/types/v1/surveys";
import { TWebhookInput } from "@formbricks/types/v1/webhooks";
import { Button, Input, Label } from "@formbricks/ui";
import clsx from "clsx";
import { Webhook } from "lucide-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
interface AddWebhookModalProps {
environmentId: string;
open: boolean;
surveys: TSurvey[];
setOpen: (v: boolean) => void;
}
export default function AddWebhookModal({ environmentId, surveys, open, setOpen }: AddWebhookModalProps) {
const router = useRouter();
const {
handleSubmit,
reset,
register,
formState: { isSubmitting },
} = useForm();
const [testEndpointInput, setTestEndpointInput] = useState("");
const [hittingEndpoint, setHittingEndpoint] = useState<boolean>(false);
const [endpointAccessible, setEndpointAccessible] = useState<boolean>();
const [selectedTriggers, setSelectedTriggers] = useState<TPipelineTrigger[]>([]);
const [selectedSurveys, setSelectedSurveys] = useState<string[]>([]);
const [selectedAllSurveys, setSelectedAllSurveys] = useState(false);
const [creatingWebhook, setCreatingWebhook] = useState(false);
const handleTestEndpoint = async (sendSuccessToast: boolean) => {
try {
setHittingEndpoint(true);
await testEndpoint(testEndpointInput);
setHittingEndpoint(false);
if (sendSuccessToast) toast.success("Yay! We are able to ping the webhook!");
setEndpointAccessible(true);
return true;
} catch (err) {
setHittingEndpoint(false);
toast.error("Oh no! We are unable to ping the webhook!");
setEndpointAccessible(false);
return false;
}
};
const handleSelectAllSurveys = () => {
setSelectedAllSurveys(!selectedAllSurveys);
setSelectedSurveys([]);
};
const handleSelectedSurveyChange = (surveyId: string) => {
setSelectedSurveys((prevSelectedSurveys: string[]) =>
prevSelectedSurveys.includes(surveyId)
? prevSelectedSurveys.filter((id) => id !== surveyId)
: [...prevSelectedSurveys, surveyId]
);
};
const handleCheckboxChange = (selectedValue: TPipelineTrigger) => {
setSelectedTriggers((prevValues) =>
prevValues.includes(selectedValue)
? prevValues.filter((value) => value !== selectedValue)
: [...prevValues, selectedValue]
);
};
const submitWebhook = async (data: TWebhookInput): Promise<void> => {
if (!isSubmitting) {
try {
setCreatingWebhook(true);
if (!testEndpointInput || testEndpointInput === "") {
throw new Error("Please enter a URL");
}
if (selectedTriggers.length === 0) {
throw new Error("Please select at least one trigger");
}
if (!selectedAllSurveys && selectedSurveys.length === 0) {
throw new Error("Please select at least one survey");
}
const endpointHitSuccessfully = await handleTestEndpoint(false);
if (!endpointHitSuccessfully) return;
const updatedData: TWebhookInput = {
name: data.name,
url: testEndpointInput,
triggers: selectedTriggers,
surveyIds: selectedSurveys,
};
await createWebhook(environmentId, updatedData);
router.refresh();
setOpenWithStates(false);
toast.success("Webhook added successfully.");
} catch (e) {
toast.error(e.message);
} finally {
setCreatingWebhook(false);
}
}
};
const setOpenWithStates = (isOpen: boolean) => {
setOpen(isOpen);
reset();
setTestEndpointInput("");
setEndpointAccessible(undefined);
setSelectedSurveys([]);
setSelectedTriggers([]);
setSelectedAllSurveys(false);
};
return (
<Modal open={open} setOpen={setOpenWithStates} noPadding closeOnOutsideClick={false}>
<div className="flex h-full flex-col rounded-lg">
<div className="rounded-t-lg bg-slate-100">
<div className="flex w-full items-center justify-between p-6">
<div className="flex items-center space-x-2">
<div className="mr-1.5 h-6 w-6 text-slate-500">
<Webhook />
</div>
<div>
<div className="text-xl font-medium text-slate-700">Add Webhook</div>
<div className="text-sm text-slate-500">Send survey response data to a custom endpoint</div>
</div>
</div>
</div>
</div>
<form onSubmit={handleSubmit(submitWebhook)}>
<div className="flex justify-between rounded-lg p-6">
<div className="w-full space-y-4">
<div className="col-span-1">
<Label htmlFor="name">Name</Label>
<div className="mt-1 flex">
<Input
type="text"
id="name"
{...register("name")}
placeholder="Optional: Label your webhook for easy identification"
/>
</div>
</div>
<div className="col-span-1">
<Label htmlFor="URL">URL</Label>
<div className="mt-1 flex">
<Input
type="url"
id="URL"
value={testEndpointInput}
onChange={(e) => {
setTestEndpointInput(e.target.value);
}}
className={clsx(
endpointAccessible === true
? "border-green-500 bg-green-50"
: endpointAccessible === false
? "border-red-200 bg-red-50"
: endpointAccessible === undefined
? "border-slate-200 bg-white"
: null
)}
placeholder="Paste the URL you want the event to trigger on"
/>
<Button
type="button"
variant="secondary"
loading={hittingEndpoint}
className="ml-2 whitespace-nowrap"
onClick={() => {
handleTestEndpoint(true);
}}>
Test Endpoint
</Button>
</div>
</div>
<div>
<Label htmlFor="Triggers">Triggers</Label>
<TriggerCheckboxGroup
triggers={triggers}
selectedTriggers={selectedTriggers}
onCheckboxChange={handleCheckboxChange}
/>
</div>
<div>
<Label htmlFor="Surveys">Surveys</Label>
<SurveyCheckboxGroup
surveys={surveys}
selectedSurveys={selectedSurveys}
selectedAllSurveys={selectedAllSurveys}
onSelectAllSurveys={handleSelectAllSurveys}
onSelectedSurveyChange={handleSelectedSurveyChange}
/>
</div>
</div>
</div>
<div className="flex justify-end border-t border-slate-200 p-6">
<div className="flex space-x-2">
<Button
type="button"
variant="minimal"
onClick={() => {
setOpenWithStates(false);
}}>
Cancel
</Button>
<Button variant="darkCTA" type="submit" loading={creatingWebhook}>
Add Webhook
</Button>
</div>
</div>
</form>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,7 @@
import { TPipelineTrigger } from "@formbricks/types/v1/pipelines";
export const triggers = [
{ title: "Response Created", value: "responseCreated" as TPipelineTrigger },
{ title: "Response Updated", value: "responseUpdated" as TPipelineTrigger },
{ title: "Response Finished", value: "responseFinished" as TPipelineTrigger },
];

View File

@@ -0,0 +1,63 @@
import React from "react";
import { Checkbox } from "@formbricks/ui";
import { TSurvey } from "@formbricks/types/v1/surveys";
interface SurveyCheckboxGroupProps {
surveys: TSurvey[];
selectedSurveys: string[];
selectedAllSurveys: boolean;
onSelectAllSurveys: () => void;
onSelectedSurveyChange: (surveyId: string) => void;
}
export const SurveyCheckboxGroup: React.FC<SurveyCheckboxGroupProps> = ({
surveys,
selectedSurveys,
selectedAllSurveys,
onSelectAllSurveys,
onSelectedSurveyChange,
}) => {
return (
<div className="mt-1 rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
<div className="my-1 flex items-center space-x-2">
<Checkbox
type="button"
id="allSurveys"
className="bg-white"
value=""
checked={selectedAllSurveys}
onCheckedChange={onSelectAllSurveys}
/>
<label
htmlFor="allSurveys"
className={`flex cursor-pointer items-center ${selectedAllSurveys ? "font-semibold" : ""}`}>
All current and new surveys
</label>
</div>
{surveys.map((survey) => (
<div key={survey.id} className="my-1 flex items-center space-x-2">
<Checkbox
type="button"
id={survey.id}
value={survey.id}
className="bg-white"
checked={selectedSurveys.includes(survey.id) && !selectedAllSurveys}
disabled={selectedAllSurveys}
onCheckedChange={() => onSelectedSurveyChange(survey.id)}
/>
<label
htmlFor={survey.id}
className={`flex cursor-pointer items-center ${
selectedAllSurveys ? "cursor-not-allowed opacity-50" : ""
}`}>
{survey.name}
</label>
</div>
))}
</div>
</div>
);
};
export default SurveyCheckboxGroup;

View File

@@ -0,0 +1,41 @@
import React from "react";
import { Checkbox } from "@formbricks/ui";
import { TPipelineTrigger } from "@formbricks/types/v1/pipelines";
interface TriggerCheckboxGroupProps {
triggers: { title: string; value: TPipelineTrigger }[];
selectedTriggers: TPipelineTrigger[];
onCheckboxChange: (selectedValue: TPipelineTrigger) => void;
}
export const TriggerCheckboxGroup: React.FC<TriggerCheckboxGroupProps> = ({
triggers,
selectedTriggers,
onCheckboxChange,
}) => {
return (
<div className="mt-1 rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
{triggers.map((trigger) => (
<div key={trigger.value} className="my-1 flex items-center space-x-2">
<label htmlFor={trigger.value} className="flex cursor-pointer items-center">
<Checkbox
type="button"
id={trigger.value}
value={trigger.value}
className="bg-white"
checked={selectedTriggers.includes(trigger.value)}
onCheckedChange={() => {
onCheckboxChange(trigger.value);
}}
/>
<span className="ml-2">{trigger.title}</span>
</label>
</div>
))}
</div>
</div>
);
};
export default TriggerCheckboxGroup;

View File

@@ -0,0 +1,47 @@
import ModalWithTabs from "@/components/shared/ModalWithTabs";
import { TWebhook } from "@formbricks/types/v1/webhooks";
import WebhookOverviewTab from "@/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookOverviewTab";
import WebhookSettingsTab from "@/app/(app)/environments/[environmentId]/integrations/webhooks/WebhookSettingsTab";
import { TSurvey } from "@formbricks/types/v1/surveys";
import { Webhook } from "lucide-react";
interface WebhookModalProps {
environmentId: string;
open: boolean;
setOpen: (v: boolean) => void;
webhook: TWebhook;
surveys: TSurvey[];
}
export default function WebhookModal({ environmentId, open, setOpen, webhook, surveys }: WebhookModalProps) {
const tabs = [
{
title: "Overview",
children: <WebhookOverviewTab webhook={webhook} surveys={surveys} />,
},
{
title: "Settings",
children: (
<WebhookSettingsTab
environmentId={environmentId}
webhook={webhook}
surveys={surveys}
setOpen={setOpen}
/>
),
},
];
return (
<>
<ModalWithTabs
open={open}
setOpen={setOpen}
tabs={tabs}
icon={<Webhook />}
label={webhook.name ? webhook.name : webhook.url}
description={""}
/>
</>
);
}

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