diff --git a/.gitpod.yml b/.gitpod.yml index e2a56d9f42..54f05d33a1 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -57,7 +57,7 @@ ports: onOpen: ignore - port: 8025 visibility: public - onOpen: ignore + onOpen: open-browser github: prebuilds: diff --git a/README.md b/README.md index b0455d29b0..c808514cb1 100644 --- a/README.md +++ b/README.md @@ -63,15 +63,53 @@ Formbricks helps you apply best practices from data-driven work and experience m ## 🚀 Getting started +We've got several options depending on your need to help you quickly get started with Formbricks + ### ☁️ Cloud Version Formbricks has a hosted cloud offering with a generous free plan to get you up and running as quickly as possible. To get started, please visit [formbricks.com](https://formbricks.com) ### 🐳 Self-hosted version -Formbricks is available Open-Source under AGPLv3 license. You can host Formbricks on your own servers using Docker without a subscription. To get started with self-hosting, take a look at our [self-hosting docs](https://formbricks.com/docs/self-hosting/deployment). +Formbricks is available Open-Source under AGPLv3 license. You can host Formbricks on your own servers using Docker without a subscription. -(In the future we may develop additional features that aren't in the free Open-Source version) +(In the future we may develop additional features that aren't in the free Open-Source version). + +If you opt for self-hosting Formbricks, here are a few options to consider: + +#### Docker + +To get started with self-hosting with Docker, take a look at our [self-hosting docs](https://formbricks.com/docs/self-hosting/deployment). + +#### Community managed One Click Hosting + +##### Railway + +You can deploy Formbricks on [Railway](https://railway.app) using the button below. + +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/PPDzCd) + +### 👨‍💻 Development + +#### Prerequisites + +Here is what you need to be able to run Formbricks + +- Node.js (Version: >=18.x) +- [Pnpm](https://pnpm.io/) +- [Docker](https://www.docker.com/) - to run PostgreSQL and MailHog + +#### Local Setup + +To get started locally, we've got a [guide to help you](https://formbricks.com/docs/contributing/setup). + +#### Gitpod Setup + +1. Click the button below to open this project in Gitpod. + +2. This will open a fully configured workspace in your browser with all the necessary dependencies already installed. + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/formbricks/formbricks) ## ✍️ Contribution diff --git a/apps/demo/package.json b/apps/demo/package.json index 19d9084eef..c7e9694e9f 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -13,7 +13,7 @@ "dependencies": { "@formbricks/js": "workspace:*", "@heroicons/react": "^2.0.18", - "next": "13.4.19", + "next": "13.5.3", "react": "18.2.0", "react-dom": "18.2.0" }, diff --git a/apps/formbricks-com/app/docs/actions/code/page.mdx b/apps/formbricks-com/app/docs/actions/code/page.mdx index e5e9ced469..178e91b911 100644 --- a/apps/formbricks-com/app/docs/actions/code/page.mdx +++ b/apps/formbricks-com/app/docs/actions/code/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "Code Actions", + title: "Implementing Code Actions in Formbricks | Real-time User Action Tracking", description: - "Integrate code actions in Formbricks using formbricks.track() to trigger surveys based on user actions, like button clicks, for precise insights. All open-source.", + "Dive into the world of Formbricks' code actions. Learn how to seamlessly integrate formbricks.track() method into your codebase, enabling real-time tracking of user actions like button clicks, visiting a specfic URL. Up your survey game with precise and exact triggers.", }; #### Actions diff --git a/apps/formbricks-com/app/docs/actions/no-code/page.mdx b/apps/formbricks-com/app/docs/actions/no-code/page.mdx index fd08b08dae..d0865cf06b 100644 --- a/apps/formbricks-com/app/docs/actions/no-code/page.mdx +++ b/apps/formbricks-com/app/docs/actions/no-code/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "No-Code Actions", + title: "Implementing No-Code Actions in Formbricks | Real-time User Action Tracking", description: - "Utilize Formbricks' No-Code Actions like Page URL, innerText, and CSS Selector for easy survey triggers and enhanced user insights.", + "Discover the power of Formbricks' No-Code Actions. Easily set up triggers based on Page URL, innerText, and CSS Selectors without touching a line of code. Inccrease user engagement and get insights at precise moments in the user journey.", }; #### Actions diff --git a/apps/formbricks-com/app/docs/actions/why/page.mdx b/apps/formbricks-com/app/docs/actions/why/page.mdx index 8d224e0630..06cc11c687 100644 --- a/apps/formbricks-com/app/docs/actions/why/page.mdx +++ b/apps/formbricks-com/app/docs/actions/why/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "What are actions and why are they useful?", + title: "Using Actions in Formbricks | Fine-tuning User Moments", description: - "Actions in Formbricks enable targeted survey displays during specific user journey moments. Enhance user segmentation by tracking actions for granular surveying.", + "Dive deep into how actions in Formbricks help products and teams to engage users at precise moments in their journey. Discover the power of actions, from coding to no-code setups, to refine user targeting and generate richer, more detailed user insights.", }; #### Actions diff --git a/apps/formbricks-com/app/docs/api/api-key-setup/page.mdx b/apps/formbricks-com/app/docs/api/api-key-setup/page.mdx index 04762ec793..bdd4f9962e 100644 --- a/apps/formbricks-com/app/docs/api/api-key-setup/page.mdx +++ b/apps/formbricks-com/app/docs/api/api-key-setup/page.mdx @@ -4,9 +4,9 @@ import AddApiKey from "./add-api-key.webp"; import ApiKeySecret from "./api-key-secret.webp"; export const meta = { - title: "API Key Setup", + title: "Formbricks API Key: Setup and Testing", description: - "Generate, store, and delete personal API keys for secure Formbricks access. Ensure safekeeping to prevent unauthorized account control.", + "This guide provides step-by-step instructions to generate, store, and delete API keys, ensuring safe and authenticated access to your Formbricks account.", }; #### API diff --git a/apps/formbricks-com/app/docs/api/display/page.mdx b/apps/formbricks-com/app/docs/api/display/page.mdx index c552968b1a..18b746b0aa 100644 --- a/apps/formbricks-com/app/docs/api/display/page.mdx +++ b/apps/formbricks-com/app/docs/api/display/page.mdx @@ -1,9 +1,9 @@ import { Fence } from "@/components/shared/Fence"; export const meta = { - title: "Responses API", + title: "Formbricks Public Client API Guide: Manage Survey Displays & Responses", description: - "Explore the Formbricks Public Client API for client-side tasks and integration into your website.", + "Dive deep into Formbricks' Public Client API designed for customisation. This comprehensive guide provides detailed instructions on how to mark surveys as displayed as well as responded for individual persons, ensuring seamless client-side interactions without compromising data security.", }; #### Management API diff --git a/apps/formbricks-com/app/docs/api/overview/page.mdx b/apps/formbricks-com/app/docs/api/overview/page.mdx index 509416380d..58029f9136 100644 --- a/apps/formbricks-com/app/docs/api/overview/page.mdx +++ b/apps/formbricks-com/app/docs/api/overview/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "API Overview", + title: "Formbricks API Overview: Public Client & Management API Breakdown", description: - "Explore Formbricks' APIs: Public Client API for client-side tasks, and Management API for account management with secure API Key authentication.", + "Get a detailed understanding of Formbricks' dual API offerings: the unauthenticated Public Client API optimized for client-side tasks and the secured Management API for advanced account operations. Choose the perfect fit for your integration needs and ensure robust data handling", }; #### API diff --git a/apps/formbricks-com/app/docs/api/people/page.mdx b/apps/formbricks-com/app/docs/api/people/page.mdx index 80a6a1580d..c682733736 100644 --- a/apps/formbricks-com/app/docs/api/people/page.mdx +++ b/apps/formbricks-com/app/docs/api/people/page.mdx @@ -1,9 +1,9 @@ import { Fence } from "@/components/shared/Fence"; export const meta = { - title: "Responses API", + title: "Formbricks People API: Fetch or Create Person Overview", description: - "Explore the Formbricks Public Client API for client-side tasks and integration into your website.", + "Dive into Formbricks' People API within the Public Client API suite, designed to work without authentication requirements. Seamlessly fetch or create a person by their userId and environmentId, optimizing client-side interactions while maintaining data privacy.", }; #### Management API diff --git a/apps/formbricks-com/app/docs/api/responses/page.mdx b/apps/formbricks-com/app/docs/api/responses/page.mdx index 2d4d1840cb..480d506073 100644 --- a/apps/formbricks-com/app/docs/api/responses/page.mdx +++ b/apps/formbricks-com/app/docs/api/responses/page.mdx @@ -1,9 +1,9 @@ import { Fence } from "@/components/shared/Fence"; export const meta = { - title: "Responses API", + title: "Formbricks Responses API Documentation - Manage Your Survey Data Seamlessly", description: - "Explore the Formbricks Public Client API for client-side tasks and integration into your website.", + "Unlock the full potential of Formbricks' Responses API. From fetching to updating survey responses, our comprehensive guide helps you integrate and manage survey data efficiently without compromising security. Ideal for client-side interactions.", }; #### Management API @@ -14,7 +14,7 @@ The Public Client API is designed for the JavaScript SDK and does not require au --- -## List all responses {{ tag: 'GET', label: '/api/v1/responses' }} +## List all responses {{ tag: 'GET', label: '/api/v1/management/responses' }} @@ -39,11 +39,11 @@ The Public Client API is designed for the JavaScript SDK and does not require au - + ```bash {{ title: 'cURL' }} curl --location \ - 'https://app.formbricks.com/api/v1/responses' \ + 'https://app.formbricks.com/api/v1/management/responses' \ --header \ 'x-api-key: ' ``` diff --git a/apps/formbricks-com/app/docs/api/surveys/page.mdx b/apps/formbricks-com/app/docs/api/surveys/page.mdx index c6512b032c..68faa22201 100644 --- a/apps/formbricks-com/app/docs/api/surveys/page.mdx +++ b/apps/formbricks-com/app/docs/api/surveys/page.mdx @@ -1,9 +1,9 @@ import { Fence } from "@/components/shared/Fence"; export const meta = { - title: "Surveys API", + title: "Formbricks Surveys API Documentation - How to Retrieve All Surveys", description: - "Explore the Formbricks Public Client API for client-side tasks and integration into your website.", + "Explore the comprehensive guide to the Formbricks Surveys API. Learn how to effectively retrieve all the surveys in your environment with the necessary headers and API key setup. Includes sample request and response formats.", }; #### Management API @@ -14,7 +14,7 @@ The Survey API currently has one endpoint that allows you to get all the surveys --- -## List all surveys {{ tag: 'GET', label: '/api/v1/surveys' }} +## List all surveys {{ tag: 'GET', label: '/api/v1/management/surveys' }} @@ -32,11 +32,11 @@ The Survey API currently has one endpoint that allows you to get all the surveys - + ```bash {{ title: 'cURL' }} curl --location \ - 'https://app.formbricks.com/api/v1/surveys' \ + 'https://app.formbricks.com/api/v1/management/surveys' \ --header \ 'x-api-key: ' ``` diff --git a/apps/formbricks-com/app/docs/api/webhooks/page.mdx b/apps/formbricks-com/app/docs/api/webhooks/page.mdx index 9cf4659bf0..d0410f78fc 100644 --- a/apps/formbricks-com/app/docs/api/webhooks/page.mdx +++ b/apps/formbricks-com/app/docs/api/webhooks/page.mdx @@ -1,6 +1,6 @@ export const meta = { - title: "Webhook API Overview", - description: "Learn how to use the Formbricks Webhook API.", + title: "Formbricks Webhook API Documentation - List, Retrieve, Create, and Delete Webhooks", + description: "Explore the comprehensive guide to the Formbricks Webhooks API. This is all you need to interact and play with the Formbricks Webhooks and integrate them into any third party app of your choice", }; #### Webhook API diff --git a/apps/formbricks-com/app/docs/attributes/custom-attributes/page.mdx b/apps/formbricks-com/app/docs/attributes/custom-attributes/page.mdx index ae48143908..dd8dd00806 100644 --- a/apps/formbricks-com/app/docs/attributes/custom-attributes/page.mdx +++ b/apps/formbricks-com/app/docs/attributes/custom-attributes/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "Setting attributes with code", + title: "Guide for Setting Custom Attributes | Formbricks Documentation", description: - "Set attributes in code using setAttribute function. Enhance user segmentation, target surveys effectively, and gather valuable insights for better decisions. All open-source.", + "Learn how to set attributes in code using setAttribute function. Enhance user segmentation, target surveys effectively, and gather valuable insights for better decisions. Easily send user-specific details for better survey segmentation and gain deeper insights.", }; #### Attributes diff --git a/apps/formbricks-com/app/docs/attributes/identify-users/page.mdx b/apps/formbricks-com/app/docs/attributes/identify-users/page.mdx index 666462d210..65be451779 100644 --- a/apps/formbricks-com/app/docs/attributes/identify-users/page.mdx +++ b/apps/formbricks-com/app/docs/attributes/identify-users/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "Identifying Users", + title: "User Identification in Formbricks | Enhancing Survey Feedback", description: - "Identify users with Formbricks by setting User ID, email, and custom attributes. Enhance survey targeting and recontacting while maintaining user privacy.", + "A comprehensive guide on identifying users in Formbricks without compromising privacy. Learn how to set User ID, email, and custom attributes to optimize survey targeting, recontact users, and control survey intervals, all while respecting user anonymity.", }; #### Attributes diff --git a/apps/formbricks-com/app/docs/attributes/why/page.mdx b/apps/formbricks-com/app/docs/attributes/why/page.mdx index e5283c4058..b5e681f657 100644 --- a/apps/formbricks-com/app/docs/attributes/why/page.mdx +++ b/apps/formbricks-com/app/docs/attributes/why/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "What are attributes and why are they useful?", + title: "Understanding User Attributes in Formbricks Surveys", description: - "How to use attributes for user segmentation, enhancing survey targeting & results. Improve feedback quality and make data-driven decisions.", + "Dive into the importance of attributes in surveys. Learn how key-value pairs can significantly improve survey targeting, enhance feedback quality, and guide data-driven decisions with Formbricks.", }; #### Attributes diff --git a/apps/formbricks-com/app/docs/best-practices/cancel-subscription/page.mdx b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/page.mdx index 9e54fc4c98..e6d32dc9e3 100644 --- a/apps/formbricks-com/app/docs/best-practices/cancel-subscription/page.mdx +++ b/apps/formbricks-com/app/docs/best-practices/cancel-subscription/page.mdx @@ -11,8 +11,8 @@ import PublishSurvey from "./publish-survey.webp"; import SelectAction from "./select-action.webp"; export const meta = { - title: "Learn from Churn", - description: "To know how to decrease churn, you have to understand it. Use a micro-survey.", + title: "Mastering Churn Surveys with Formbricks | Essential Tips & Steps", + description: "Learn how to effectively utilize Formbricks' Churn Surveys to gain deeper insights into user departures. Dive into a step-by-step guide to craft, trigger, and optimize your churn surveys, ensuring you capture invaluable feedback at critical junctures", }; #### Best Practices diff --git a/apps/formbricks-com/app/docs/best-practices/docs-feedback/page.mdx b/apps/formbricks-com/app/docs/best-practices/docs-feedback/page.mdx index 035128799c..a18955f333 100644 --- a/apps/formbricks-com/app/docs/best-practices/docs-feedback/page.mdx +++ b/apps/formbricks-com/app/docs/best-practices/docs-feedback/page.mdx @@ -11,8 +11,8 @@ import WhenToAsk from "./when-to-ask.webp"; import CopyIds from "./copy-ids.webp"; export const meta = { - title: "Docs Feedback", - description: "Docs Feedback allows you to measure how clear your documentation is.", + title: "Integrate Docs Feedback in Your Website: A Step-by-Step Guide on getting feedback on your Documentation with Formbricks", + description: "Learn the step-by-step process to effectively measure the clarity of your documentation using Formbricks Cloud. Dive into best practices, setting up cloud environments, integrating feedback widgets on your frontend, and connecting to the Formbricks API for a seamless user experience.", }; #### Best Practices diff --git a/apps/formbricks-com/app/docs/best-practices/feature-chaser/page.mdx b/apps/formbricks-com/app/docs/best-practices/feature-chaser/page.mdx index 74db0313a6..68d68cb324 100644 --- a/apps/formbricks-com/app/docs/best-practices/feature-chaser/page.mdx +++ b/apps/formbricks-com/app/docs/best-practices/feature-chaser/page.mdx @@ -10,8 +10,8 @@ import RecontactOptions from "./recontact-options.webp"; import SelectAction from "./select-action.webp"; export const meta = { - title: "Feature Chaser", - description: "Follow up with users who used a specific feature. Gather feedback and improve your product.", + title: "Setting Up Feature Chaser Surveys with Formbricks: A Comprehensive Guide", + description: "Learn how to harness the power of Formbricks to gather targeted user feedback on specific features. Dive deep into creating, triggering, and publishing the Feature Chaser survey to enhance your product with actionable insights for specific users.", }; #### Best Practices diff --git a/apps/formbricks-com/app/docs/best-practices/feedback-box/page.mdx b/apps/formbricks-com/app/docs/best-practices/feedback-box/page.mdx index b7f60c2cf2..635a3f3240 100644 --- a/apps/formbricks-com/app/docs/best-practices/feedback-box/page.mdx +++ b/apps/formbricks-com/app/docs/best-practices/feedback-box/page.mdx @@ -12,8 +12,8 @@ import SelectAction from "./select-feedback-button-action.webp"; import RecontactOptions from "./set-recontact-options.webp"; export const meta = { - title: "Feedback Box", - description: "The Feedback Box gives your users a direct channel to share their feedback and feel heard.", + title: "Implementing the Feedback Box with Formbricks: A Step-by-Step Tutorial", + description: "Unlock user insights effortlessly! Discover how to set up the Feedback Box in your app using Formbricks, allowing your users to provide real-time feedback. Follow our comprehensive guide to enhance user experience and respond rapidly to feedback", }; #### Best Practices diff --git a/apps/formbricks-com/app/docs/best-practices/improve-trial-cr/page.mdx b/apps/formbricks-com/app/docs/best-practices/improve-trial-cr/page.mdx index b3a8e00d5b..ba0dec1876 100644 --- a/apps/formbricks-com/app/docs/best-practices/improve-trial-cr/page.mdx +++ b/apps/formbricks-com/app/docs/best-practices/improve-trial-cr/page.mdx @@ -10,8 +10,8 @@ import RecontactOptions from "./recontact-options.webp"; import SelectAction from "./select-action.webp"; export const meta = { - title: "Improve Trial Conversion", - description: "Understand how to improve the trial conversions to get more paying customers.", + title: "Boost Your Trial Conversion Rates with Formbricks: Comprehensive Guide", + description: "Unlock the secret to converting more trial users into paying customers using Formbricks. Understand insights behind trial cancellations and tailor your offering to fit user needs. Dive into our step-by-step tutorial and improve your conversion strategy today", }; #### Best Practices diff --git a/apps/formbricks-com/app/docs/best-practices/interview-prompt/page.mdx b/apps/formbricks-com/app/docs/best-practices/interview-prompt/page.mdx index 87f72601a7..081928c9ef 100644 --- a/apps/formbricks-com/app/docs/best-practices/interview-prompt/page.mdx +++ b/apps/formbricks-com/app/docs/best-practices/interview-prompt/page.mdx @@ -13,8 +13,8 @@ import RecontactOptions from "./recontact-options.webp"; import SelectAction from "./select-action.webp"; export const meta = { - title: "In-app Interview Prompt", - description: "Invite only power users to schedule an interview with your product team.", + title: "Maximize User Interview Participation with In-app Interview Prompts", + description: "Engage with your power users seamlessly using Formbricks' In-app Interview Prompt. Ditch traditional email invites and experience way more more respondents. Dive into our comprehensive guide on setting up auto-scheduled interviews today and enhance your user understanding", }; #### Best Practices diff --git a/apps/formbricks-com/app/docs/best-practices/pmf-survey/page.mdx b/apps/formbricks-com/app/docs/best-practices/pmf-survey/page.mdx index 96398afbe3..b82839c2dd 100644 --- a/apps/formbricks-com/app/docs/best-practices/pmf-survey/page.mdx +++ b/apps/formbricks-com/app/docs/best-practices/pmf-survey/page.mdx @@ -10,8 +10,8 @@ import RecontactOptions from "./recontact-options.webp"; import SelectAction from "./select-action.webp"; export const meta = { - title: "Product-Market Fit Survey", - description: "The Product-Market Fit survey helps you measure, well, Product-Market Fit (PMF).", + title: "How to Set Up a Product-Market Fit Survey Using Formbricks - Step-by-Step Guide", + description: "Learn to leverage Formbricks to create and implement a Product-Market Fit survey in your web app. Follow our detailed step-by-step guide to measure and understand your PMF effectively. Ensure high data quality, efficient triggers, and actionable insights.", }; #### Best Practices diff --git a/apps/formbricks-com/app/docs/contributing/demo/page.mdx b/apps/formbricks-com/app/docs/contributing/demo/page.mdx index ce29710ed7..e506366cdc 100644 --- a/apps/formbricks-com/app/docs/contributing/demo/page.mdx +++ b/apps/formbricks-com/app/docs/contributing/demo/page.mdx @@ -3,8 +3,8 @@ import Image from "next/image"; import DemoApp from "./demoapp.webp"; export const meta = { - title: "Demo App", - description: "To test in-app surveys, trigger actions and set attributes, you can use the Demo App.", + title: "Formbricks Demo App Guide: Play around with Formbricks", + description: "To test in-app surveys, trigger actions and set attributes, you can use the Demo App. This guide provides hands-on examples of sending both code and no-code actions", }; #### Contributing diff --git a/apps/formbricks-com/app/docs/contributing/introduction/page.mdx b/apps/formbricks-com/app/docs/contributing/introduction/page.mdx index c9d01bcc51..28ce951945 100644 --- a/apps/formbricks-com/app/docs/contributing/introduction/page.mdx +++ b/apps/formbricks-com/app/docs/contributing/introduction/page.mdx @@ -1,6 +1,6 @@ export const meta = { - title: "Contribution Guide", - description: "How to contribute to Formbricks", + title: "Formbricks Open Source Contribution Guide: How to Enhance yourself and Contribute to Formbricks", + description: "Join the Formbricks community and learn how to effectively contribute. From raising issues and feature requests to creating PRs, discover the best practices and communicate with our responsive team on Discord", }; #### Contributing diff --git a/apps/formbricks-com/app/docs/contributing/setup/page.mdx b/apps/formbricks-com/app/docs/contributing/setup/page.mdx index c76e1f265d..8c8afaac59 100644 --- a/apps/formbricks-com/app/docs/contributing/setup/page.mdx +++ b/apps/formbricks-com/app/docs/contributing/setup/page.mdx @@ -1,6 +1,6 @@ export const meta = { - title: "Setup Dev Environment", - description: "Setup a development environment for Formbricks.", + title: "Formbricks Development Setup: Complete Guide to Local Environment Configuration for Dev", + description: "Step-by-step guide to setting up your local development environment for Formbricks. Includes installing essential tools like Node.JS, pnpm, and Docker, and accessing the entire Formbricks stack including the Demo app and the main website", }; #### Contributing diff --git a/apps/formbricks-com/app/docs/contributing/troubleshooting/page.mdx b/apps/formbricks-com/app/docs/contributing/troubleshooting/page.mdx index 7c0879b95b..443ed002d4 100644 --- a/apps/formbricks-com/app/docs/contributing/troubleshooting/page.mdx +++ b/apps/formbricks-com/app/docs/contributing/troubleshooting/page.mdx @@ -5,9 +5,9 @@ import UncaughtPromise from "./uncaught-promise.webp"; import Logout from "./logout.webp"; export const meta = { - title: "Troubleshooting", + title: "Formbricks Troubleshooting Guide: How to Solve & Debug Common Issues", description: - "Formbricks is a complex application in constant development. Sometimes, things don't go as planned. Here are some tips to help you troubleshoot.", + "Facing issues with Formbricks? This troubleshooting guide covers frequently encountered problems, from Prisma migrations to package errors and more. Detailed solutions, accompanied by visual aids, ensure a smoother user experience with Formbricks", }; #### Contributing diff --git a/apps/formbricks-com/app/docs/getting-started/framework-guides/page.mdx b/apps/formbricks-com/app/docs/getting-started/framework-guides/page.mdx index a2a4c1e447..3ee14fc763 100644 --- a/apps/formbricks-com/app/docs/getting-started/framework-guides/page.mdx +++ b/apps/formbricks-com/app/docs/getting-started/framework-guides/page.mdx @@ -6,8 +6,8 @@ import WidgetConnected from "./widget-connected.webp"; import ReactApp from "./react-in-app-survey-app-popup-form.webp"; export const metadata = { - title: "Framework Guides", - description: "Explore all the possible ways you can integrate Formbricks into your application.", + title: "Integrate Formbricks: Comprehensive Framework Guide & Integration Tutorial", + description: "Master the integration of Formbricks into your application with our detailed guides. From HTML to ReactJS, NextJS, and VueJS, get step-by-step instructions and ensure seamless setup.", }; # Framework Guides diff --git a/apps/formbricks-com/app/docs/getting-started/quickstart-in-app-survey/page.mdx b/apps/formbricks-com/app/docs/getting-started/quickstart-in-app-survey/page.mdx index 7d0ef7b023..3bea67630a 100644 --- a/apps/formbricks-com/app/docs/getting-started/quickstart-in-app-survey/page.mdx +++ b/apps/formbricks-com/app/docs/getting-started/quickstart-in-app-survey/page.mdx @@ -14,8 +14,8 @@ import I11 from "./11-survey-logs-in-app-survey-popup.webp"; import ReactApp from "../framework-guides/react-in-app-survey-app-popup-form.webp"; export const meta = { - title: "Collect in app survey responses in 10 minutes", - description: "Get your first in app survey response in 10 minutes.", + title: "Formbricks Quickstart Guide: In-App Surveys Made Simple", + description: "Launch your first in-app survey effortlessly. Dive into our step-by-step guide to set up, integrate, and debug Formbricks in your web app in under 15 minutes.", }; #### Getting Started diff --git a/apps/formbricks-com/app/docs/integrations/google-sheets/connect-with-google.webp b/apps/formbricks-com/app/docs/integrations/google-sheets/connect-with-google.webp new file mode 100644 index 0000000000..782718a973 Binary files /dev/null and b/apps/formbricks-com/app/docs/integrations/google-sheets/connect-with-google.webp differ diff --git a/apps/formbricks-com/app/docs/integrations/google-sheets/delete-connection.webp b/apps/formbricks-com/app/docs/integrations/google-sheets/delete-connection.webp new file mode 100644 index 0000000000..d845f2d0a1 Binary files /dev/null and b/apps/formbricks-com/app/docs/integrations/google-sheets/delete-connection.webp differ diff --git a/apps/formbricks-com/app/docs/integrations/google-sheets/google-connected.webp b/apps/formbricks-com/app/docs/integrations/google-sheets/google-connected.webp new file mode 100644 index 0000000000..30abe13403 Binary files /dev/null and b/apps/formbricks-com/app/docs/integrations/google-sheets/google-connected.webp differ diff --git a/apps/formbricks-com/app/docs/integrations/google-sheets/integrations-tab.webp b/apps/formbricks-com/app/docs/integrations/google-sheets/integrations-tab.webp new file mode 100644 index 0000000000..79e4cf9b19 Binary files /dev/null and b/apps/formbricks-com/app/docs/integrations/google-sheets/integrations-tab.webp differ diff --git a/apps/formbricks-com/app/docs/integrations/google-sheets/link-survey-with-sheet.webp b/apps/formbricks-com/app/docs/integrations/google-sheets/link-survey-with-sheet.webp new file mode 100644 index 0000000000..90e1e02d7b Binary files /dev/null and b/apps/formbricks-com/app/docs/integrations/google-sheets/link-survey-with-sheet.webp differ diff --git a/apps/formbricks-com/app/docs/integrations/google-sheets/link-with-questions.webp b/apps/formbricks-com/app/docs/integrations/google-sheets/link-with-questions.webp new file mode 100644 index 0000000000..767a2f4143 Binary files /dev/null and b/apps/formbricks-com/app/docs/integrations/google-sheets/link-with-questions.webp differ diff --git a/apps/formbricks-com/app/docs/integrations/google-sheets/list-linked-surveys.webp b/apps/formbricks-com/app/docs/integrations/google-sheets/list-linked-surveys.webp new file mode 100644 index 0000000000..cc84c7ca6a Binary files /dev/null and b/apps/formbricks-com/app/docs/integrations/google-sheets/list-linked-surveys.webp differ diff --git a/apps/formbricks-com/app/docs/integrations/google-sheets/page.mdx b/apps/formbricks-com/app/docs/integrations/google-sheets/page.mdx index a407c4d13f..2f974c3130 100644 --- a/apps/formbricks-com/app/docs/integrations/google-sheets/page.mdx +++ b/apps/formbricks-com/app/docs/integrations/google-sheets/page.mdx @@ -1,5 +1,12 @@ import { Fence } from "@/components/shared/Fence"; import { Callout } from "@/components/shared/Callout"; +import IntegrationTab from "./integrations-tab.webp"; +import ConnectWithGoogle from "./connect-with-google.webp"; +import GoogleConnected from "./google-connected.webp"; +import LinkSurveyWithSheet from "./link-survey-with-sheet.webp"; +import LinkWithQuestions from "./link-with-questions.webp"; +import ListLinkedSurveys from "./list-linked-surveys.webp"; +import DeleteConnection from "./delete-connection.webp"; import Image from "next/image"; export const meta = { @@ -18,12 +25,144 @@ The Google Sheets integration allows you to automatically send responses to a Go self-hosted version of Formbricks. +## Formbricks Cloud + +1. Go to the Integrations tab in your [Formbricks Cloud dashboard](https://app.formbricks.com/) and click on the "Connect" button under Google Sheets integration. + +Formbricks Integrations Tab + +2. Now click on the "Connect with Google" button to authenticate yourself with Google. + +Connect Formbricks with your Google + +3. You will now be taken to the Google OAuth page where you can select the Google account you want to use for the integration. + +4. Once you have selected the account and completed the authentication process, you will be taken back to Formbricks Cloud and see the connected status as below: + +Formbricks is now connected with Google + + + +Before the next step, make sure that you have a Formbricks Survey with at least one question and a Google Sheet in the Google account you integrated. + + + +6. Now click on the "Link New Sheet" button to link a new Google Sheet with Formbricks and a modal will open up. + +Link Formbricks with a Google Sheet + +7. Select the Google Sheet you want to link with Formbricks and the Survey. On doing so, you will be asked with what questions' responses you want to feed in the Google Sheet. Select the questions and click on the "Link Sheet" button. + +Select question to link with Google Sheet + +8. On submitting, the modal will close and you will see the linked Google Sheet in the list of linked Google Sheets. + +List of linked Google Sheets + +Congratulations! You have successfully linked a Google Sheet with Formbricks. Now whenever a response is submitted for the linked Survey, it will be automatically added to the linked Google Sheet. + ## Setup in self-hosted Formbricks Enabling the Google Sheets Integration in a self-hosted environment isn't easy and requires a setup using Google Cloud and changing the environment variables of your Formbricks instance. -The environment variables you need to set are: +This process is really complicated and we recommend using Formbricks Cloud for this feature. + +We will first create a Google Cloud Project and then enable the Google Sheets API for it. Then we will create an OAuth Client ID and Client Secret for our Formbricks instance and set them as environment variables. + +1. Go to the [Google Cloud Console](https://console.cloud.google.com/) and **create a new project**. +2. Now select the project you just created and go to the "**APIs & Services**" section. +3. Click on the "**Enable APIs and Services**" button and search for "**Google Sheets API**" and enable it. +4. Now go to the "**OAuth Consent screen**" section and select the **"External" User Type** if you want any Google User to be able to use the integration or select "Internal" if you want only the users of your Google Workspace to be able to use the integration. +5. Now provide it the details such as + - App name (Will **show up in the OAuth modal** when the user is asked to authenticate with Google) + - User support email (ideally should be **your email** for any support queries by the Users or Google) + - Developer contact information (ideally should be **your email** for any **support queries by Google**) +6. Now click on the "Save and Continue" button and you will be taken to the Scopes step. +7. Click on the "**Add or Remove Scopes**" button and add the scopes `https://www.googleapis.com/auth/userinfo.email`, `https://www.googleapis.com/auth/spreadsheets` & `https://www.googleapis.com/auth/drive` and click on the "Update" button: +8. Now Verify the scopes and click on the "Save and Continue" button. +9. Now go to the **"Test Users" section, skip the step**, and click the "Save and Continue" button. +10. You will now be shown a summary of the OAuth Consent Screen. Verify the details and Click on the "**Back to Dashboard**" button. +11. Now go to the "**Credentials**" section and click on the "**Create Credentials**" button and select "**OAuth Client ID**". +12. Select "**Web Application**" as the Application Type and provide it a name (this name will **not be visible** to your end users). +13. Now add your **public facing URL** in the "**Authorized JavaScript Origins**" section: + - https://` +14. Now add the following URL in the "**Authorized redirect URIs**" section and click on the "**Create**" button: + - https://`/api/google-sheet/callback +15. You will now be shown the **Client ID** and **Client Secret**. Copy them and set them as the **environment variables** in your Formbricks instance as: + - `GOOGLE_SHEETS_CLIENT_ID` - Client ID + - `GOOGLE_SHEETS_CLIENT_SECRET` - Client Secret +16. Also use the **same Authorized redirect URI** in the `GOOGLE_SHEETS_REDIRECT_URL` environment variable. + +### By now, your environment variables should include the below ones as well: - `GOOGLE_SHEETS_CLIENT_ID` - `GOOGLE_SHEETS_CLIENT_SECRET` - `GOOGLE_SHEETS_REDIRECT_URL` + +Voila! You have successfully enabled the Google Sheets integration in your self-hosted Formbricks instance. Now you can follow the steps mentioned in the [Formbricks Cloud](#formbricks-cloud) section to link a Google Sheet with Formbricks. + +## Remove Integration with Google Account + +To remove the integration with Google Account, + +1. Visit the Integrations tab in your Formbricks Cloud dashboard. +2. Select "Manage" button in the Google Sheets card. +3. Click on the "Connected with `" just before the "Link new Sheet" button. +4. It will now ask for a confirmation to remove the integration. Click on the "Delete" button to remove the integration. You can always come back and connect again with the same Google Account. + +Delete Google Integration with Formbricks + +## What data do you need? + +- Your **Email ID** for authentication (We use this to identify you) +- Your **Google Sheets Names and IDs** (We fetch this to list and show you the options of choosing a sheet to integrate with) +- Write access to **selected Google Sheet** (The google sheet you choose to integrate it with, we write survey responses to it) + +For the above, we ask for: + +1. **User Email**: To identify you (that's it, nothing else, we're opensource, see this in our codebase [here](https://github.com/formbricks/formbricks/blob/main/apps/web/app/api/google-sheet/callback/route.ts#L47C17-L47C25)) +1. **Google Drive API**: To list all your google sheets (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/services/googleSheet.ts#L13)) +1. **Google Spreadsheet API**: To write to the spreadsheet you select (that's it, nothing else, we're opensource, see this method in our codebase [here](https://github.com/formbricks/formbricks/blob/main/packages/lib/services/googleSheet.ts#L70)) + + + We do not store any other information of yours! We value Privacy more than you and rest assured you're safe + with us! + + +Still struggling or something not working as expected? [Join our Discord!](https://formbricks.com/discord) and we'd be glad to assist you! diff --git a/apps/formbricks-com/app/docs/integrations/make/page.mdx b/apps/formbricks-com/app/docs/integrations/make/page.mdx index 16ac2f922b..3110173fa4 100644 --- a/apps/formbricks-com/app/docs/integrations/make/page.mdx +++ b/apps/formbricks-com/app/docs/integrations/make/page.mdx @@ -15,8 +15,8 @@ import Result from "./result.webp"; import SelectAction from "./select-action.webp"; export const meta = { - title: "Make.com Setup", - description: "Wire up Formbricks with Make and 1000+ other apps", + title: "Formbricks Integration with Make.com: A Step-by-Step Guide", + description: "Discover how to seamlessly integrate Formbricks with Make.com. Dive into our comprehensive guide to set up scenarios, connect with a plethora of apps, and send your survey data to more than 1000 platforms.", }; #### Integrations diff --git a/apps/formbricks-com/app/docs/integrations/n8n/page.mdx b/apps/formbricks-com/app/docs/integrations/n8n/page.mdx index bd55876910..f72cd5a298 100644 --- a/apps/formbricks-com/app/docs/integrations/n8n/page.mdx +++ b/apps/formbricks-com/app/docs/integrations/n8n/page.mdx @@ -19,8 +19,8 @@ import SuccessConnection from "./success-connection.png"; import UpdateQuestionId from "./update-question-id.png"; export const meta = { - title: "n8n Setup", - description: "Wire up Formbricks with n8n and 350+ other apps", + title: "Comprehensive Guide to Integrating Formbricks with n8n", + description: "Unlock the potential of combining Formbricks with n8n for a streamlined workflow experience. Dive into our step-by-step guide and send your survey data effortlessly to 350+ applications. Streamline your data processes now!", }; #### Integrations diff --git a/apps/formbricks-com/app/docs/integrations/zapier/page.mdx b/apps/formbricks-com/app/docs/integrations/zapier/page.mdx index 72f6819949..ec1fa25d83 100644 --- a/apps/formbricks-com/app/docs/integrations/zapier/page.mdx +++ b/apps/formbricks-com/app/docs/integrations/zapier/page.mdx @@ -15,8 +15,8 @@ import UpdateQuestionId from "./update-question-id.webp"; import ZapierMessage from "./zapier-message.webp"; export const meta = { - title: "Zapier Setup", - description: "Wire up Formbricks with Zapier and 5000+ other apps", + title: "Step-by-Step Guide to Integrating Formbricks with Zapier", + description: "Master the integration of Formbricks with Zapier using our detailed guide. Seamlessly connect your surveys to 5000+ apps, automate data transfers, and enhance feedback management. Start optimizing your workflow today.", }; #### Integrations diff --git a/apps/formbricks-com/app/docs/introduction/how-it-works/page.mdx b/apps/formbricks-com/app/docs/introduction/how-it-works/page.mdx index 93850af41f..c78a7878df 100644 --- a/apps/formbricks-com/app/docs/introduction/how-it-works/page.mdx +++ b/apps/formbricks-com/app/docs/introduction/how-it-works/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "How Formbricks works", + title: "Inside Look: Formbricks In-Product Micro-Surveys", description: - "Master in-product micro-surveys with Formbricks: user-friendly form builder, precise targeting, seamless integration, and insightful analytics for SaaS products.", + "Unlock the full potential of Formbricks: From intuitive form-building and event-based triggers to effortless integrations and deep analytics. Master the art of in-product surveys for your SaaS or digital platform.", }; #### Introduction diff --git a/apps/formbricks-com/app/docs/introduction/what-is-formbricks/page.mdx b/apps/formbricks-com/app/docs/introduction/what-is-formbricks/page.mdx index 214b917b34..6bdfa43686 100644 --- a/apps/formbricks-com/app/docs/introduction/what-is-formbricks/page.mdx +++ b/apps/formbricks-com/app/docs/introduction/what-is-formbricks/page.mdx @@ -4,9 +4,9 @@ import { HeroPattern } from "@/components/docs/HeroPattern"; import { Button } from "@/components/docs/Button"; export const metadata = { - title: "Formbricks – Open Source Experience Management", + title: "Formbricks: Privacy-first Experience Management", description: - "Natively embed qualitative user research into your B2B SaaS. Leverage Best Practices for user discovery to increase Product-Market Fit", + "Enhance your product with Formbricks – the leading open-source solution for in-product micro-surveys. Dive deep into user research, amplify product-market fit, and uncover the 'why' behind your analytics.", }; export const sections = [ diff --git a/apps/formbricks-com/app/docs/introduction/why-is-it-better/page.mdx b/apps/formbricks-com/app/docs/introduction/why-is-it-better/page.mdx index 53dcd603da..72caf33763 100644 --- a/apps/formbricks-com/app/docs/introduction/why-is-it-better/page.mdx +++ b/apps/formbricks-com/app/docs/introduction/why-is-it-better/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "Why is Formbricks better?", + title: "Formbricks vs. Generic Survey Tools: A Comparative Insight", description: - "The ultimate survey solution for SaaS, offering in-depth micro-surveys, precise targeting, and seamless integrations. Discover the difference today!", + "Discover how Formbricks excels as a specialized in-product micro-survey platform for SaaS. Get unmatched targeting, seamless integrations, and make informed decisions with our open-source advantage.", }; #### Introduction diff --git a/apps/formbricks-com/app/docs/link-surveys/data-prefilling/page.mdx b/apps/formbricks-com/app/docs/link-surveys/data-prefilling/page.mdx index cbfbde2954..7b5e29c41e 100644 --- a/apps/formbricks-com/app/docs/link-surveys/data-prefilling/page.mdx +++ b/apps/formbricks-com/app/docs/link-surveys/data-prefilling/page.mdx @@ -3,8 +3,8 @@ import Image from "next/image"; import QuestionId from "./question-id.webp"; export const meta = { - title: "Data Prefilling in Link Surveys", - description: "Prefill data in your surveys to make it easier for your users to provide feedback.", + title: "URL Data Prefilling for Link Surveys in Formbricks", + description: "Master the art of data prefilling in Formbricks link surveys. Dive into our guide on how to use URL parameters to prepopulate answers, boosting conversion rates and enhancing user experience. Learn through examples and ensure correct validation for each question type.", }; #### Link Surveys diff --git a/apps/formbricks-com/app/docs/link-surveys/user-identification/page.mdx b/apps/formbricks-com/app/docs/link-surveys/user-identification/page.mdx index 951ea3a55e..3e31fb1cd5 100644 --- a/apps/formbricks-com/app/docs/link-surveys/user-identification/page.mdx +++ b/apps/formbricks-com/app/docs/link-surveys/user-identification/page.mdx @@ -3,9 +3,9 @@ import Image from "next/image"; import PeopleView from "./people-view.webp"; export const meta = { - title: "User Identification in Link Surveys", + title: "Effective User Identification in Formbricks Link Surveys", description: - "Identify users in link surveys via URL parameter. Connect responses to existing users in Formbricks.", + "Discover how to seamlessly connect responses from Formbricks link surveys to existing users in your database. Learn the intricacies of the userId URL parameter to enhance user tracking, profiling, and segmentation, ensuring more personalized interactions and data-driven decisions.", }; #### Link Surveys diff --git a/apps/formbricks-com/app/docs/self-hosting/deployment/page.mdx b/apps/formbricks-com/app/docs/self-hosting/deployment/page.mdx index 301cd2954c..1f2c722b2a 100644 --- a/apps/formbricks-com/app/docs/self-hosting/deployment/page.mdx +++ b/apps/formbricks-com/app/docs/self-hosting/deployment/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "Deploying Formbricks to production", + title: "Comprehensive Guide to Self-Hosting Formbricks", description: - "Utilize Docker-Compose for easy deployment on your machine. Clone the repo, configure settings, and build the image to access the app on localhost.", + "Discover versatile options to deploy Formbricks tailored to your expertise level. From Ubuntu setups using shell scripts, swift Docker deployments, to manual source configurations, harness the flexibility and power of Formbricks to fit your unique hosting needs. Dive in today!", }; #### Self-Hosting diff --git a/apps/formbricks-com/app/docs/self-hosting/docker/page.mdx b/apps/formbricks-com/app/docs/self-hosting/docker/page.mdx index 7182a7167e..0ec213a9f3 100644 --- a/apps/formbricks-com/app/docs/self-hosting/docker/page.mdx +++ b/apps/formbricks-com/app/docs/self-hosting/docker/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "Deploying Formbricks with Docker", + title: "Guide to Deploying Formbricks Using Docker", description: - "Utilize Docker-Compose for easy deployment on your machine. Clone the repo, configure settings, and build the image to access the app on localhost.", + "Step-by-step tutorial on how to effortlessly set up and run Formbricks via Docker. Explore the quick deployment process with Docker-Compose, learn how to update Formbricks, and troubleshoot common issues. Ideal for those looking for a hassle-free Formbricks experience", }; #### Self-Hosting diff --git a/apps/formbricks-com/app/docs/self-hosting/from-source/page.mdx b/apps/formbricks-com/app/docs/self-hosting/from-source/page.mdx index 16b7689c81..89e3cbe2f0 100644 --- a/apps/formbricks-com/app/docs/self-hosting/from-source/page.mdx +++ b/apps/formbricks-com/app/docs/self-hosting/from-source/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "Deploying Formbricks from Source Code", + title: "Formbricks Self-Hosting Guide: Deploy from Source Code for Complete Customization", description: - "Build the Formbricks Image right from the open-sourced codebase and make changes however needed. Deploy it then with the freedom of customizing even the compile-time environment variables.", + "Build the Formbricks Image right from the open-sourced codebase and make changes however needed. Deploy it then with the freedom of customizing even the compile-time environment variables. Gain more control and flexibility by customizing compile-time environment variables and configuring your instance. Dive into our comprehensive guide for building and running Formbricks with Docker.", }; #### Self-Hosting diff --git a/apps/formbricks-com/app/docs/self-hosting/production/page.mdx b/apps/formbricks-com/app/docs/self-hosting/production/page.mdx index 6d8388455c..d9638a8763 100644 --- a/apps/formbricks-com/app/docs/self-hosting/production/page.mdx +++ b/apps/formbricks-com/app/docs/self-hosting/production/page.mdx @@ -1,7 +1,7 @@ export const meta = { - title: "Deploying Formbricks to production", + title: "Step by Step Guide on Deploying Formbricks to Production on Ubuntu", description: - "Utilize Docker-Compose for easy deployment on your machine. Clone the repo, configure settings, and build the image to access the app on localhost.", + "Master the swift deployment of Formbricks on an Ubuntu server with our step-by-step guide. Use a single command to automate Docker, Postgres DB, SSL certificate configuration, and more. Encounter issues? Dive into our troubleshooting steps or join our community on Discord for assistance.", }; #### Self-Hosting diff --git a/apps/formbricks-com/components/shared/HeaderLight.tsx b/apps/formbricks-com/components/shared/HeaderLight.tsx deleted file mode 100644 index cffc8b27e0..0000000000 --- a/apps/formbricks-com/components/shared/HeaderLight.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Button } from "@formbricks/ui"; -import { Popover } from "@headlessui/react"; -import { usePlausible } from "next-plausible"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { FooterLogo } from "./Logo"; - -export default function HeaderLight() { - const plausible = usePlausible(); - const router = useRouter(); - return ( - -
-
- - Formbricks - - -
- -
- - -
-
-
- ); -} diff --git a/apps/formbricks-com/components/shared/NewsletterSignup.tsx b/apps/formbricks-com/components/shared/NewsletterSignup.tsx index 3f6695bf0d..1306d8b375 100644 --- a/apps/formbricks-com/components/shared/NewsletterSignup.tsx +++ b/apps/formbricks-com/components/shared/NewsletterSignup.tsx @@ -5,7 +5,7 @@ import Image from "next/image"; export default function WaitlistForm() { return (
-

Build in public

+

Stay in the loop!

Get all the juicy details of our journey building Formbricks in public 👇
diff --git a/apps/formbricks-com/images/formtribe/dhru.jpeg b/apps/formbricks-com/images/formtribe/dhru.jpeg new file mode 100644 index 0000000000..608d1ecc04 Binary files /dev/null and b/apps/formbricks-com/images/formtribe/dhru.jpeg differ diff --git a/apps/formbricks-com/images/formtribe/formtribe-logo.png b/apps/formbricks-com/images/formtribe/formtribe-logo.png new file mode 100644 index 0000000000..eb9b7e0d6f Binary files /dev/null and b/apps/formbricks-com/images/formtribe/formtribe-logo.png differ diff --git a/apps/formbricks-com/images/formtribe/jojo.jpeg b/apps/formbricks-com/images/formtribe/jojo.jpeg new file mode 100644 index 0000000000..00e3d00e59 Binary files /dev/null and b/apps/formbricks-com/images/formtribe/jojo.jpeg differ diff --git a/apps/formbricks-com/images/formtribe/matti.jpeg b/apps/formbricks-com/images/formtribe/matti.jpeg new file mode 100644 index 0000000000..8c366ea742 Binary files /dev/null and b/apps/formbricks-com/images/formtribe/matti.jpeg differ diff --git a/apps/formbricks-com/images/formtribe/oss-loop.png b/apps/formbricks-com/images/formtribe/oss-loop.png new file mode 100644 index 0000000000..9e0e218c0f Binary files /dev/null and b/apps/formbricks-com/images/formtribe/oss-loop.png differ diff --git a/apps/formbricks-com/images/formtribe/package.jpeg b/apps/formbricks-com/images/formtribe/package.jpeg new file mode 100644 index 0000000000..7956a70e07 Binary files /dev/null and b/apps/formbricks-com/images/formtribe/package.jpeg differ diff --git a/apps/formbricks-com/images/formtribe/pandeyman.jpeg b/apps/formbricks-com/images/formtribe/pandeyman.jpeg new file mode 100644 index 0000000000..cb9c4faa0b Binary files /dev/null and b/apps/formbricks-com/images/formtribe/pandeyman.jpeg differ diff --git a/apps/formbricks-com/images/formtribe/shubham.jpeg b/apps/formbricks-com/images/formtribe/shubham.jpeg new file mode 100644 index 0000000000..44a7410efb Binary files /dev/null and b/apps/formbricks-com/images/formtribe/shubham.jpeg differ diff --git a/apps/formbricks-com/images/formtribe/timeline.png b/apps/formbricks-com/images/formtribe/timeline.png new file mode 100644 index 0000000000..2daf0d6717 Binary files /dev/null and b/apps/formbricks-com/images/formtribe/timeline.png differ diff --git a/apps/formbricks-com/package.json b/apps/formbricks-com/package.json index 1153544b41..1101313dfd 100644 --- a/apps/formbricks-com/package.json +++ b/apps/formbricks-com/package.json @@ -24,14 +24,14 @@ "@mapbox/rehype-prism": "^0.8.0", "@mdx-js/loader": "^2.3.0", "@mdx-js/react": "^2.3.0", - "@next/mdx": "13.4.19", + "@next/mdx": "13.5.3", "@paralleldrive/cuid2": "^2.2.2", "@sindresorhus/slugify": "^2.2.1", "@tailwindcss/typography": "^0.5.10", - "@types/node": "20.6.0", - "@types/react-highlight-words": "^0.16.4", + "@types/node": "20.7.0", + "@types/react-highlight-words": "^0.16.5", "acorn": "^8.10.0", - "autoprefixer": "^10.4.15", + "autoprefixer": "^10.4.16", "clsx": "^2.0.0", "fast-glob": "^3.3.1", "flexsearch": "^0.7.31", @@ -39,13 +39,13 @@ "lottie-web": "^5.12.2", "mdast-util-to-string": "^4.0.0", "mdx-annotations": "^0.1.3", - "next": "13.4.19", + "next": "13.5.3", "next-plausible": "^3.11.1", "next-seo": "^6.1.0", "next-sitemap": "^4.2.3", "next-themes": "^0.2.1", "node-fetch": "^3.3.2", - "prism-react-renderer": "^2.0.6", + "prism-react-renderer": "^2.1.0", "prismjs": "^1.29.0", "react": "18.2.0", "react-dom": "18.2.0", @@ -56,7 +56,7 @@ "remark": "^14.0.3", "remark-gfm": "^3.0.1", "remark-mdx": "^2.3.0", - "sharp": "^0.32.5", + "sharp": "^0.32.6", "shiki": "^0.14.4", "simple-functional-loader": "^1.2.1", "tailwindcss": "^3.3.3", diff --git a/apps/formbricks-com/pages/api/oss-friends/index.ts b/apps/formbricks-com/pages/api/oss-friends/index.ts index 02c82668d8..4a3f61c6e2 100644 --- a/apps/formbricks-com/pages/api/oss-friends/index.ts +++ b/apps/formbricks-com/pages/api/oss-friends/index.ts @@ -98,6 +98,12 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) description: "Open-source monitoring platform with beautiful status pages", href: "https://www.openstatus.dev", }, + { + name: "Papermark", + description: + "Open-Source Docsend Alternative to securely share documents with real-time analytics.", + href: "https://www.papermark.io/", + }, { name: "Requestly", description: diff --git a/apps/formbricks-com/pages/demo/index.tsx b/apps/formbricks-com/pages/demo/index.tsx index 5d54a00df8..2713807fb8 100644 --- a/apps/formbricks-com/pages/demo/index.tsx +++ b/apps/formbricks-com/pages/demo/index.tsx @@ -1,11 +1,11 @@ -import LayoutWaitlist from "@/components/shared/LayoutLight"; import DemoView from "@/components/dummyUI/DemoView"; +import LayoutWaitlist from "@/pages/formtribe/LayoutLight"; export default function DemoPage() { return ( + description="Play around with our pre-defined 30+ templates and them to kick-start your survey & experience management."> ); diff --git a/apps/formbricks-com/pages/docs-feedback/index.tsx b/apps/formbricks-com/pages/docs-feedback/index.tsx index c50f2570c8..113507f244 100644 --- a/apps/formbricks-com/pages/docs-feedback/index.tsx +++ b/apps/formbricks-com/pages/docs-feedback/index.tsx @@ -7,7 +7,7 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation"; export default function DocsFeedbackPage() { return (
diff --git a/apps/formbricks-com/pages/feature-chaser/index.tsx b/apps/formbricks-com/pages/feature-chaser/index.tsx index 78c578c31e..393989fc35 100644 --- a/apps/formbricks-com/pages/feature-chaser/index.tsx +++ b/apps/formbricks-com/pages/feature-chaser/index.tsx @@ -7,8 +7,8 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation"; export default function FeatureChaserPage() { return ( + title="Feature Chaser with Formbricks" + description="Show a survey about a new feature shown only to people who used it and gain insightful data.">
diff --git a/apps/formbricks-com/pages/feedback-box/index.tsx b/apps/formbricks-com/pages/feedback-box/index.tsx index 215ad6fa02..04cb9ce25c 100644 --- a/apps/formbricks-com/pages/feedback-box/index.tsx +++ b/apps/formbricks-com/pages/feedback-box/index.tsx @@ -7,7 +7,7 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation"; export default function FeedbackBoxPage() { return (
diff --git a/apps/formbricks-com/pages/formtribe/HeaderLight.tsx b/apps/formbricks-com/pages/formtribe/HeaderLight.tsx new file mode 100644 index 0000000000..ba45747c0c --- /dev/null +++ b/apps/formbricks-com/pages/formtribe/HeaderLight.tsx @@ -0,0 +1,74 @@ +import Logo from "@/images/formtribe/formtribe-logo.png"; +import { Button, Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui"; +import { Bars3Icon } from "@heroicons/react/24/solid"; +import Image from "next/image"; +import Link from "next/link"; +import { useState } from "react"; + +const navigation = [ + { name: "How it works", href: "#how" }, + { name: "Prizes", href: "#prizes" }, + { name: "Leaderboard", href: "#leaderboard" }, + { name: "FAQ", href: "#faq" }, +]; + +export default function HeaderLight() { + const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false); + + return ( +
+
+ + FormTribe + Formtribe Logo + + + + Star us ⭐ + +
+ + {/* Desktop Menu */} +
+ {navigation.map((navItem) => ( + + {navItem.name} + + ))} + +
+ + {/* Mobile Menu */} +
+ + setMobileNavMenuOpen(!mobileNavMenuOpen)}> + + + + + +
+ {navigation.map((navItem) => ( + +
setMobileNavMenuOpen(false)} + className="flex items-center space-x-2 rounded-md p-2"> + {navItem.name} +
+ + ))} +
+
+
+
+
+ ); +} diff --git a/apps/formbricks-com/components/shared/LayoutLight.tsx b/apps/formbricks-com/pages/formtribe/LayoutLight.tsx similarity index 59% rename from apps/formbricks-com/components/shared/LayoutLight.tsx rename to apps/formbricks-com/pages/formtribe/LayoutLight.tsx index 252544cbd4..a9069a27de 100644 --- a/apps/formbricks-com/components/shared/LayoutLight.tsx +++ b/apps/formbricks-com/pages/formtribe/LayoutLight.tsx @@ -1,5 +1,5 @@ -import Footer from "./Footer"; -import MetaInformation from "./MetaInformation"; +import Footer from "../../components/shared/Footer"; +import MetaInformation from "../../components/shared/MetaInformation"; import HeaderLight from "./HeaderLight"; interface LayoutProps { @@ -10,11 +10,11 @@ interface LayoutProps { export default function Layout({ title, description, children }: LayoutProps) { return ( -
+
{ -
+
{children}
} diff --git a/apps/formbricks-com/pages/formtribe/index.tsx b/apps/formbricks-com/pages/formtribe/index.tsx new file mode 100644 index 0000000000..e6997206e3 --- /dev/null +++ b/apps/formbricks-com/pages/formtribe/index.tsx @@ -0,0 +1,662 @@ +import LayoutLight from "@/pages/formtribe/LayoutLight"; +import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui"; +import Head from "next/head"; +import Image from "next/image"; + +import Dhru from "@/images/formtribe/dhru.jpeg"; +import Jojo from "@/images/formtribe/jojo.jpeg"; +import Matti from "@/images/formtribe/matti.jpeg"; +import OSSLoop from "@/images/formtribe/oss-loop.png"; +import Mac from "@/images/formtribe/package.jpeg"; +import Pandey from "@/images/formtribe/pandeyman.jpeg"; +import Shubham from "@/images/formtribe/shubham.jpeg"; +import Timeline from "@/images/formtribe/timeline.png"; +import { useEffect } from "react"; + +const HowTo = [ + { + step: "1", + header: "Pick an issue from the list below (or start with a side quest)", + }, + { + step: "2", + header: "Comment on the issue to signal that you started working on it.", + }, + { + step: "3", + header: "Join our Discord to ask questions and submit side quests.", + link: "https://formbricks.com/discord", + }, + { + step: "4", + header: "Code and open a PR with your contribution. ", + }, + { + step: "5", + header: "Get your PR merged and collect points.", + }, + { + step: "6", + header: "Tweet about your contribution and tag @formbricks", + }, + { + step: "7", + header: "Solve side quests to increase your chances on the MacBook 👀", + link: "#prizes", + }, +]; + +const SideQuests = [ + { + points: "100 Points:", + quest: "You think you're smart removing the blur to see the side quests first?", + }, + { + points: "150 Points:", + quest: + "You are! Take a screenshot of this and share it in the 'side-quest' channel on Discord to get 100 points.", + }, + { + points: "200 Points:", + quest: "The rest of the side quests will be released on the 1st of October.", + }, + { + points: "250 Points:", + quest: "Follow us on Twitter and join us on Discord to be the first to know!", + }, + { + points: "Pushmaster Prime | +500 Points + Hoodie:", + quest: "Merge the highest amount of Formbricks PRs in October.", + }, + { + points: "Guidance Guru | +500 Points + Hoodie:", + quest: "Most active and helpful in the community helping other contributors.", + }, + { + points: "Buzz Builder Guru | +500 Points + Hoodie:", + quest: "Marketing Genie with great and effective ideas to spread the word about FormTribe", + }, +]; + +const TheDeal = [ + { + os: "100% free", + free: "Unlimited Surveys", + pro: "Custom URL", + }, + { + os: "All features included", + free: "Unlimited Submissions", + pro: "Remove Branding", + }, + { + os: "It's your storage, go nuts!", + free: "Upload Limit 10 MB", + pro: "Unlimited Uploads", + }, + { + os: "Hook up your own Stripe", + free: "Payments with 2% Mark Up", + pro: "Remove Mark Up from Payments", + }, + { + os: "Your server, your rules", + free: "Invite Team Members", + pro: "", + }, + { + os: "The 'Do what you want' plan", + free: "Verify Email before Submission", + pro: "", + }, + { + os: "at this point I'm just filling rows", + free: "Partial Submissions", + pro: "", + }, + { + os: "I should stop", + free: "Custom Thank You Page", + pro: "", + }, + + { + os: "ok one more", + free: "Close Survey after Submission Limit", + pro: "", + }, + { + os: "no flavor like free flavor", + free: "Custom Survey Closed Message", + pro: "", + }, + { + os: "...", + free: "Close Survey on Date", + pro: "", + }, + { + free: "Redirect on Completion", + pro: "", + }, + { + free: "+ all upcoming community-built features", + pro: "", + }, +]; + +const FAQ = [ + { + question: "Is this part of Formbricks or a different tool?", + answer: + "The link survey is part of the Formbricks core product. The code is managed in the Formbricks mono repository.", + }, + { + question: "Why are there only 30ish issues to work on?", + answer: + "We believe in Quality over Quantity. We’re a small team and want to be able to pay enough attention to each of the contributors.", + }, + { + question: "Can everyone win?", + answer: + "There are 64 prizes. Every contributor can only win 1 prize, so there will be 64 winners. Every contributor has a chance to win. The more points you make, the higher your chance to win.", + }, + { + question: "Why do I have to sign a CLA?", + answer: + "To assure this project to be financially viable, we have to be able to relicense the code for enterprise customers and governments. To be able to do so, we are legally obliged to have you sign a CLA.", + }, + { + question: "Where will this be hosted?", + answer: + "We offer a Formbricks Cloud with a generous free plan but you can also easily self-host using Docker.", + }, + { + question: "What is the FormTribe?", + answer: "The FormTribe is what we call our community of contributors.", + }, + { + question: "Why is there a Commercial plan?", + answer: + "The commercial plan is for features who break the OSS WIN-WIN Loop or incur additional cost. We charge 29$ if you want a custom domain, remove Formbricks branding, collect large files in surveys or collect payments. We think that’s fair :)", + }, + { + question: "Are your in app surveys also free forever?", + answer: + "The in app surveys you can run with Formbricks are not part of this Deal. We offer a generous free plan but keep full control over the pricing in the long run. In app surveys are really powerful for products with thousands of users and something has to bring in the dollars.", + }, + { + question: "Do I need to pay duty for the SWAG?", + answer: "No, we cover that.", + }, + { + question: "How long will it take to receive the swag?", + answer: "30-60 days.", + }, + { + question: "How does it work?", + answer: "Here are detailed instructions, please read through them.", + }, + { + question: "What's in it for me?", + answer: "A Macbook Air M2 or AirPods if your lucky, and life long friends for sure.", + }, + { + question: "When is the event happening?", + answer: "1st of October until 31st of October", + }, + { + question: "Can everyone participate?", + answer: + "Yes! Even when you don’t know how to write code you can take part completing side quests. As long as you can open a PR you are very welcome to take part irrespective of your age, gender, nationality, food preferences, taste in clothing and favorite Pokemon.", + }, +]; + +export default function FormTribeHackathon() { + // dark mode fix + useEffect(() => { + document.documentElement.classList.remove("dark"); + }, []); + return ( + + + + + {/* Header */} + +
+ + Write code, win a Macbook 🔥 + +

+ Let's ship Open Source Typeform in Hacktoberfest +

+ +

+ Can we build an open source Typeform alternative in 30 days? +

+
+ + {/* Video + Nutshell */} +
+ {/* Left Column: YouTube Video */} +
+ +
+ + {/* Right Column: Headline + Ordered List */} +
+
+

In a nutshell

+
    +
  1. + As a community, we will ship all link survey features for a Typeform like + experience in 30 days 🚢 +
  2. +
  3. + All code and non-code contributors have a chance to win a MacBook Air M2 + 💻 +
  4. +
  5. + The link surveys will be 100% free to use - from the community, for the + community 🫶 +
  6. +
+ +
+
+
+ + {/* Note */} +
+
+
+
+
+
+
+
+
+

What is this? (And are we 🥜?)

+

+ Charlie Munger famously said “Show me the incentives and I show you the outcome”. +

+ +

+ The beauty of Open Source Software (and the reason for its inevitable domination) is that + incentives between the different groups of users and developers are perfectly aligned. +

+ +

Let’s have a look:

+ + oss loop + +

With open-source software, everyone wins:

+ +

+ The community of contributors gets to learn on the job working on a real product with real users. + The free users get to use a feature-complete product free of charge. we don’t have to pay insane + sales people salaries to sell our enterprise solution in the future.{" "} + Welcome to the OSS win-win loop™ +

+ +

+ This is why we have decided to spend the complete month of Hacktoberfest hacking away with you, + our community! +

+ +

But why would you hack with us?

+ +

There’s a couple of reasons, but let’s first nail this down:

+ +

+ The time for an elegant 👏, open source 👏 survey builder has + come. It has been tried before, but it has never been maintained long enough because it’s hard to + make money with a free tool. +

+ +

This is why the FormTribe is coming together this October!

+ +

And you can be a part of it!

+ +

+ Imagine yourself flexing on a date that your code is used by thousands and soon millions of + people! +

+ +

+ Apart from you dating life levelling up, here are some proper reasons: +

+

+ +
    +
  • + You can win a new MacBook Air M2 💻 +
  • +
  • + You can win limited hoodies, t-shirts and stickers! +
  • +
  • + You can complete official Hacktoberfest PRs +
  • +
  • You’ll earn your spot of honour on our Community page on formbricks.com
  • +
  • + You’ll be working closely with the Formbricks team, shipping features which have been requested + since this was called snoopForms +
  • +
  • You’ll join an active community of open source fans, connect and learn together
  • +
+

+

Doesn’t this sound like fun?

+ +

It does! And I almost forgot the most important part:

+ +

The grande finale is a ProductHunt launch 🚀

+ +

What are you waiting for?

+ +

Roll up your sleeves, pick one of the issues linked below and join us!

+

+ Matti, Johannes, Anshuman, Shubham & Dhruwang +

+
+ matti + jojo + pandey + shubh + dhru +
+
+
+ + {/* Breaker 1 */} + + + {/* Prizes */} + +
+ macbook air m2 +
+
    +
  • 🎉 1 x MacBook Air M2
  • +
  • 🎉 3 x Limited FormTribe Premium Hoodie
  • +
  • 🎉 10 x Limited FormTribe Premium Shirt
  • +
  • 🎉 50 x Sets of Formbricks Stickers
  • +
+
+
+ +
+
+
🍀
+
+
+

+ Every participant can win! How? +

+

+ For every point you make, your name will be added to a virtual jar. From this jar we will draw the + winners. An example: +

+
    +
  • + Lola makes 200 points for a PR and a total of 100 points with side quests. Lola’s name will be + added 300x to the jar.{" "} +
  • +
  • Bricky completes side quests for 50 points, so his name will be added 50x to the jar.
  • +
+

+ In a live stream we will pull a name from the jar for each of the prizes. Since Lola has 6x more + points than Bricky, her chance of winning is 6x higher. +

+
+
+ + {/* Here is how */} +
+ +
+
+ {HowTo.map((offer) => ( +
+
+

+ {offer.step} +

+
+
+ {offer.link ? ( + + {offer.header} + + ) : ( +

{offer.header}

+ )} +
+
+ ))} +
+ +
+
+

No issues released yet.

+ + Join Discord to get notified first. + +
+
+ {/* Side Quests */} +
+

+ 🏰 Side Quests: Increase your chances +

+

+ While code contributions are what gives the most points, everyone gets to bump up their chance of + winning. Here is a list of side quests you can complete:{" "} +

+
+ {SideQuests.map((quest) => ( +
+
+
+

+ {quest.points} {quest.quest} +

+
+
+ ))} +
+
+ {/* The Leaderboard */} + + + + + {/* The Timeline */} + + + timeline + + {/* Breaker 1 */} + + + {/* The Deal */} +
+ +
+
+
Self-hosted
+
Formbricks Cloud
+ + + +
+ Formbricks Cloud Pro{" "} + + Why tho? + +
+
+ + You can always self-host to get all features free. + +
+
+
+ + {TheDeal.map((feature) => ( +
+
{feature.os}
+
{feature.free}
+
{feature.pro}
+
+ ))} +
+
+
+
+ 🤓 +
+
+
+

+ Are Formbricks in-app surveys also free? +

+

+ Just a heads-up: this deal doesn't cover Formbricks' in-app surveys. We've got a + solid free plan, but we've gotta keep some control over pricing to keep things running + long-term. +

+
+
+
+ + {/* FAQ */} +
+

FAQ

+

Anything unclear?

+
+ {FAQ.map((question) => ( +
+
+

{question.question}

+

{question.answer}

+
+
+ ))} +
+
+ + {/* Breaker 3 */} + +
+ ); +} + +const SectionHeading = ({ title, subTitle, description, id }) => { + return ( +
+

{subTitle}

+

+ {title} +

+

{description}

+
+ ); +}; + +const Breaker = ({ icon, title }) => { + return ( +
+
+
+
{icon}
+
+
+

{title}

+

Get notified on launch plus a weekly update:

+ +
+ + +
+
+
+ +
+ +
+ +
+
+ ); +}; diff --git a/apps/formbricks-com/pages/gdpr-guide.mdx b/apps/formbricks-com/pages/gdpr-guide.mdx index 0d07e34522..a854fcd355 100644 --- a/apps/formbricks-com/pages/gdpr-guide.mdx +++ b/apps/formbricks-com/pages/gdpr-guide.mdx @@ -3,6 +3,7 @@ import { Callout } from "@/components/shared/Callout"; export const meta = { title: "How to create a GDPR compliant form", + description: "Steps to understand how GDPR might be needed for you with Formbricks.", }; diff --git a/apps/formbricks-com/pages/gdpr.mdx b/apps/formbricks-com/pages/gdpr.mdx index ff55974cd5..6cdb972785 100644 --- a/apps/formbricks-com/pages/gdpr.mdx +++ b/apps/formbricks-com/pages/gdpr.mdx @@ -3,6 +3,7 @@ import { Callout } from "@/components/shared/Callout"; export const meta = { title: "GDPR FAQs", + description: "Frequently asked questions about GDPR and Formbricks", }; diff --git a/apps/formbricks-com/pages/improve-trial-conversion/index.tsx b/apps/formbricks-com/pages/improve-trial-conversion/index.tsx index cd35cc4b45..3ab56f8e59 100644 --- a/apps/formbricks-com/pages/improve-trial-conversion/index.tsx +++ b/apps/formbricks-com/pages/improve-trial-conversion/index.tsx @@ -7,7 +7,7 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation"; export default function MissedTrialPagePage() { return (
diff --git a/apps/formbricks-com/pages/interview-prompt/index.tsx b/apps/formbricks-com/pages/interview-prompt/index.tsx index e2527a069b..6d760503cd 100644 --- a/apps/formbricks-com/pages/interview-prompt/index.tsx +++ b/apps/formbricks-com/pages/interview-prompt/index.tsx @@ -7,7 +7,7 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation"; export default function InterviewPromptPage() { return (
diff --git a/apps/formbricks-com/pages/learn-from-churn/index.tsx b/apps/formbricks-com/pages/learn-from-churn/index.tsx index b986369566..cf1c6a69c0 100644 --- a/apps/formbricks-com/pages/learn-from-churn/index.tsx +++ b/apps/formbricks-com/pages/learn-from-churn/index.tsx @@ -7,7 +7,7 @@ import BestPracticeNavigation from "@/components/shared/BestPracticeNavigation"; export default function LearnFromChurnPage() { return (
diff --git a/apps/formbricks-com/pages/measure-product-market-fit/index.tsx b/apps/formbricks-com/pages/measure-product-market-fit/index.tsx index f9df4a45ff..55392cf594 100644 --- a/apps/formbricks-com/pages/measure-product-market-fit/index.tsx +++ b/apps/formbricks-com/pages/measure-product-market-fit/index.tsx @@ -15,7 +15,7 @@ import Image from "next/image"; export default function MeasurePMFPage() { return (
diff --git a/apps/formbricks-com/pages/onboarding-segmentation/index.tsx b/apps/formbricks-com/pages/onboarding-segmentation/index.tsx index 1f9388fd65..31e77893c3 100644 --- a/apps/formbricks-com/pages/onboarding-segmentation/index.tsx +++ b/apps/formbricks-com/pages/onboarding-segmentation/index.tsx @@ -6,7 +6,7 @@ import UseCaseHeader from "@/components/shared/UseCaseHeader"; export default function OnboardingSegmentationPage() { return (
diff --git a/apps/formbricks-com/tailwind.config.ts b/apps/formbricks-com/tailwind.config.ts index 8ed8a85ae8..25d3466b7f 100644 --- a/apps/formbricks-com/tailwind.config.ts +++ b/apps/formbricks-com/tailwind.config.ts @@ -1,6 +1,6 @@ import headlessuiPlugin from "@headlessui/tailwindcss"; -import typographyPlugin from "@tailwindcss/typography"; import forms from "@tailwindcss/forms"; +import typographyPlugin from "@tailwindcss/typography"; import { type Config } from "tailwindcss"; import defaultTheme from "tailwindcss/defaultTheme"; import typographyStyles from "./typography"; @@ -64,6 +64,7 @@ export default { fontFamily: { sans: ["Poppins", ...defaultTheme.fontFamily.sans], display: ["Lexend", ...defaultTheme.fontFamily.sans], + kablammo: ["Kablammo", "sans"], }, screens: { xs: "430px", diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 65d82cc1cd..994a16b76e 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -37,3 +37,6 @@ next-env.d.ts # Sentry Auth Token .sentryclirc + +# Google Sheets Token File +token.json diff --git a/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx b/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx index 9569383be8..af6407fd7e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/integrations/page.tsx @@ -6,8 +6,14 @@ import n8nLogo from "@/images/n8n.png"; import MakeLogo from "@/images/make-small.png"; import { Card } from "@formbricks/ui"; import Image from "next/image"; +import { getIntegrations } from "@formbricks/lib/services/integrations"; + +export default async function IntegrationsPage({ params }) { + const integrations = await getIntegrations(params.environmentId); + const containsGoogleSheetIntegration = integrations.some( + (integration) => integration.type === "googleSheets" + ); -export default function IntegrationsPage({ params }) { return (

Integrations

@@ -45,7 +51,7 @@ export default function IntegrationsPage({ params }) { /> person.attributes[attributeName]?.toString(); -export default async function PeoplePage({ params }) { - const people = await getPeople(params.environmentId); +export default async function PeoplePage({ + params, + searchParams, +}: { + params: { environmentId: string }; + searchParams: { [key: string]: string | string[] | undefined }; +}) { + const pageNumber = searchParams.page ? parseInt(searchParams.page as string) : 1; + const totalPeople = await getPeopleCount(params.environmentId); + const maxPageNumber = Math.ceil(totalPeople / PEOPLE_PER_PAGE); + let hidePagination = false; + + let people: TPerson[] = []; + + if (pageNumber < 1 || pageNumber > maxPageNumber) { + people = []; + hidePagination = true; + } else { + people = await getPeople(params.environmentId, pageNumber); + } return ( <> @@ -64,6 +82,14 @@ export default async function PeoplePage({ params }) { ))}
)} + {hidePagination ? null : ( + + )} ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/people/pagination.tsx b/apps/web/app/(app)/environments/[environmentId]/people/pagination.tsx new file mode 100644 index 0000000000..f215893e10 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/people/pagination.tsx @@ -0,0 +1,57 @@ +function Pagination({ environmentId, currentPage, totalItems, itemsPerPage }) { + const totalPages = Math.ceil(totalItems / itemsPerPage); + + const previousPageLink = + currentPage === 1 ? "#" : `/environments/${environmentId}/people?page=${currentPage - 1}`; + const nextPageLink = + currentPage === totalPages ? "#" : `/environments/${environmentId}/people?page=${currentPage + 1}`; + + return ( + + ); +} + +export default Pagination; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.tsx new file mode 100644 index 0000000000..2d68a0e88e --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys.tsx @@ -0,0 +1,32 @@ +import React from "react"; +import { Unplug } from "lucide-react"; +import { Button } from "@formbricks/ui"; +import Link from "next/link"; + +type TEmptyInAppSurveysProps = { + environmentId: string; +}; + +const EmptyInAppSurveys = ({ environmentId }: TEmptyInAppSurveysProps) => { + return ( +
+
+ +
+ +
+

You're not plugged in yet!

+ +

Connect your app with Formbricks to run in app surveys.

+ + + + +
+
+ ); +}; + +export default EmptyInAppSurveys; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/RatingResponse.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/RatingResponse.tsx similarity index 100% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/RatingResponse.tsx rename to apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/RatingResponse.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/ResponsesLimitReachedBanner.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/ResponsesLimitReachedBanner.tsx similarity index 100% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/ResponsesLimitReachedBanner.tsx rename to apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/ResponsesLimitReachedBanner.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/SurveyResultsTabs.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyResultsTabs.tsx similarity index 100% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/SurveyResultsTabs.tsx rename to apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyResultsTabs.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseNote.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseNote.tsx similarity index 100% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseNote.tsx rename to apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseNote.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponsePage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx similarity index 93% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponsePage.tsx rename to apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx index 2f69fc9954..2c1ace7291 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponsePage.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponsePage.tsx @@ -1,8 +1,8 @@ "use client"; import CustomFilter from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/CustomFilter"; import SummaryHeader from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/SummaryHeader"; -import SurveyResultsTabs from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/SurveyResultsTabs"; -import ResponseTimeline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseTimeline"; +import SurveyResultsTabs from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/SurveyResultsTabs"; +import ResponseTimeline from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline"; import ContentWrapper from "@/components/shared/ContentWrapper"; import { useResponseFilter } from "@/app/(app)/environments/[environmentId]/ResponseFilterContext"; import { getFilterResponses } from "@/lib/surveys/surveys"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseTagsWrapper.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTagsWrapper.tsx similarity index 98% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseTagsWrapper.tsx rename to apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTagsWrapper.tsx index 3b34a590e9..dcb1ad55e8 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseTagsWrapper.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTagsWrapper.tsx @@ -1,6 +1,6 @@ "use client"; -import TagsCombobox from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/TagsCombobox"; +import TagsCombobox from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/TagsCombobox"; import { removeTagFromResponse, useAddTagToResponse, useCreateTag } from "@/lib/tags/mutateTags"; import { useTagsForEnvironment } from "@/lib/tags/tags"; import React, { useEffect, useState } from "react"; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseTimeline.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline.tsx similarity index 87% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseTimeline.tsx rename to apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline.tsx index 2bb820ab6c..989b380312 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/ResponseTimeline.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/responses/components/ResponseTimeline.tsx @@ -5,6 +5,7 @@ import { TSurvey } from "@formbricks/types/v1/surveys"; import { createId } from "@paralleldrive/cuid2"; import { useMemo } from "react"; import SingleResponse from "./SingleResponse"; +import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys"; interface ResponseTimelineProps { environmentId: string; @@ -40,7 +41,7 @@ export default function ResponseTimeline({ // iterate over survey questions and build the updated response for (const question of survey.questions) { const answer = response.data[question.id]; - if (answer) { + if (answer !== null && answer !== undefined) { updatedResponse.push({ id: createId(), question: question.headline, @@ -66,7 +67,8 @@ export default function ResponseTimeline({ return (
- {responses.length === 0 ? ( + {survey.type === "web" && responses.length === 0 && } + {survey.type !== "web" && responses.length === 0 ? ( ; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/StatusDropdown.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/StatusDropdown.tsx similarity index 100% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/StatusDropdown.tsx rename to apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/StatusDropdown.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/SuccessMessage.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.tsx similarity index 100% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/SuccessMessage.tsx rename to apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SuccessMessage.tsx diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/SummaryList.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx similarity index 91% rename from apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/SummaryList.tsx rename to apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx index 6940e68cfe..648d663cd0 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/SummaryList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/SummaryList.tsx @@ -1,4 +1,4 @@ -import ConsentSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/ConsentSummary"; +import ConsentSummary from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/components/ConsentSummary"; import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller"; import { QuestionType } from "@formbricks/types/questions"; import type { QuestionSummary } from "@formbricks/types/responses"; @@ -19,6 +19,7 @@ import MultipleChoiceSummary from "./MultipleChoiceSummary"; import NPSSummary from "./NPSSummary"; import OpenTextSummary from "./OpenTextSummary"; import RatingSummary from "./RatingSummary"; +import EmptyInAppSurveys from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/components/EmptyInAppSurveys"; interface SummaryListProps { environmentId: string; @@ -46,7 +47,11 @@ export default function SummaryList({ environmentId, survey, responses }: Summar return ( <>
- {responses.length === 0 ? ( + {survey.type === "web" && responses.length === 0 && ( + + )} + + {survey.type !== "web" && responses.length === 0 ? ( { - if (survey?.questions?.length > 0) { - setActiveQuestionId(survey.questions[0].id); + if (localSurvey && localSurvey.questions?.length > 0) { + setActiveQuestionId(localSurvey.questions[0].id); } }, [localSurvey?.type]); diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/WhenToSendCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/WhenToSendCard.tsx index 391c2f4fc7..5b46da1f36 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/WhenToSendCard.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/WhenToSendCard.tsx @@ -96,7 +96,6 @@ export default function WhenToSendCard({ setLocalSurvey(updatedSurvey); }; useEffect(() => { - console.log(actionClassArray); if (activeIndex !== null) { setTriggerEvent(activeIndex, actionClassArray[actionClassArray.length - 1].id); } diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx index 0e0d19e320..92dc1a7199 100644 --- a/apps/web/app/(app)/layout.tsx +++ b/apps/web/app/(app)/layout.tsx @@ -5,6 +5,7 @@ import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import { Suspense } from "react"; import PosthogIdentify from "./PosthogIdentify"; +import { NoMobileOverlay } from "@formbricks/ui"; export default async function AppLayout({ children }) { const session = await getServerSession(authOptions); @@ -14,6 +15,7 @@ export default async function AppLayout({ children }) { return ( <> + diff --git a/apps/web/app/(auth)/layout.tsx b/apps/web/app/(auth)/layout.tsx index 950a83c444..56ae20fcdd 100644 --- a/apps/web/app/(auth)/layout.tsx +++ b/apps/web/app/(auth)/layout.tsx @@ -1,9 +1,11 @@ import { PHProvider, PostHogPageview } from "../PostHogClient"; import { Suspense } from "react"; +import { NoMobileOverlay } from "@formbricks/ui"; export default function AppLayout({ children }) { return ( <> + diff --git a/apps/web/app/api/v1/auth.ts b/apps/web/app/api/v1/auth.ts new file mode 100644 index 0000000000..2234d081f7 --- /dev/null +++ b/apps/web/app/api/v1/auth.ts @@ -0,0 +1,39 @@ +import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey"; +import { TAuthenticationApiKey } from "@formbricks/types/v1/auth"; +import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; +import { responses } from "@/lib/api/response"; +import { NextResponse } from "next/server"; + +export async function authenticateRequest(request: Request): Promise { + const apiKey = request.headers.get("x-api-key"); + if (apiKey) { + const apiKeyData = await getApiKeyFromKey(apiKey); + if (apiKeyData) { + const authentication: TAuthenticationApiKey = { + type: "apiKey", + environmentId: apiKeyData.environmentId, + }; + return authentication; + } + return null; + } + return null; +} + +export function handleErrorResponse(error: any): NextResponse { + switch (error.message) { + case "NotAuthenticated": + return responses.notAuthenticatedResponse(); + case "Unauthorized": + return responses.unauthorizedResponse(); + default: + if ( + error instanceof DatabaseError || + error instanceof InvalidInputError || + error instanceof ResourceNotFoundError + ) { + return responses.badRequestResponse(error.message); + } + return responses.internalServerErrorResponse("Some error occurred"); + } +} diff --git a/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts b/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts index 0b4abf5967..10cafc9191 100644 --- a/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts +++ b/apps/web/app/api/v1/js/people/[personId]/set-attribute/route.ts @@ -1,11 +1,9 @@ import { getUpdatedState } from "@/app/api/v1/js/sync/lib/sync"; import { responses } from "@/lib/api/response"; import { transformErrorToDetails } from "@/lib/api/validator"; -import { prisma } from "@formbricks/database"; import { createAttributeClass, getAttributeClassByNameCached } from "@formbricks/lib/services/attributeClass"; -import { getPersonCached } from "@formbricks/lib/services/person"; +import { getPersonCached, updatePersonAttribute } from "@formbricks/lib/services/person"; import { ZJsPeopleAttributeInput } from "@formbricks/types/v1/js"; -import { revalidateTag } from "next/cache"; import { NextResponse } from "next/server"; export async function OPTIONS(): Promise { @@ -48,42 +46,13 @@ export async function POST(req: Request, { params }): Promise { } // upsert attribute (update or create) - await prisma.attribute.upsert({ - where: { - attributeClassId_personId: { - attributeClassId: attributeClass.id, - personId, - }, - }, - update: { - value, - }, - create: { - attributeClass: { - connect: { - id: attributeClass.id, - }, - }, - person: { - connect: { - id: personId, - }, - }, - value, - }, - }); - - // revalidate person - revalidateTag(personId); + updatePersonAttribute(personId, attributeClass.id, value); const state = await getUpdatedState(environmentId, personId, sessionId); return responses.successResponse({ ...state }, true); } catch (error) { console.error(error); - return responses.internalServerErrorResponse( - "Unable to complete response. See server logs for details.", - true - ); + return responses.internalServerErrorResponse(`Unable to complete request: ${error.message}`, true); } } diff --git a/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts b/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts new file mode 100644 index 0000000000..8702de259a --- /dev/null +++ b/apps/web/app/api/v1/management/action-classes/[actionClassId]/route.ts @@ -0,0 +1,93 @@ +import { responses } from "@/lib/api/response"; +import { NextResponse } from "next/server"; +import { deleteActionClass, getActionClass, updateActionClass } from "@formbricks/lib/services/actionClass"; +import { TActionClass, ZActionClassInput } from "@formbricks/types/v1/actionClasses"; +import { authenticateRequest } from "@/app/api/v1/auth"; +import { transformErrorToDetails } from "@/lib/api/validator"; +import { TAuthenticationApiKey } from "@formbricks/types/v1/auth"; +import { handleErrorResponse } from "@/app/api/v1/auth"; + +async function fetchAndAuthorizeActionClass( + authentication: TAuthenticationApiKey, + actionClassId: string +): Promise { + const actionClass = await getActionClass(actionClassId); + if (!actionClass) { + return null; + } + if (actionClass.environmentId !== authentication.environmentId) { + throw new Error("Unauthorized"); + } + return actionClass; +} + +export async function GET( + request: Request, + { params }: { params: { actionClassId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const actionClass = await fetchAndAuthorizeActionClass(authentication, params.actionClassId); + if (actionClass) { + return responses.successResponse(actionClass); + } + return responses.notFoundResponse("Action Class", params.actionClassId); + } catch (error) { + return handleErrorResponse(error); + } +} + +export async function PUT( + request: Request, + { params }: { params: { actionClassId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const actionClass = await fetchAndAuthorizeActionClass(authentication, params.actionClassId); + if (!actionClass) { + return responses.notFoundResponse("Action Class", params.actionClassId); + } + const actionCLassUpdate = await request.json(); + const inputValidation = ZActionClassInput.safeParse(actionCLassUpdate); + if (!inputValidation.success) { + return responses.badRequestResponse( + "Fields are missing or incorrectly formatted", + transformErrorToDetails(inputValidation.error) + ); + } + const updatedActionClass = await updateActionClass( + inputValidation.data.environmentId, + params.actionClassId, + inputValidation.data + ); + if (updatedActionClass) { + return responses.successResponse(updatedActionClass); + } + return responses.internalServerErrorResponse("Some error ocured while updating action"); + } catch (error) { + return handleErrorResponse(error); + } +} + +export async function DELETE( + request: Request, + { params }: { params: { actionClassId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const actionClass = await fetchAndAuthorizeActionClass(authentication, params.actionClassId); + if (!actionClass) { + return responses.notFoundResponse("Action Class", params.actionClassId); + } + if (actionClass.type === "automatic") { + return responses.badRequestResponse("Automatic action classes cannot be deleted"); + } + const deletedActionClass = await deleteActionClass(authentication.environmentId, params.actionClassId); + return responses.successResponse(deletedActionClass); + } catch (error) { + return handleErrorResponse(error); + } +} diff --git a/apps/web/app/api/v1/management/action-classes/route.ts b/apps/web/app/api/v1/management/action-classes/route.ts new file mode 100644 index 0000000000..89d0fe72bf --- /dev/null +++ b/apps/web/app/api/v1/management/action-classes/route.ts @@ -0,0 +1,49 @@ +import { responses } from "@/lib/api/response"; +import { DatabaseError } from "@formbricks/types/v1/errors"; +import { authenticateRequest } from "@/app/api/v1/auth"; +import { NextResponse } from "next/server"; +import { TActionClass, ZActionClassInput } from "@formbricks/types/v1/actionClasses"; +import { createActionClass, getActionClasses } from "@formbricks/lib/services/actionClass"; +import { transformErrorToDetails } from "@/lib/api/validator"; + +export async function GET(request: Request) { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const actionClasses: TActionClass[] = await getActionClasses(authentication.environmentId!); + return responses.successResponse(actionClasses); + } catch (error) { + if (error instanceof DatabaseError) { + return responses.badRequestResponse(error.message); + } + throw error; + } +} + +export async function POST(request: Request): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const actionClassInput = await request.json(); + const inputValidation = ZActionClassInput.safeParse(actionClassInput); + + if (!inputValidation.success) { + return responses.badRequestResponse( + "Fields are missing or incorrectly formatted", + transformErrorToDetails(inputValidation.error), + true + ); + } + + const actionClass: TActionClass = await createActionClass( + authentication.environmentId!, + inputValidation.data + ); + return responses.successResponse(actionClass); + } catch (error) { + if (error instanceof DatabaseError) { + return responses.badRequestResponse(error.message); + } + throw error; + } +} diff --git a/apps/web/app/api/v1/management/attribute-classes/[attributeClassId]/route.ts b/apps/web/app/api/v1/management/attribute-classes/[attributeClassId]/route.ts new file mode 100644 index 0000000000..f46f0145f9 --- /dev/null +++ b/apps/web/app/api/v1/management/attribute-classes/[attributeClassId]/route.ts @@ -0,0 +1,93 @@ +import { responses } from "@/lib/api/response"; +import { handleErrorResponse } from "@/app/api/v1/auth"; +import { NextResponse } from "next/server"; +import { + deleteAttributeClass, + getAttributeClass, + updatetAttributeClass, +} from "@formbricks/lib/services/attributeClass"; +import { TAttributeClass, ZAttributeClassUpdateInput } from "@formbricks/types/v1/attributeClasses"; +import { transformErrorToDetails } from "@/lib/api/validator"; +import { authenticateRequest } from "@/app/api/v1/auth"; +import { TAuthenticationApiKey } from "@formbricks/types/v1/auth"; + +async function fetchAndAuthorizeAttributeClass( + authentication: TAuthenticationApiKey, + attributeId: string +): Promise { + const attributeClass = await getAttributeClass(attributeId); + if (!attributeClass) { + return null; + } + if (attributeClass.environmentId !== authentication.environmentId) { + throw new Error("Unauthorized"); + } + return attributeClass; +} + +export async function GET( + request: Request, + { params }: { params: { attributeClassId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const attributeClass = await fetchAndAuthorizeAttributeClass(authentication, params.attributeClassId); + if (attributeClass) { + return responses.successResponse(attributeClass); + } + return responses.notFoundResponse("Attribute Class", params.attributeClassId); + } catch (error) { + return handleErrorResponse(error); + } +} + +export async function DELETE( + request: Request, + { params }: { params: { attributeClassId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const attributeClass = await fetchAndAuthorizeAttributeClass(authentication, params.attributeClassId); + if (!attributeClass) { + return responses.notFoundResponse("Attribute Class", params.attributeClassId); + } + if (attributeClass.type === "automatic") { + return responses.badRequestResponse("Automatic Attribute Classes cannot be deleted"); + } + const deletedAttributeClass = await deleteAttributeClass(params.attributeClassId); + return responses.successResponse(deletedAttributeClass); + } catch (error) { + return handleErrorResponse(error); + } +} + +export async function PUT( + request: Request, + { params }: { params: { attributeClassId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const attributeClass = await fetchAndAuthorizeAttributeClass(authentication, params.attributeClassId); + if (!attributeClass) { + return responses.notFoundResponse("Attribute Class", params.attributeClassId); + } + const attributeClassUpdate = await request.json(); + const inputValidation = ZAttributeClassUpdateInput.safeParse(attributeClassUpdate); + if (!inputValidation.success) { + return responses.badRequestResponse( + "Fields are missing or incorrectly formatted", + transformErrorToDetails(inputValidation.error) + ); + } + const updatedAttributeClass = await updatetAttributeClass(params.attributeClassId, inputValidation.data); + if (updatedAttributeClass) { + return responses.successResponse(updatedAttributeClass); + } + return responses.internalServerErrorResponse("Some error ocured while updating action"); + } catch (error) { + return handleErrorResponse(error); + } +} diff --git a/apps/web/app/api/v1/management/attribute-classes/route.ts b/apps/web/app/api/v1/management/attribute-classes/route.ts new file mode 100644 index 0000000000..ca0b33f2c6 --- /dev/null +++ b/apps/web/app/api/v1/management/attribute-classes/route.ts @@ -0,0 +1,53 @@ +import { responses } from "@/lib/api/response"; +import { DatabaseError } from "@formbricks/types/v1/errors"; +import { authenticateRequest } from "@/app/api/v1/auth"; +import { NextResponse } from "next/server"; +import { transformErrorToDetails } from "@/lib/api/validator"; +import { TAttributeClass, ZAttributeClassInput } from "@formbricks/types/v1/attributeClasses"; +import { createAttributeClass, getAttributeClasses } from "@formbricks/lib/services/attributeClass"; + +export async function GET(request: Request) { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const atributeClasses: TAttributeClass[] = await getAttributeClasses(authentication.environmentId!); + return responses.successResponse(atributeClasses); + } catch (error) { + if (error instanceof DatabaseError) { + return responses.badRequestResponse(error.message); + } + throw error; + } +} + +export async function POST(request: Request): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const attributeClassInput = await request.json(); + const inputValidation = ZAttributeClassInput.safeParse(attributeClassInput); + + if (!inputValidation.success) { + return responses.badRequestResponse( + "Fields are missing or incorrectly formatted", + transformErrorToDetails(inputValidation.error), + true + ); + } + + const attributeClass: TAttributeClass | null = await createAttributeClass( + authentication.environmentId, + inputValidation.data.name, + inputValidation.data.type + ); + if (!attributeClass) { + return responses.internalServerErrorResponse("Failed creating attribute class"); + } + return responses.successResponse(attributeClass); + } catch (error) { + if (error instanceof DatabaseError) { + return responses.badRequestResponse(error.message); + } + throw error; + } +} diff --git a/apps/web/app/api/v1/me/route.ts b/apps/web/app/api/v1/management/me/route.ts similarity index 100% rename from apps/web/app/api/v1/me/route.ts rename to apps/web/app/api/v1/management/me/route.ts diff --git a/apps/web/app/api/v1/management/people/[personId]/route.ts b/apps/web/app/api/v1/management/people/[personId]/route.ts new file mode 100644 index 0000000000..420d62f77c --- /dev/null +++ b/apps/web/app/api/v1/management/people/[personId]/route.ts @@ -0,0 +1,77 @@ +import { authenticateRequest, handleErrorResponse } from "@/app/api/v1/auth"; +import { responses } from "@/lib/api/response"; +import { deletePerson, getPerson } from "@formbricks/lib/services/person"; +import { TAuthenticationApiKey } from "@formbricks/types/v1/auth"; +import { TPerson } from "@formbricks/types/v1/people"; +import { NextResponse } from "next/server"; + +async function fetchAndAuthorizePerson( + authentication: TAuthenticationApiKey, + personId: string +): Promise { + const person = await getPerson(personId); + if (!person) { + return null; + } + if (person.environmentId !== authentication.environmentId) { + throw new Error("Unauthorized"); + } + return person; +} + +export async function GET( + request: Request, + { params }: { params: { personId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const person = await fetchAndAuthorizePerson(authentication, params.personId); + if (person) { + return responses.successResponse(person); + } + return responses.notFoundResponse("Person", params.personId); + } catch (error) { + return handleErrorResponse(error); + } +} + +// Please use the methods provided by the client API to update a person + +/* export async function PUT( + request: Request, + { params }: { params: { personId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + await fetchAndAuthorizePerson(authentication, params.personId); + + const personUpdate = await request.json(); + const inputValidation = ZPersonUpdateInput.safeParse(personUpdate); + if (!inputValidation.success) { + return responses.badRequestResponse( + "Fields are missing or incorrectly formatted", + transformErrorToDetails(inputValidation.error) + ); + } + return responses.successResponse(await updatePerson(params.personId, inputValidation.data)); + } catch (error) { + return handleErrorResponse(error); + } +} */ + +export async function DELETE(request: Request, { params }: { params: { personId: string } }) { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const person = await fetchAndAuthorizePerson(authentication, params.personId); + if (!person) { + return responses.notFoundResponse("Person", params.personId); + } + await deletePerson(params.personId); + return responses.successResponse({ success: "Person deleted successfully" }); + } catch (error) { + return handleErrorResponse(error); + } +} diff --git a/apps/web/app/api/v1/management/people/route.ts b/apps/web/app/api/v1/management/people/route.ts new file mode 100644 index 0000000000..ff36af3654 --- /dev/null +++ b/apps/web/app/api/v1/management/people/route.ts @@ -0,0 +1,35 @@ +import { authenticateRequest } from "@/app/api/v1/auth"; +import { responses } from "@/lib/api/response"; +import { getPeople } from "@formbricks/lib/services/person"; +import { DatabaseError } from "@formbricks/types/v1/errors"; +import { TPerson } from "@formbricks/types/v1/people"; + +export async function GET(request: Request) { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const people: TPerson[] = await getPeople(authentication.environmentId!); + return responses.successResponse(people); + } catch (error) { + if (error instanceof DatabaseError) { + return responses.badRequestResponse(error.message); + } + throw error; + } +} + +// Please use the client API to create a new person + +/* export async function POST(request: Request): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const person: TPerson = await createPerson(authentication.environmentId); + return responses.successResponse(person); + } catch (error) { + if (error instanceof DatabaseError) { + return responses.badRequestResponse(error.message); + } + throw error; + } +} */ diff --git a/apps/web/app/api/v1/management/responses/[responseId]/route.ts b/apps/web/app/api/v1/management/responses/[responseId]/route.ts new file mode 100644 index 0000000000..ac1325f890 --- /dev/null +++ b/apps/web/app/api/v1/management/responses/[responseId]/route.ts @@ -0,0 +1,88 @@ +import { responses } from "@/lib/api/response"; +import { NextResponse } from "next/server"; +import { transformErrorToDetails } from "@/lib/api/validator"; +import { deleteResponse, getResponse, updateResponse } from "@formbricks/lib/services/response"; +import { TResponse, ZResponseUpdateInput } from "@formbricks/types/v1/responses"; +import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper"; +import { getSurvey } from "@formbricks/lib/services/survey"; +import { authenticateRequest } from "@/app/api/v1/auth"; +import { handleErrorResponse } from "@/app/api/v1/auth"; + +async function fetchAndValidateResponse(authentication: any, responseId: string): Promise { + const response = await getResponse(responseId); + if (!response || !(await canUserAccessResponse(authentication, response))) { + throw new Error("Unauthorized"); + } + return response; +} + +const canUserAccessResponse = async (authentication: any, response: TResponse): Promise => { + const survey = await getSurvey(response.surveyId); + if (!survey) return false; + + if (authentication.type === "session") { + return await hasUserEnvironmentAccess(authentication.session.user, survey.environmentId); + } else if (authentication.type === "apiKey") { + return survey.environmentId === authentication.environmentId; + } else { + throw Error("Unknown authentication type"); + } +}; + +export async function GET( + request: Request, + { params }: { params: { responseId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + await fetchAndValidateResponse(authentication, params.responseId); + const response = await fetchAndValidateResponse(authentication, params.responseId); + if (response) { + return responses.successResponse(response); + } + return responses.notFoundResponse("Response", params.responseId); + } catch (error) { + return handleErrorResponse(error); + } +} + +export async function DELETE( + request: Request, + { params }: { params: { responseId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const response = await fetchAndValidateResponse(authentication, params.responseId); + if (!response) { + return responses.notFoundResponse("Response", params.responseId); + } + const deletedResponse = await deleteResponse(params.responseId); + return responses.successResponse(deletedResponse); + } catch (error) { + return handleErrorResponse(error); + } +} + +export async function PUT( + request: Request, + { params }: { params: { responseId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + await fetchAndValidateResponse(authentication, params.responseId); + const responseUpdate = await request.json(); + const inputValidation = ZResponseUpdateInput.safeParse(responseUpdate); + if (!inputValidation.success) { + return responses.badRequestResponse( + "Fields are missing or incorrectly formatted", + transformErrorToDetails(inputValidation.error) + ); + } + return responses.successResponse(await updateResponse(params.responseId, inputValidation.data)); + } catch (error) { + return handleErrorResponse(error); + } +} diff --git a/apps/web/app/api/v1/management/responses/route.ts b/apps/web/app/api/v1/management/responses/route.ts new file mode 100644 index 0000000000..087cc40809 --- /dev/null +++ b/apps/web/app/api/v1/management/responses/route.ts @@ -0,0 +1,20 @@ +import { responses } from "@/lib/api/response"; +import { getEnvironmentResponses } from "@formbricks/lib/services/response"; +import { authenticateRequest } from "@/app/api/v1/auth"; +import { DatabaseError } from "@formbricks/types/v1/errors"; + +export async function GET(request: Request) { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const responseArray = await getEnvironmentResponses(authentication.environmentId!); + return responses.successResponse(responseArray); + } catch (error) { + if (error instanceof DatabaseError) { + return responses.badRequestResponse(error.message); + } + throw error; + } +} + +// Please use the client API to create a new response diff --git a/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts new file mode 100644 index 0000000000..c25e2ec9a1 --- /dev/null +++ b/apps/web/app/api/v1/management/surveys/[surveyId]/route.ts @@ -0,0 +1,78 @@ +import { responses } from "@/lib/api/response"; +import { NextResponse } from "next/server"; +import { getSurvey, updateSurvey, deleteSurvey } from "@formbricks/lib/services/survey"; +import { TSurvey, ZSurveyInput } from "@formbricks/types/v1/surveys"; +import { transformErrorToDetails } from "@/lib/api/validator"; +import { authenticateRequest } from "@/app/api/v1/auth"; +import { handleErrorResponse } from "@/app/api/v1/auth"; + +async function fetchAndAuthorizeSurvey(authentication: any, surveyId: string): Promise { + const survey = await getSurvey(surveyId); + if (!survey) { + return null; + } + if (survey.environmentId !== authentication.environmentId) { + throw new Error("Unauthorized"); + } + return survey; +} + +export async function GET( + request: Request, + { params }: { params: { surveyId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const survey = await fetchAndAuthorizeSurvey(authentication, params.surveyId); + if (survey) { + return responses.successResponse(survey); + } + return responses.notFoundResponse("Survey", params.surveyId); + } catch (error) { + return handleErrorResponse(error); + } +} + +export async function DELETE( + request: Request, + { params }: { params: { surveyId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const survey = await fetchAndAuthorizeSurvey(authentication, params.surveyId); + if (!survey) { + return responses.notFoundResponse("Survey", params.surveyId); + } + const deletedSurvey = await deleteSurvey(params.surveyId); + return responses.successResponse(deletedSurvey); + } catch (error) { + return handleErrorResponse(error); + } +} + +export async function PUT( + request: Request, + { params }: { params: { surveyId: string } } +): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const survey = await fetchAndAuthorizeSurvey(authentication, params.surveyId); + if (!survey) { + return responses.notFoundResponse("Survey", params.surveyId); + } + const surveyUpdate = await request.json(); + const inputValidation = ZSurveyInput.safeParse(surveyUpdate); + if (!inputValidation.success) { + return responses.badRequestResponse( + "Fields are missing or incorrectly formatted", + transformErrorToDetails(inputValidation.error) + ); + } + return responses.successResponse(await updateSurvey(inputValidation.data)); + } catch (error) { + return handleErrorResponse(error); + } +} diff --git a/apps/web/app/api/v1/management/surveys/route.ts b/apps/web/app/api/v1/management/surveys/route.ts new file mode 100644 index 0000000000..1ced6ec5d9 --- /dev/null +++ b/apps/web/app/api/v1/management/surveys/route.ts @@ -0,0 +1,49 @@ +import { responses } from "@/lib/api/response"; +import { authenticateRequest } from "@/app/api/v1/auth"; +import { NextResponse } from "next/server"; +import { transformErrorToDetails } from "@/lib/api/validator"; +import { createSurvey, getSurveys } from "@formbricks/lib/services/survey"; +import { ZSurveyInput } from "@formbricks/types/v1/surveys"; +import { DatabaseError } from "@formbricks/types/v1/errors"; + +export async function GET(request: Request) { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const surveys = await getSurveys(authentication.environmentId!); + return responses.successResponse(surveys); + } catch (error) { + if (error instanceof DatabaseError) { + return responses.badRequestResponse(error.message); + } + throw error; + } +} + +export async function POST(request: Request): Promise { + try { + const authentication = await authenticateRequest(request); + if (!authentication) return responses.notAuthenticatedResponse(); + const surveyInput = await request.json(); + const inputValidation = ZSurveyInput.safeParse(surveyInput); + + if (!inputValidation.success) { + return responses.badRequestResponse( + "Fields are missing or incorrectly formatted", + transformErrorToDetails(inputValidation.error), + true + ); + } + + const environmentId = authentication.environmentId; + const surveyData = { ...inputValidation.data, environmentId: undefined }; + + const survey = await createSurvey(environmentId, surveyData); + return responses.successResponse(survey); + } catch (error) { + if (error instanceof DatabaseError) { + return responses.badRequestResponse(error.message); + } + throw error; + } +} diff --git a/apps/web/app/api/v1/responses/route.ts b/apps/web/app/api/v1/responses/route.ts deleted file mode 100644 index f2e473a042..0000000000 --- a/apps/web/app/api/v1/responses/route.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { responses } from "@/lib/api/response"; -import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey"; -import { getEnvironmentResponses, getSurveyResponses } from "@formbricks/lib/services/response"; -import { headers } from "next/headers"; -import { DatabaseError } from "@formbricks/types/v1/errors"; -import { getSurvey } from "@formbricks/lib/services/survey"; - -export async function GET(request: Request) { - const apiKey = headers().get("x-api-key"); - if (!apiKey) { - return responses.notAuthenticatedResponse(); - } - let apiKeyData; - try { - apiKeyData = await getApiKeyFromKey(apiKey); - if (!apiKeyData) { - return responses.notAuthenticatedResponse(); - } - } catch (error) { - return responses.notAuthenticatedResponse(); - } - - // get surveyId from searchParams - const { searchParams } = new URL(request.url); - const surveyId = searchParams.get("surveyId"); - - // get responses from database - try { - if (!surveyId) { - const environmentResponses = await getEnvironmentResponses(apiKeyData.environmentId); - return responses.successResponse(environmentResponses); - } - // check if survey is part of environment - const survey = await getSurvey(surveyId); - if (!survey) { - return responses.notFoundResponse(surveyId, "survey"); - } - if (survey.environmentId !== apiKeyData.environmentId) { - return responses.notFoundResponse(surveyId, "survey"); - } - // get responses for survey - const surveyResponses = await getSurveyResponses(surveyId); - return responses.successResponse(surveyResponses); - } catch (error) { - if (error instanceof DatabaseError) { - return responses.badRequestResponse(error.message); - } - throw error; - } -} diff --git a/apps/web/app/api/v1/surveys/route.ts b/apps/web/app/api/v1/surveys/route.ts deleted file mode 100644 index cc07462ead..0000000000 --- a/apps/web/app/api/v1/surveys/route.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { responses } from "@/lib/api/response"; -import { DatabaseError } from "@formbricks/types/v1/errors"; -import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey"; -import { getSurveys } from "@formbricks/lib/services/survey"; -import { headers } from "next/headers"; - -export async function GET() { - const apiKey = headers().get("x-api-key"); - if (!apiKey) { - return responses.notAuthenticatedResponse(); - } - let apiKeyData; - try { - apiKeyData = await getApiKeyFromKey(apiKey); - if (!apiKeyData) { - return responses.notAuthenticatedResponse(); - } - } catch (error) { - return responses.notAuthenticatedResponse(); - } - - // get surveys from database - try { - const surveys = await getSurveys(apiKeyData.environmentId); - return responses.successResponse(surveys); - } catch (error) { - if (error instanceof DatabaseError) { - return responses.badRequestResponse(error.message); - } - throw error; - } -} diff --git a/apps/web/components/shared/SurveyStatusDropdown.tsx b/apps/web/components/shared/SurveyStatusDropdown.tsx index ae4f572358..8fd1509fd5 100644 --- a/apps/web/components/shared/SurveyStatusDropdown.tsx +++ b/apps/web/components/shared/SurveyStatusDropdown.tsx @@ -53,6 +53,7 @@ export default function SurveyStatusDropdown({
) : (