diff --git a/.env.docker b/.env.docker index d0c378e4d3..106955bcfb 100644 --- a/.env.docker +++ b/.env.docker @@ -7,7 +7,7 @@ # BASICS # ############ -NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000 +WEBAPP_URL=http://localhost:3000 ############## # DATABASE # @@ -63,25 +63,25 @@ NEXTAUTH_URL=http://localhost:3000 ##################### # Email Verification. If you enable Email Verification you have to setup SMTP-Settings, too. -NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1 +EMAIL_VERIFICATION_DISABLED=1 # Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too. -NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1 +PASSWORD_RESET_DISABLED=1 # Signup. Disable the ability for new users to create an account. -# NEXT_PUBLIC_SIGNUP_DISABLED=1 +# SIGNUP_DISABLED=1 # Team Invite. Disable the ability for invited users to create an account. -# NEXT_PUBLIC_INVITE_DISABLED=1 +# INVITE_DISABLED=1 ########## # Other # ########## # Display privacy policy, imprint and terms of service links in the footer of signup & public pages. -NEXT_PUBLIC_PRIVACY_URL= -NEXT_PUBLIC_TERMS_URL= -NEXT_PUBLIC_IMPRINT_URL= +PRIVACY_URL= +TERMS_URL= +IMPRINT_URL= # Disable Sentry warning SENTRY_IGNORE_API_RESOLUTION_ERROR=1 @@ -90,12 +90,12 @@ SENTRY_IGNORE_API_RESOLUTION_ERROR=1 NEXT_PUBLIC_SENTRY_DSN= # Configure Github Login -NEXT_PUBLIC_GITHUB_AUTH_ENABLED=0 +GITHUB_AUTH_ENABLED=0 GITHUB_ID= GITHUB_SECRET= # Configure Google Login -NEXT_PUBLIC_GOOGLE_AUTH_ENABLED=0 +GOOGLE_AUTH_ENABLED=0 GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= diff --git a/.env.example b/.env.example index 7b3c52feec..11c21391d3 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,7 @@ # BASICS # ############ -NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000 +WEBAPP_URL=http://localhost:3000 ############## # DATABASE # @@ -20,6 +20,7 @@ DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=pu # Cold boots will be faster and you'll be able to scale your DB independently of your app. # @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy # PRISMA_GENERATE_DATAPROXY=true +# i dont think we use it so ask Matti and remove it PRISMA_GENERATE_DATAPROXY= ############### @@ -34,9 +35,6 @@ NEXTAUTH_SECRET=RANDOM_STRING # You do not need the NEXTAUTH_URL environment variable in Vercel. NEXTAUTH_URL=http://localhost:3000 -# If you encounter NEXT_AUTH URL problems this should always be localhost:3000 (or whatever port your app is running on) -# NEXTAUTH_URL_INTERNAL=http://localhost:3000 - ################ # MAIL SETUP # ################ @@ -64,33 +62,33 @@ SMTP_PASSWORD=smtpPassword ##################### # Email Verification. If you enable Email Verification you have to setup SMTP-Settings, too. -# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1 +# EMAIL_VERIFICATION_DISABLED=1 # Password Reset. If you enable Password Reset functionality you have to setup SMTP-Settings, too. -# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1 +# PASSWORD_RESET_DISABLED=1 # Signup. Disable the ability for new users to create an account. -# NEXT_PUBLIC_SIGNUP_DISABLED=1 +# SIGNUP_DISABLED=1 # Team Invite. Disable the ability for invited users to create an account. -# NEXT_PUBLIC_INVITE_DISABLED=1 +# INVITE_DISABLED=1 ########## # Other # ########## # Display privacy policy, imprint and terms of service links in the footer of signup & public pages. -NEXT_PUBLIC_PRIVACY_URL= -NEXT_PUBLIC_TERMS_URL= -NEXT_PUBLIC_IMPRINT_URL= +PRIVACY_URL= +TERMS_URL= +IMPRINT_URL= # Configure Github Login -NEXT_PUBLIC_GITHUB_AUTH_ENABLED=0 +GITHUB_AUTH_ENABLED=0 GITHUB_ID= GITHUB_SECRET= # Configure Google Login -NEXT_PUBLIC_GOOGLE_AUTH_ENABLED=0 +GOOGLE_AUTH_ENABLED=0 GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 338cbcbe7a..c5b00f7561 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -4,42 +4,45 @@ title: "[FEATURE]" labels: enhancement assignees: [] body: -- type: textarea - id: problem-description - attributes: - label: Is your feature request related to a problem? Please describe. - description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - validations: - required: true -- type: textarea - id: solution-description - attributes: - label: Describe the solution you'd like - description: A clear and concise description of what you want to happen. - validations: - required: true -- type: textarea - id: alternate-solution-description - attributes: - label: Describe alternatives you've considered - description: A clear and concise description of any alternative solutions or features you've considered. - validations: - required: false -- type: textarea - id: additional-context - attributes: - label: Additional context - description: Add any other context or screenshots about the feature request here. - validations: - required: false -- type: markdown - id: formbricks-info - attributes: - value: | - ### How we code at Formbricks 🤓 + - type: textarea + id: problem-description + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: true + - type: textarea + id: solution-description + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: alternate-solution-description + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + validations: + required: false + - type: markdown + id: formbricks-info + attributes: + value: | + ### How we code at Formbricks 🤓 - - Everything is type-safe - - All UI components are in the package `formbricks/ui` - - Run `pnpm dev` to find a demo app to test in-app surveys at `localhost:3002` - - We use **chatGPT** to help refactor code. Use our [Formbricks ✨ megaprompt ✨](https://github.com/formbricks/formbricks/blob/main/megaprompt.md) to create the right - context before you write your prompt. + - Follow Best Practices lined out in our [Contributor Docs](https://formbricks.com/docs/contributing/how-we-code) + - First time: Please read our [introductory blog post](https://formbricks.com/blog/join-the-formtribe) + - All UI components are in the package `formbricks/ui` + - Run `pnpm go` to find a demo app to test in-app surveys at `localhost:3002` + - Everything is type-safe + - We use **chatGPT** to help refactor code. Use our [Formbricks ✨ megaprompt ✨](https://github.com/formbricks/formbricks/blob/main/megaprompt.md) to create the right + context before you write your prompt. + - Anything unclear? [Ask in Discord](https://formbricks.com/discord) diff --git a/.gitpod.yml b/.gitpod.yml index 54f05d33a1..3c5941863b 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -32,7 +32,7 @@ tasks: command: | gp sync-await init && cp .env.example .env && - sed -i -r "s#^(NEXT_PUBLIC_WEBAPP_URL=).*#\1 $(gp url 3000)#" .env && + sed -i -r "s#^(WEBAPP_URL=).*#\1 $(gp url 3000)#" .env && sed -i -r "s#^(NEXTAUTH_URL=).*#\1 $(gp url 3000)#" .env && turbo --filter "@formbricks/web" go diff --git a/README.md b/README.md index 65aa009456..c808514cb1 100644 --- a/README.md +++ b/README.md @@ -63,23 +63,48 @@ 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). -### Development +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. @@ -98,13 +123,6 @@ Here are a few options: Please check out [our contribution guide](https://formbricks.com/docs/contributing/introduction) and our [list of open issues](https://github.com/formbricks/formbricks/issues) for more information. -## 🧑‍💻 Community managed One Click Hosting -### Railway -[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/new/template/PPDzCd) - -You can deploy Formbricks on [Railway](https://railway.app) using the button above. - - ## 📆 Contact us Let's have a chat about your survey needs and get you started. 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/gitpod/page.mdx b/apps/formbricks-com/app/docs/contributing/gitpod/page.mdx new file mode 100644 index 0000000000..1128322297 --- /dev/null +++ b/apps/formbricks-com/app/docs/contributing/gitpod/page.mdx @@ -0,0 +1,15 @@ +import Image from "next/image"; + +export const meta = { + title: "Gitpod Setup", + description: + "With one click, you can setup the Formbricks developer environment in your browser using Gitpod", +}; + +### One Click 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) diff --git a/apps/formbricks-com/app/docs/contributing/how-we-code/cors-handling-in-api.webp b/apps/formbricks-com/app/docs/contributing/how-we-code/cors-handling-in-api.webp new file mode 100644 index 0000000000..10ced089d6 Binary files /dev/null and b/apps/formbricks-com/app/docs/contributing/how-we-code/cors-handling-in-api.webp differ diff --git a/apps/formbricks-com/app/docs/contributing/how-we-code/page.mdx b/apps/formbricks-com/app/docs/contributing/how-we-code/page.mdx new file mode 100644 index 0000000000..4ea09e78c6 --- /dev/null +++ b/apps/formbricks-com/app/docs/contributing/how-we-code/page.mdx @@ -0,0 +1,128 @@ +import Image from "next/image"; +import CorsHandling from "./cors-handling-in-api.webp"; + +export const meta = { + title: "Formbricks Code Contribution Guide: Best Practices and Standards", + description: + "Effortlessly Navigate Your Contribution Journey with Formbricks' Coding Guidelines and PR Review Process", +}; + +#### Contributing + +# How we Code at Formbricks + +Thank you for choosing to contribute to Formbricks. Before you start, please familiarize yourself with our coding standards and best practices, as these are key criteria for pull request reviews. Your contributions are greatly valued, and if you have any questions about these guidelines, please don't hesitate to ask. + +**Table of content** + +- Use Typescript types throughout the application +- Always prioritise Server components +- Fetch data only in server components +- Use Server-Action for mutations +- Use service abstraction instead of direct database calls +- Handle authentication and CORS in management APIs +- Always Document API changes +- Constants should be in the packages folder +- Types should be in the packages folder +- How we handle Pull Requests +- Read environment variables from `.env.mjs` + +--- + +## Use Typescript types throughout the application + +The entire codebase is written in TypeScript, and it is crucial that every new piece of code is thoroughly and accurately typed. Instead of resorting to using the `any` type for variables, please ensure that you explicitly specify the appropriate type. + +## Always prioritise Server components + +When it comes to prioritizing the development of our components, our main focus is on building them as server components. This ensures that they are optimized for server-side rendering and can handle any necessary interactivity seamlessly. However, in cases where a component requires client-side interactivity, we are able to adapt it into a client component. If you would like to learn more about the advantages and benefits of server components, we highly recommend reading the comprehensive documentation provided by Next.js, which can be accessed [here](https://nextjs.org/docs/app/building-your-application/rendering/server-components). + +## Fetch data only in server components + +In order to ensure that both data fetching and rendering take place on the server, it is expected that actions to fetch data from the server will be performed within server components. This approach is prioritized as discussed in the previous point, which provides further details on the benefits and importance of server components. + +**Note**: Data fetching is done in the `page.tsx` of the route or the corresponding server component that needs this data. + +## Use Server-Action for mutations + +Server actions are used to perform server actions in client components. For example, a button click (client-side) that should change something in the database. Server actions should be placed in an `actions.ts` file within the specific route to maintain code organization and facilitate efficient development. + +## Use service abstraction instead of direct database calls + +We utilize [prisma](https://www.prisma.io/) as our Object-Relational Mapping (ORM) tool to interact with the database. This implies that when you need to fetch or modify data in the database, you will be utilizing prisma. All prisma calls should be written in the services folder `packages/lib/services`, and before creating a new service, please ensure that one does not already exist. + +## Handle authentication and CORS in management APIs + +We have two APIs: Management API and Client API. + +The public endpoints of the Client API are used by the link survey and `formbricks-js` to send responses and displays to formbricks. Client APIs can be found in `apps/web/app/api/v1/client` + +The Management API offers the same functionality as the management frontend and can be used to create surveys, view responses or change account settings. The endpoints require an api key that the user can obtain in the management frontend. Management APIs can be found in `apps/web/app/api/v1/management`. + +Please keep the following in mind: + +- When dealing with Management APIs always make sure to require authentication via API keys and a sufficient authorization check. +- Make sure to handle CORS requests in any new Client API endpoint you create as these are called from the browser in link surveys or `formbricks-js`. Example below: + +Cors handling within an API + +## Always Document API changes + +It is imperative that any and all modifications or updates made to the client API are thoroughly and comprehensively documented. This documentation should provide clear and detailed information about the nature of the changes, their impact on existing functionality, and any new features or improvements introduced. This practice not only ensures transparency and accountability but also aids developers, both internal and external, in understanding and effectively utilizing the API, ultimately fostering a more robust and user-friendly development ecosystem. + +## Constants should be in the packages folder + +You should store constants in `packages/lib/constants` + +## Types should be in the packages folder + +You should store type in `packages/types/v1` + +## Read environment variables from `.env.mjs` + +Environment variables (`process.env`) shouldn’t be accessed directly but be added in the `.env.mjs` and should be accessed from here. This practice helps us ensure that the variables are typesafe. + +## How we handle Pull Requests + +We have a number of requirements for PRs to ensure they are as easy to review as possible and to ensure that they are up to standard with the code. + +### Code change + +When submitting a pull request, it is important to avoid combining multiple changes or issues into a single PR. By keeping each PR focused on a specific change or issue, it becomes easier and faster to review. Additionally, separating changes into individual PRs makes it easier to test each change independently. This allows for more efficient and thorough testing, ensuring that each change is properly validated before merging. + +### Title & Content + +We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). You should provide a short and concise title. Don’t put something generic (e.g. bug fixes), and instead mention more specifically what your PR achieves, for instance “fix: dropdown not expanding on survey page”. + +For the PR description, you should go into much greater detail about what your PR adds or fixes. Firstly, the description should include a link to any relevant issues or discussions surrounding the feature or bug that your PR addresses. + +### Feature PRs + +Give a functional overview of how your feature works, including how the user can use the feature. Then, share any technical details in an overview of how the PR works (e.g. “Once the user enters their password, the password is hashed using BCrypt and stored in the Users database field”). + +### Bug Fix PRs + +Give an overview of how your PR fixes the bug both as a high-level overview and a technical explanation of what caused the issue and how your PR resolves this. + +Add a short video or screenshots of what your PR achieves. Loom is a great way of sharing short videos however you can upload your videos directly to the PR description. + +### Code Quality & Styling + +All submitted code must match our **[code styling](https://www.notion.so/Code-Styling-65ddc5dd2deb4b28a9876f1f7cc89ca9?pvs=21)** standards. We will reject pull requests that differ significantly from our standardised code styles. + +All code is automatically checked by Github actions, and will notify you if there are any issues with the code that you submit. We require that code passes these quality checks before merging. + +### PR review process + +At the moment Matti is responsible for approving and merging pull requests, so please make sure to request his review when opening the PR and make the changes he requests in order to merge the PR. + +### Making a Pull Request + +- Be sure to **[check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork)** while creating your PR. +- If your PR refers to or fixes an issue, be sure to add **`refs #XXX`** or **`fixes #XXX`** to the PR description. Replacing **`XXX`** with the respective issue number. See more about **[Linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)** . +- Be sure to fill the PR Template accordingly. diff --git a/apps/formbricks-com/app/docs/contributing/introduction/page.mdx b/apps/formbricks-com/app/docs/contributing/introduction/page.mdx index c9d01bcc51..e852110835 100644 --- a/apps/formbricks-com/app/docs/contributing/introduction/page.mdx +++ b/apps/formbricks-com/app/docs/contributing/introduction/page.mdx @@ -1,6 +1,7 @@ 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 @@ -36,3 +37,11 @@ We are currently working on having a clear [Roadmap](https://github.com/formbric But you can also pick a feature that is not already on the roadmap if you think it creates a positive impact for Formbricks. If you are at all unsure, just raise it as an enhancement issue first and tell us that you like to work on it, and we'll very quickly respond. + +## Contributor License Agreement (CLA) + +To be able to keep working on Formbricks over the coming years, we need to collect a CLA from all relevant contributors. + +Please note that we can only get your contribution merged when we have a CLA signed by you. + +To access the CLA form, please click [here](https://formbricks.com/clmyhzfrymr4ko00hycsg1tvx) 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..33e0d1462c 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 @@ -1,12 +1,12 @@ import { GettingStarted } from "@/components/docs/GettingStarted"; -import { BestPractices } from "@/components/docs/BestPractices"; +import BestPractices from "@/components/docs/BestPractices"; 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..137e14b120 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 @@ -29,7 +29,7 @@ Ensure `docker` & `docker compose` are installed on your server/system. Both are -2. **Modify the `.env.docker` file as required by your setup.**
This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings. If you configured your email credentials, you can also comment the following lines to enable email verification (`# NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# NEXT_PUBLIC_PASSWORD_RESET_DISABLED=1`) +2. **Modify the `.env.docker` file as required by your setup.**
This file comes with a basic setup and Formbricks works without making any changes to the file. To enable email sending functionality you need to configure the SMTP settings. If you configured your email credentials, you can also comment the following lines to enable email verification (`# EMAIL_VERIFICATION_DISABLED=1`) and password reset (`# PASSWORD_RESET_DISABLED=1`) ## Editing a NEXT_PUBLIC_* variable? @@ -50,67 +50,64 @@ Ensure `docker` & `docker compose` are installed on your server/system. Both are You can now access the app on [http://localhost:3000](http://localhost:3000). You will be automatically redirected to the login. To use your local installation of Formbricks, create a new account. -### Important Run-time Variables +## Important Run-time Variables These variables must also be provided at runtime. -| Variable | Description | Required | Default | -| ------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------- | --- | -| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` | -| DATABASE_URL | Database URL with credentials. | required | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` | -| PRISMA_GENERATE_DATAPROXY | Enables a dedicated connection pool for Prisma using Prisma Data Proxy. Uncomment to enable. | optional | | -| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) | -| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` | -| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | | -| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | | -| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | | -| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | | -| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | | -| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to `1` else to `0`. | optional (required if email services are to be enabled) | | -| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | | -| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | | -| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | | -| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | | -| CRON_SECRET | API Secret for running cron jobs. | optional | | -| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | | -| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | | -| TELEMETRY_DISABLED | Disables telemetry if set to `1`. | optional | | -| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | | -| INTERNAL_SECRET | Internal Secret (Currently we do not use this anywhere). | optional | | -| RENDER_EXTERNAL_URL | External URL for rendering (used instead of WEBAPP_URL). | optional | | -| HEROKU_APP_NAME | Heroku app name (used instead of WEBAPP_URL). | optional | | -| RAILWAY_STATIC_URL | Railway static URL (used instead of WEBAPP_URL). | optional | | | +| Variable | Description | Required | Default | +| --------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------------- | +| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` | +| SURVEY_BASE_URL | Base URL of the link surveys. | required | `http://localhost:3000/s/` | +| DATABASE_URL | Database URL with credentials. | required | `postgresql://postgres:postgres@postgres:5432/formbricks?schema=public` | +| PRISMA_GENERATE_DATAPROXY | Enables a dedicated connection pool for Prisma using Prisma Data Proxy. Uncomment to enable. | optional | | +| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) | +| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` | +| PRIVACY_URL | URL for privacy policy. | optional | | +| TERMS_URL | URL for terms of service. | optional | | +| IMPRINT_URL | URL for imprint. | optional | | +| SIGNUP_DISABLED | Disables the ability for new users to create an account if set to `1`. | optional | | +| PASSWORD_RESET_DISABLED | Disables password reset functionality if set to `1`. | optional | | +| EMAIL_VERIFICATION_DISABLED | Disables email verification if set to `1`. | optional | | +| INVITE_DISABLED | Disables the ability for invited users to create an account if set to `1`. | optional | | +| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | | +| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | | +| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | | +| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | | +| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | | +| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to `1` else to `0`. | optional (required if email services are to be enabled) | | +| GITHUB_AUTH_ENABLED | Enables GitHub login if set to `1`. | optional | | +| GOOGLE_AUTH_ENABLED | Enables Google login if set to `1`. | optional | | +| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | | +| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | | +| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | | +| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | | +| CRON_SECRET | API Secret for running cron jobs. | optional | | +| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | | +| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | | +| TELEMETRY_DISABLED | Disables telemetry if set to `1`. | optional | | +| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | | +| INTERNAL_SECRET | Internal Secret (Currently we overwrite the value with a random value). | optional | | +| IS_FORMBRICKS_CLOUD | Uses Formbricks Cloud if set to `1` | optional | | +| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | `#64748b` | -### Build-time Variables +## Build-time Variables These variables must be provided at the time of the docker build and can be provided by updating the `.env` file. -| Variable | Description | Required | Default | -| -------------------------------------------------- | -------------------------------------------------------------------------- | -------- | ----------------------- | -| NEXT_PUBLIC_WEBAPP_URL | Base URL injected into static files. | required | `http://localhost:3000` | -| NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED | Disables email verification if set to `1`. | optional | | -| NEXT_PUBLIC_PASSWORD_RESET_DISABLED | Disables password reset functionality if set to `1`. | optional | | -| NEXT_PUBLIC_SIGNUP_DISABLED | Disables the ability for new users to create an account if set to `1`. | optional | | -| NEXT_PUBLIC_INVITE_DISABLED | Disables the ability for invited users to create an account if set to `1`. | optional | | -| NEXT_PUBLIC_PRIVACY_URL | URL for privacy policy. | optional | | -| NEXT_PUBLIC_TERMS_URL | URL for terms of service. | optional | | -| NEXT_PUBLIC_IMPRINT_URL | URL for imprint. | optional | | -| NEXT_PUBLIC_GITHUB_AUTH_ENABLED | Enables GitHub login if set to `1`. | optional | | -| NEXT_PUBLIC_GOOGLE_AUTH_ENABLED | Enables Google login if set to `1`. | optional | | -| NEXT_PUBLIC_FORMBRICKS_API_HOST | Host for the Formbricks API. | optional | | -| NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID | If you already have a preset environment ID of the environment. | optional | | -| NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID | If you already have an onboarding survey ID to show new users. | optional | | -| NEXT_PUBLIC_IS_FORMBRICKS_CLOUD | Uses Formbricks Cloud if set to `1` | optional | | -| NEXT_PUBLIC_POSTHOG_API_KEY | API key for PostHog analytics. | optional | | -| NEXT_PUBLIC_POSTHOG_API_HOST | Host for PostHog analytics. | optional | | -| NEXT_PUBLIC_SENTRY_DSN | DSN for Sentry error tracking. | optional | | -| NEXT_PUBLIC_DOCSEARCH_APP_ID | ID of the DocSearch application. | optional | | -| NEXT_PUBLIC_DOCSEARCH_API_KEY | API key of the DocSearch application. | optional | | -| NEXT_PUBLIC_DOCSEARCH_INDEX_NAME | Name of the DocSearch index. | optional | | -| NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID | Environment ID for Formbricks (if you already have one) | optional | | -| NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID | Survey ID for the feedback survey on the docs site. | optional | | -| NEXT_PUBLIC_FORMBRICKS_COM_API_HOST | Host for the Formbricks API. | optional | | -| NEXT_PUBLIC_VERCEL_URL | Vercel URL (used instead of WEBAPP_URL). | optional | +| Variable | Description | Required | Default | +| -------------------------------------------------- | --------------------------------------------------------------- | -------- | ------- | +| NEXT_PUBLIC_FORMBRICKS_API_HOST | Host for the Formbricks API. | optional | | +| NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID | If you already have a preset environment ID of the environment. | optional | | +| NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID | If you already have an onboarding survey ID to show new users. | optional | | +| NEXT_PUBLIC_POSTHOG_API_KEY | API key for PostHog analytics. | optional | | +| NEXT_PUBLIC_POSTHOG_API_HOST | Host for PostHog analytics. | optional | | +| NEXT_PUBLIC_SENTRY_DSN | DSN for Sentry error tracking. | optional | | +| NEXT_PUBLIC_DOCSEARCH_APP_ID | ID of the DocSearch application. | optional | | +| NEXT_PUBLIC_DOCSEARCH_API_KEY | API key of the DocSearch application. | optional | | +| NEXT_PUBLIC_DOCSEARCH_INDEX_NAME | Name of the DocSearch index. | optional | | +| NEXT_PUBLIC_FORMBRICKS_COM_ENVIRONMENT_ID | Environment ID for Formbricks (if you already have one) | optional | | +| NEXT_PUBLIC_FORMBRICKS_COM_DOCS_FEEDBACK_SURVEY_ID | Survey ID for the feedback survey on the docs site. | optional | | +| NEXT_PUBLIC_FORMBRICKS_COM_API_HOST | Host for the Formbricks API. | optional | | ## Debugging diff --git a/apps/formbricks-com/app/docs/self-hosting/migrating-to-1.1/page.mdx b/apps/formbricks-com/app/docs/self-hosting/migrating-to-1.1/page.mdx new file mode 100644 index 0000000000..7d3ab4f1b2 --- /dev/null +++ b/apps/formbricks-com/app/docs/self-hosting/migrating-to-1.1/page.mdx @@ -0,0 +1,138 @@ +export const meta = { + title: "Migrating Formbricks to v1.1", + description: + "Formbricks v1.1 comes with an amazing set of features including the ability to define most environment variables at runtime itself! No need to build the image again! This guide will help you migrate your existing Formbricks instance to v1.1", +}; + +#### Self-Hosting + +# Migrating to v1.1 + +Formbricks v1.1 includes a lot of new features and improvements. However, it also comes with a few breaking changes specifically with the environment variables. This guide will help you migrate your existing Formbricks instance to v1.1 without losing any data. + +## Changes in .env + +### Renamed Environment Variables +This was introduced because we got a lot of requests from our users for the ability to define some common environment variables at runtime itself i.e. without having to rebuild the image for the changes to take effect. +This is now possible with v1.1. However, due to Next.JS best practices, we had to deprecate the prefix **NEXT_PUBLIC_** in the following environment variables: + +| till v1.0 | v1.1 | +| ------------------------------------------- | --------------------------- | +| **NEXT_PUBLIC_**EMAIL_VERIFICATION_DISABLED | EMAIL_VERIFICATION_DISABLED | +| **NEXT_PUBLIC_**PASSWORD_RESET_DISABLED | PASSWORD_RESET_DISABLED | +| **NEXT_PUBLIC_**SIGNUP_DISABLED | SIGNUP_DISABLED | +| **NEXT_PUBLIC_**INVITE_DISABLED | INVITE_DISABLED | +| **NEXT_PUBLIC_**PRIVACY_URL | PRIVACY_URL | +| **NEXT_PUBLIC_**TERMS_URL | TERMS_URL | +| **NEXT_PUBLIC_**IMPRINT_URL | IMPRINT_URL | +| **NEXT_PUBLIC_**GITHUB_AUTH_ENABLED | GITHUB_AUTH_ENABLED | +| **NEXT_PUBLIC_**GOOGLE_AUTH_ENABLED | GOOGLE_AUTH_ENABLED | +| **NEXT_PUBLIC_**WEBAPP_URL | WEBAPP_URL | +| **NEXT_PUBLIC_**IS_FORMBRICKS_CLOUD | IS_FORMBRICKS_CLOUD | +| **NEXT_PUBLIC_**SURVEY_BASE_URL | SURVEY_BASE_URL | + + +Please note that their values and the logic remains exactly the same. Only the prefix has been deprecated. The other environment variables remain the same as well. + + +### Deprecated Environment Variables + +- **NEXT_PUBLIC_VERCEL_URL**: Was used as Vercel URL (used instead of WEBAPP_URL), but from v1.1, you can just set the WEBAPP_URL environment variable to your Vercel URL. +- **RAILWAY_STATIC_URL**: Was used as Railway Static URL (used instead of WEBAPP_URL), but from v1.1, you can just set the WEBAPP_URL environment variable. +- **RENDER_EXTERNAL_URL**: Was used as an external URL to Render (used instead of WEBAPP_URL), but from v1.1, you can just set the WEBAPP_URL environment variable. +- **HEROKU_APP_NAME**: Was used to build the App name on a Heroku hosted webapp, but from v1.1, you can just set the WEBAPP_URL environment variable. +- **NEXT_PUBLIC_WEBAPP_URL**: Was used for the same purpose as WEBAPP_URL, but from v1.1, you can just set the WEBAPP_URL environment variable. +- **PRISMA_GENERATE_DATAPROXY**: Was used to tell Prisma that it should generate the runtime for Dataproxy usage. But its officially deprecated now. + +## Helper Shell Script +For a seamless migration, below is a shell script for your self-hosted instance that will automatically update your environment variables to be compliant with the new naming conventions. + +### Building From Source +The below script will: +1. Create a backup of your existing .env file as `.env.old` +2. Update the .env file to be compliant with the new naming conventions + + +```shell {{ title: '.env file' }} +for var in NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED NEXT_PUBLIC_PASSWORD_RESET_DISABLED NEXT_PUBLIC_SIGNUP_DISABLED NEXT_PUBLIC_INVITE_DISABLED NEXT_PUBLIC_PRIVACY_URL NEXT_PUBLIC_TERMS_URL NEXT_PUBLIC_IMPRINT_URL NEXT_PUBLIC_GITHUB_AUTH_ENABLED NEXT_PUBLIC_GOOGLE_AUTH_ENABLED NEXT_PUBLIC_WEBAPP_URL NEXT_PUBLIC_IS_FORMBRICKS_CLOUD NEXT_PUBLIC_SURVEY_BASE_URL; do sed -i.old "s/^$var=/$(echo $var | sed 's/NEXT_PUBLIC_//')=/" .env; done; echo "Formbricks environment variables have been migrated as per v1.1! You are good to go." +``` + + + +### Docker & Single Script Setup + +Now that these variables can be defined at runtime, you can append them inside your `x-environment` in the `docker-compose.yml` itself. +For a more detailed guide on these environment variables, please refer to the [Important Runtime Variables](/docs/self-hosting/from-source#important-run-time-variables) section. + + +```yaml {{ title: 'docker-compose.yml' }} +version: "3.3" +x-environment: &environment + environment: + # The url of your Formbricks instance used in the admin panel + WEBAPP_URL: + + # PostgreSQL DB for Formbricks to connect to + DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" + + # Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy + # Cold boots will be faster and you'll be able to scale your DB independently of your app. + # @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy + # PRISMA_GENERATE_DATAPROXY=true + PRISMA_GENERATE_DATAPROXY: + + # NextJS Auth + # @see: https://next-auth.js.org/configuration/options#nextauth_secret + # You can use: `openssl rand -base64 32` to generate one + NEXTAUTH_SECRET: + + # Set this to your public-facing URL, e.g., https://example.com + # You do not need the NEXTAUTH_URL environment variable in Vercel. + NEXTAUTH_URL: http://localhost:3000 + + # PostgreSQL password + POSTGRES_PASSWORD: postgres + + # Email Configuration + MAIL_FROM: + SMTP_HOST: + SMTP_PORT: + SMTP_SECURE_ENABLED: + SMTP_USER: + SMTP_PASSWORD: + + # Uncomment the below and set it to 1 to disable Email Verification for new signups + # EMAIL_VERIFICATION_DISABLED: + + # Uncomment the below and set it to 1 to disable Password Reset + # PASSWORD_RESET_DISABLED: + + # Uncomment the below and set it to 1 to disable Signups + # SIGNUP_DISABLED: + + # Uncomment the below and set it to 1 to disable Invites + # INVITE_DISABLED: + + # Uncomment the below and set a value to have your own Privacy Page URL on the signup & login page + # PRIVACY_URL: + + # Uncomment the below and set a value to have your own Terms Page URL on the auth and the surveys page + # TERMS_URL: + + # Uncomment the below and set a value to have your own Imprint Page URL on the auth and the surveys page + # IMPRINT_URL: + + # Uncomment the below and set to 1 if you want to enable GitHub OAuth + # GITHUB_AUTH_ENABLED: + # GITHUB_ID: + # GITHUB_SECRET: + + # Uncomment the below and set to 1 if you want to enable Google OAuth + # GOOGLE_AUTH_ENABLED: + # GOOGLE_CLIENT_ID: + # GOOGLE_CLIENT_SECRET: + +``` + + +Did we miss something? Are you still facing issues migrating your app? [Join our Discord!](https://formbricks.com/discord) We'd be happy to help! \ No newline at end of file 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/docs/BestPractices.tsx b/apps/formbricks-com/components/docs/BestPractices.tsx index c01a2a24ab..b48d3036e1 100644 --- a/apps/formbricks-com/components/docs/BestPractices.tsx +++ b/apps/formbricks-com/components/docs/BestPractices.tsx @@ -151,7 +151,7 @@ function BestPractice({ resource }: { resource: BestPractice }) { ); } -export function BestPractices() { +export default function BestPractices() { return (
diff --git a/apps/formbricks-com/components/docs/Navigation.tsx b/apps/formbricks-com/components/docs/Navigation.tsx index 0608c66539..5e945cf9b6 100644 --- a/apps/formbricks-com/components/docs/Navigation.tsx +++ b/apps/formbricks-com/components/docs/Navigation.tsx @@ -1,16 +1,16 @@ "use client"; -import { useRef } from "react"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; import clsx from "clsx"; import { AnimatePresence, motion, useIsPresent } from "framer-motion"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useRef } from "react"; +import { remToPx } from "@/lib/remToPx"; import { Button } from "./Button"; import { useIsInsideMobileNavigation } from "./MobileNavigation"; import { useSectionStore } from "./SectionProvider"; import { Tag } from "./Tag"; -import { remToPx } from "@/lib/remToPx"; interface NavGroup { title: string; @@ -246,13 +246,16 @@ export const navigation: Array = [ { title: "Production", href: "/docs/self-hosting/production" }, { title: "Docker", href: "/docs/self-hosting/docker" }, { title: "From Source", href: "/docs/self-hosting/from-source" }, + { title: "Migration to v1.1", href: "/docs/self-hosting/migrating-to-1.1" }, ], }, { title: "Contributing", links: [ { title: "Introduction", href: "/docs/contributing/introduction" }, + { title: "How we code at Formbricks", href: "/docs/contributing/how-we-code" }, { title: "Setup Dev Environment", href: "/docs/contributing/setup" }, + { title: "Gitpod", href: "/docs/contributing/gitpod" }, { title: "Demo App", href: "/docs/contributing/demo" }, { title: "Troubleshooting", href: "/docs/contributing/troubleshooting" }, ], diff --git a/apps/formbricks-com/components/home/GitHubSponsorship.tsx b/apps/formbricks-com/components/home/GitHubSponsorship.tsx index a089c5b7dd..4e331ec086 100644 --- a/apps/formbricks-com/components/home/GitHubSponsorship.tsx +++ b/apps/formbricks-com/components/home/GitHubSponsorship.tsx @@ -1,5 +1,4 @@ -import GitHubMarkWhite from "@/images/github-mark-white.svg"; -import GitHubMarkDark from "@/images/github-mark.svg"; +import HackIconGold from "@/images/formtribe/hack-icon-gold.svg"; import Image from "next/image"; import Link from "next/link"; @@ -10,7 +9,7 @@ export const GitHubSponsorship: React.FC = () => { @media (min-width: 426px); `}
- GitHub Sponsors Formbricks badge { alt="GitHub Sponsors Formbricks badge" width={100} height={100} - className="mr-12 hidden dark:block md:mr-4 " + className="mr-12 hidden dark:block md:mr-4" + /> */} + Hacktober Icon Gold

- Proudly Open-Source 🤍 + The FormTribe goes Hacktoberfest 🥨

- We're proud to to be supported by GitHubs Open-Source Program!{" "} + Write code, win a Mac! We're running a Hacktoberfest community Hackathon: - - Read more. + + Find out more.

diff --git a/apps/formbricks-com/components/home/Hero.tsx b/apps/formbricks-com/components/home/Hero.tsx index 2b06350668..89552ac8a2 100644 --- a/apps/formbricks-com/components/home/Hero.tsx +++ b/apps/formbricks-com/components/home/Hero.tsx @@ -20,10 +20,10 @@ export const Hero: React.FC = ({}) => {
- We're Open-Source | Star us on GitHub{" "} + className="border-brand-dark animate-bounce rounded-full border px-4 py-1.5 text-sm text-slate-500 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800"> + The FormTribe Hackathon is on 🔥

diff --git a/apps/formbricks-com/components/shared/Header.tsx b/apps/formbricks-com/components/shared/Header.tsx index 76b57ee726..65ef9c71c9 100644 --- a/apps/formbricks-com/components/shared/Header.tsx +++ b/apps/formbricks-com/components/shared/Header.tsx @@ -250,7 +250,7 @@ export default function Header() { */} Pricing diff --git a/apps/formbricks-com/components/shared/OpenSourceInfo.tsx b/apps/formbricks-com/components/shared/OpenSourceInfo.tsx new file mode 100644 index 0000000000..99b810b92e --- /dev/null +++ b/apps/formbricks-com/components/shared/OpenSourceInfo.tsx @@ -0,0 +1,33 @@ +import { Button } from "@formbricks/ui"; + +export const OpenSourceInfo = () => { + return ( +
+
+
+

+ Open Source +

+ +

+ Formbricks is an open source project. You can self-host it for free. We provide multiple easy + deployment options as per your customisation needs. We have documented the process of self-hosting + Formbricks on your own server using Docker, Bash Scripting, and Building from Source. +

+
+ + +
+
+
+
+ ); +}; diff --git a/apps/formbricks-com/components/shared/PricingCalculator.tsx b/apps/formbricks-com/components/shared/PricingCalculator.tsx new file mode 100644 index 0000000000..03972ac30f --- /dev/null +++ b/apps/formbricks-com/components/shared/PricingCalculator.tsx @@ -0,0 +1,115 @@ +import { Slider } from "@/components/shared/Slider"; +import { useState } from "react"; + +const ProductItem = ({ label, usersCount, price, onSliderChange }) => ( +
+
+
+ {label} +
+
+ {Math.round(usersCount).toLocaleString()} MTU +
+
+ ${price.toFixed(2)} +
+
+
+ +
+ {[3, 4, 5, 6].map((mark) => ( + + {mark === 3 ? "1K" : mark === 4 ? "10K" : mark === 5 ? "100K" : "1M"} + + ))} +
+
+
+); + +const Headers = () => ( +
+

Product

+

+ Subtotal +

+
+); + +const MonthlyEstimate = ({ price }) => ( +
+ + Monthly estimate: + +
+ + ${price.toFixed(2)} + + + {" "} + / month + +
+
+); + +export const PricingCalculator = () => { + const [inProductSlider, setInProductSlider] = useState(Math.log10(1000)); + const [linkSlider, setLinkSlider] = useState(Math.log10(1000)); + + const transformToLog = (value) => Math.pow(10, value); + + const calculatePrice = (users) => { + if (users <= 5000) { + return 0; + } else { + return users * 0.005; + } + }; + + const usersCountForInProductSlider = transformToLog(inProductSlider); + const productSurveysPrice = calculatePrice(usersCountForInProductSlider); + + return ( +
+

+ Pricing Calculator +

+ +
+
+ + +
+ + setInProductSlider(value[0])} + /> + +
+ + setLinkSlider(value[0])} + /> + +
+ + +
+
+
+ ); +}; diff --git a/apps/formbricks-com/components/shared/PricingGetStarted.tsx b/apps/formbricks-com/components/shared/PricingGetStarted.tsx new file mode 100644 index 0000000000..a93df8b2a0 --- /dev/null +++ b/apps/formbricks-com/components/shared/PricingGetStarted.tsx @@ -0,0 +1,47 @@ +import { Button } from "@formbricks/ui"; + +export const GetStartedWithPricing = ({ showDetailed }: { showDetailed: boolean }) => { + return ( + <> +
+
+
+

Free

+ + {showDetailed && ( +

+ General free usage on every product. Best for early stage startups and hobbyists +

+ )} + + +
+
+

Paid

+ {showDetailed && ( +

+ Formbricks with the next-generation features, Pay only for the tracked users. +

+ )} + + +
+
+
+ + ); +}; diff --git a/apps/formbricks-com/components/shared/PricingTable.tsx b/apps/formbricks-com/components/shared/PricingTable.tsx new file mode 100644 index 0000000000..f997336626 --- /dev/null +++ b/apps/formbricks-com/components/shared/PricingTable.tsx @@ -0,0 +1,103 @@ +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui"; +import { CheckIcon, XMarkIcon } from "@heroicons/react/24/outline"; + +export const PricingTable = ({ leadRow, pricing, endRow }) => { + return ( +
+
+
+
+ {leadRow.title} +
+
+ {leadRow.free} +
+ +
+ {leadRow.paid} +
+
+
+ +
+ {pricing.map((feature) => ( +
+
+ {feature.name} + {feature.addOnText && ( + + Addon + + )} +
+
+ {feature.addOnText ? ( + + + + {feature.free} + + +

+ {feature.addOnText} +

+
+
+
+ ) : feature.free ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ {feature.addOnText ? ( + + + + {feature.paid} + + +

+ {feature.addOnText} +

+
+
+
+ ) : feature.paid ? ( +
+ +
+ ) : ( +
+ +
+ )} +
+
+ ))} +
+ +
+
+
+ {endRow.title} +
+
+ {endRow.free} +
+ +
+ {endRow.paid} +
+
+
+
+ ); +}; diff --git a/apps/formbricks-com/components/shared/Slider.tsx b/apps/formbricks-com/components/shared/Slider.tsx new file mode 100644 index 0000000000..07bce61744 --- /dev/null +++ b/apps/formbricks-com/components/shared/Slider.tsx @@ -0,0 +1,24 @@ +"use client"; + +import * as React from "react"; +import * as SliderPrimitive from "@radix-ui/react-slider"; + +import { cn } from "@formbricks/lib/cn"; + +const Slider = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + +)); +Slider.displayName = SliderPrimitive.Root.displayName; + +export { Slider }; diff --git a/apps/formbricks-com/components/shared/icons/XCircleIcon.jsx b/apps/formbricks-com/components/shared/icons/XCircleIcon.jsx new file mode 100644 index 0000000000..d1573c7733 --- /dev/null +++ b/apps/formbricks-com/components/shared/icons/XCircleIcon.jsx @@ -0,0 +1,13 @@ + + +; diff --git a/apps/formbricks-com/images/formtribe/hack-icon-gold.svg b/apps/formbricks-com/images/formtribe/hack-icon-gold.svg new file mode 100644 index 0000000000..0286b6b8a0 --- /dev/null +++ b/apps/formbricks-com/images/formtribe/hack-icon-gold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/formbricks-com/package.json b/apps/formbricks-com/package.json index 1153544b41..338b344d89 100644 --- a/apps/formbricks-com/package.json +++ b/apps/formbricks-com/package.json @@ -47,6 +47,8 @@ "node-fetch": "^3.3.2", "prism-react-renderer": "^2.0.6", "prismjs": "^1.29.0", + "@radix-ui/react-slider": "^1.1.2", + "@radix-ui/react-tooltip": "^1.0.6", "react": "18.2.0", "react-dom": "18.2.0", "react-highlight-words": "^0.20.0", diff --git a/apps/formbricks-com/pages/api/oss-friends/index.ts b/apps/formbricks-com/pages/api/oss-friends/index.ts index bbe21a59ee..4a3f61c6e2 100644 --- a/apps/formbricks-com/pages/api/oss-friends/index.ts +++ b/apps/formbricks-com/pages/api/oss-friends/index.ts @@ -100,7 +100,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse) }, { name: "Papermark", - description: "Open-Source Docsend Alternative to securely share documents with real-time analytics.", + description: + "Open-Source Docsend Alternative to securely share documents with real-time analytics.", href: "https://www.papermark.io/", }, { diff --git a/apps/formbricks-com/pages/blog/join-the-formtribe/create-a-new-survey-with-formbricks.png b/apps/formbricks-com/pages/blog/join-the-formtribe/create-a-new-survey-with-formbricks.png new file mode 100644 index 0000000000..6803f12105 Binary files /dev/null and b/apps/formbricks-com/pages/blog/join-the-formtribe/create-a-new-survey-with-formbricks.png differ diff --git a/apps/formbricks-com/pages/blog/join-the-formtribe/formbricks-monorepo-folder-structure.png b/apps/formbricks-com/pages/blog/join-the-formtribe/formbricks-monorepo-folder-structure.png new file mode 100644 index 0000000000..5ac0e6b4c5 Binary files /dev/null and b/apps/formbricks-com/pages/blog/join-the-formtribe/formbricks-monorepo-folder-structure.png differ diff --git a/apps/formbricks-com/pages/blog/join-the-formtribe/formbricks-packages-folder.png b/apps/formbricks-com/pages/blog/join-the-formtribe/formbricks-packages-folder.png new file mode 100644 index 0000000000..29e9f5f233 Binary files /dev/null and b/apps/formbricks-com/pages/blog/join-the-formtribe/formbricks-packages-folder.png differ diff --git a/apps/formbricks-com/pages/blog/join-the-formtribe/index.mdx b/apps/formbricks-com/pages/blog/join-the-formtribe/index.mdx new file mode 100644 index 0000000000..8264126c90 --- /dev/null +++ b/apps/formbricks-com/pages/blog/join-the-formtribe/index.mdx @@ -0,0 +1,107 @@ +import Image from "next/image"; +import LayoutMdx from "@/components/shared/LayoutMdx"; +import MonorepoImage from "./formbricks-monorepo-folder-structure.png"; +import HeaderImage from "./create-a-new-survey-with-formbricks.png"; +import GitpodImage from "./setup-formbricks-via-gitpod.png"; +import PackagesFolderImage from "./formbricks-packages-folder.png"; +import AuthorBox from "@/components/shared/AuthorBox"; + +export const meta = { + title: "Join the FormTribe 🔥", + description: "Here is everything you need to know about joining the Formbricks community", + date: "2023-10-01", + publishedTime: "2023-10-01T00:00:00", + authors: ["Johannes"], + section: "Open-Source", + tags: ["Open-Source", "No-Code", "Formbricks", "Geting started", "Welcome guide"], +}; + + + +Title Image + +_Get a quick intro to the Formbricks community, also known as FormTribe, and learn all the deets about making awesome contributions to the project._ + +## Welcome to the Formbricks community! + +We are so excited to have you with us 😊 + +In this post we will be helping you get familiar with the Formbricks codebase and get you up to speed contributing in no time. If you want to learn about Formbricks check out our [docs intro](https://formbricks.com/docs/introduction/what-is-formbricks), for more info about our founding story and why we're building open source checkout out our [blog](https://formbricks.com/blog). + +### Prerequisites + +Our codebase is written fully in Typescript and we love it 😍. To power our the experience management solution, here is the stack behind it all: + +[Next.js](https://nextjs.org/) - React Framework + +[Prisma](https://www.prisma.io/) - ORM + +[Typescript](https://www.typescriptlang.org/) - Language + +[Lucide React](https://lucide.dev/guide/packages/lucide-react) - Icons + +[TalwindCSS](https://tailwindcss.com/) - Styling + +[Zod](https://zod.dev/) - Validation + +[Auth.js](https://authjs.dev/) - Authentication + +### 😎 Installation and Setup + +To get up and running we have 2 options: Gitpod and local. + +#### Get started with Gitpod + +With Gitpod you can run all of Formbricks in the cloud. With one click you can start coding right away in your browser: + +[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/formbricks/formbricks) + +Setup Formbricks via Gitpod + +#### Run on a local machine + +If you choose to get setup locally, we also have a well documented guide to hold you through the process, you can find it [here](https://formbricks.com/docs/contributing/setup) + +### 👩🏽‍💻 Codebase Overview + +Our codebase is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) which means we have different projects in one repository. At moment we have 3 different projects: + +1. **demo** `apps/demo` - It's a simple React app that you can run locally and use to trigger actions and set **[Attributes](https://formbricks.com/docs/attributes/why)**. It allows you to test your setup easily. +2. **formbricks-com** `apps/formbricks-com` - The landing page of [Formbricks](https://formbricks.com) +3. **web** `apps/web` - Our [cloud offering](https://app.formbricks.com/) for Formbricks. + +Formbricks monorepo folder structure + +#### TurboRepo and our own packages + +To manage all of these projects in one repository we use [turborepo](https://turbo.build/repo/docs/core-concepts/monorepos). Depending on what part of the codebase you need to contribute in, now you know where to begin 😃 + +We also have a set of packages which we manage: They are located in the `packages` folder. There we keep our styling library, components, database migrations and connection, a couple of configurations and much more. We do this to use any of these packages seamlessly between our mono repos. + +Formbricks packages folder + +### ⚖️ Contribution Guidelines + +You want to get started contributing? Amazing! Checkout our must-read post on [How we Code at Formbricks](https://formbricks.com/docs/contributing/how-we-code). This will give you everything you need to know about successfully contributing to our codebase in no time. + +### 🤗 Our Community + +We really value our community. It might be small but it is close to our hearts. Join our [Discord](https://formbricks.com/discord) to learn from other contributors and meet the Formbricks community. + +### Conclusion + +Contributing to open source projects like Formbricks can be a rewarding experience. By contributing, you have the opportunity to make a meaningful impact on a project used by many and gain valuable experience in the process. + +Whether you are a seasoned developer or just starting out, your contributions are appreciated. We might not always have to onboard everyone but try our best. You can help improve the codebase, fix bugs, add new features, or even contribute to the documentation. Every contribution, no matter how small, can make a difference. + +Not only will you be able to showcase your skills and build your portfolio, but you will also have the chance to collaborate with other talented designers and developers in the Formbricks community. You can learn from their expertise and share your own knowledge. + +### So, why wait? + +Join the Formbricks community today and start contributing to an up and coming open source project. Your contributions can help shape the future of Formbricks and make a positive impact on the lives of tens of thousands of users worldwide. + +We look forward to seeing your contributions and welcoming you to the Formbricks community! + +### [Say Hi 👋](https://formbricks.com/discord) + +export default ({ children }) => {children}; diff --git a/apps/formbricks-com/pages/blog/join-the-formtribe/setup-formbricks-via-gitpod.png b/apps/formbricks-com/pages/blog/join-the-formtribe/setup-formbricks-via-gitpod.png new file mode 100644 index 0000000000..4756254185 Binary files /dev/null and b/apps/formbricks-com/pages/blog/join-the-formtribe/setup-formbricks-via-gitpod.png differ diff --git a/apps/formbricks-com/pages/demo/index.tsx b/apps/formbricks-com/pages/demo/index.tsx index 020d17b2ef..2713807fb8 100644 --- a/apps/formbricks-com/pages/demo/index.tsx +++ b/apps/formbricks-com/pages/demo/index.tsx @@ -5,7 +5,7 @@ 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/index.tsx b/apps/formbricks-com/pages/formtribe/index.tsx index c17c197a77..5894bd3451 100644 --- a/apps/formbricks-com/pages/formtribe/index.tsx +++ b/apps/formbricks-com/pages/formtribe/index.tsx @@ -48,33 +48,84 @@ const HowTo = [ const SideQuests = [ { - points: "100 Points:", - quest: "You think you're smart removing the blur to see the side quests first?", + points: "Join the Tribe Tweet (100 Points)", + quest: "Tweet a single “🧱” emoji before the 7th of October EOD to join the #FormTribe.", + proof: "Share the link to the tweet in the “side-quest” channel.", }, { - points: "150 Points:", + points: "Spread the Word Tweet (100 Points)", + quest: "Tweet “🧱🚀” on the day of the ProductHunt launch to spread the word.", + proof: "Share the link to the tweet in the “side-quest” channel.", + }, + { + points: "Setup Insights (200 Points)", + quest: "Screen record yourself setting up the Formbricks dev environment.", + proof: "Upload to WeTransfer and send to johannes@formbricks.com", + }, + { + points: "Meme Magic (50 Points + up to 100 Points)", quest: - "You are! Take a screenshot of this and share it in the 'side-quest' channel on Discord to get 100 points.", + "Craft a meme where a brick plays a role. For extra points, tweet it, tag us and score +5 for each like.", + proof: "Share meme or link to the tweet in the “side-quest” channel.", }, { - points: "200 Points:", - quest: "The rest of the side quests will be released on the 1st of October.", + points: "GIF Magic (100 Points)", + quest: + "Create a branded gif related to Formbricks. Upload it to Giphy. For extra points, tweet it, tag us and score +5 for each like.", + proof: "Share link to Giphy in the “side-quest” channel.", }, { - points: "250 Points:", - quest: "Follow us on Twitter and join us on Discord to be the first to know!", + points: "Design a background (250 Points)", + quest: "Illustrate a captivating background for survey enthusiasts (more infos on Notion).", + proof: "Share the design in the “side-quest” channel.", }, { - points: "Pushmaster Prime | +500 Points + Hoodie:", - quest: "Merge the highest amount of Formbricks PRs in October.", + points: "Transform Animation to CSS (350 Points per background)", + quest: "Animate an existing background to CSS versions (more infos on Notion).", + proof: "Share the animated background.", }, { - points: "Guidance Guru | +500 Points + Hoodie:", - quest: "Most active and helpful in the community helping other contributors.", + points: "Enhance Docs (50-250 Points)", + quest: + "Add a new section to our docs where you see gaps. Follow the current style of documentation incl. code snippets and screenshots. Pls no spam.", + proof: "Open a PR with “docs” in the title", }, { - points: "Buzz Builder Guru | +500 Points + Hoodie:", - quest: "Marketing Genie with great and effective ideas to spread the word about FormTribe", + points: "Starry-eyed Supporter (250 Points)", + quest: "Get five friends to star our repository.", + proof: "Share 5 screenshots of the chats where you asked them and they confirmed + their GitHub names", + }, + { + points: "Bug Hunter (50-250 Points)", + quest: "Find and report any functionality bugs.", + proof: "Open a bug issue in our repository.", + }, + { + points: "Brickify someone famous with AI (200 Points + up to 100 Points)", + quest: + "Find someone whose name would be funny as a play on words with “brick”. Then, with the help of AI, create a brick version of this person like Brick Astley, Brickj Minaj, etc. For extra points, tweet it, tag us and score +5 for each like.", + proof: "Share your art or link to the tweet in the “side-quest” channel.", + }, + { + points: "SEO Sage (50-250 Points)", + quest: "Provide detailed SEO recommendations or improvements for our main website.", + proof: "Share your insights.", + }, + { + points: "Community Connector (50 points each, up to 250 points)", + quest: + "Introduce and onboard new members to the community. Have them join Discord and reply to their automated welcome message with your discord handle (in “say-hi” channel).", + proof: "New member joined and commented with your Discord handle", + }, + { + points: "Feedback Fanatic (50 Points)", + quest: "Fill out our feedback survey after the hackathon with suggestions for improvement.", + proof: "Submit the survey.", + }, + { + points: "Side Quest Babo (500 Points)", + quest: "Complete all side quests.", + proof: "All quests marked as completed.", }, ]; @@ -212,6 +263,115 @@ const FAQ = [ }, ]; +const Leaderboard = [ + { + name: "Piyush", + points: "550", + link: "https://github.com/gupta-piyush19", + }, + { + name: "Suman", + points: "200", + }, + { + name: "Subhdeep", + points: "100", + }, + { + name: "Pratik", + points: "250", + }, + { + name: "Karuppiah", + points: "100", + }, + { + name: "Arth", + points: "100", + }, + { + name: "Neztep", + points: "100", + }, + { + name: "Kelvin Parmar", + points: "200", + }, + { + name: "Arjun", + points: "100", + }, + { + name: "Yashhhh", + points: "200", + }, + { + name: "vishleshak", + points: "100", + }, + { + name: "Ashu999", + points: "100", + }, + { + name: "Sachin H", + points: "100", + }, + { + name: "Suraj Jadhav", + points: "100", + }, + { + name: "Vishrut", + points: "250", + }, + { + name: "cataxcab", + points: "100", + }, + { + name: "Eldemarkki", + points: "100", + }, + { + name: "Suyash", + points: "100", + }, + { + name: "Rohan Gupta", + points: "100", + }, + { + name: "Nafees Nazik", + points: "100", + }, + { + name: "monk", + points: "100", + }, + { + name: "Pratik Tiwari (Pratik)", + points: "100", + }, + { + name: "Ardash Malviya", + points: "100", + }, + { + name: "Aditya Deshlahre", + points: "450", + link: "https://github.com/adityadeshlahre", + }, + { + name: "Rutam", + points: "250", + }, + { + name: "Sagnik Sahoo", + points: "100", + }, +]; + export default function FormTribeHackathon() { // dark mode fix useEffect(() => { @@ -219,7 +379,7 @@ export default function FormTribeHackathon() { }, []); return (

- Let's ship Open Source Typeform in Hacktoberfest + Let's ship Open Source Typeform during Hacktoberfest

@@ -462,40 +622,57 @@ export default function FormTribeHackathon() {

))}
- -
-
-

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:{" "} + winning. Here is a list of side quests you can complete:

-
- {SideQuests.map((quest) => ( -
-
✅
-
-

- {quest.points} {quest.quest} -

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

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

+
+
+ +

+

Proof: {quest.proof}

+

+
+
-
- ))} + ))} +
+
+ {/* The Leaderboard */} -
-
-

Not live yet.

- - Sign up to get notified on kick-off. - +
+
+
User
+
Points
+ {Leaderboard.sort((a, b) => parseInt(b.points) - parseInt(a.points)).map((player) => ( + +
+
+
{player.name}
+
+
{player.points} Points
+
+
+ ))}
{/* The Timeline */} 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/index.tsx b/apps/formbricks-com/pages/index.tsx index c21f7950a1..c4b8bc18dd 100644 --- a/apps/formbricks-com/pages/index.tsx +++ b/apps/formbricks-com/pages/index.tsx @@ -4,7 +4,6 @@ import Features from "@/components/home/Features"; import Highlights from "@/components/home/Highlights"; import BreakerCTA from "@/components/shared/BreakerCTA"; import Steps from "@/components/home/Steps"; -import Pricing from "@/components/shared/Pricing"; import GitHubSponsorship from "@/components/home/GitHubSponsorship"; import BestPractices from "@/components/shared/BestPractices"; @@ -42,7 +41,6 @@ const IndexPage = () => ( href="https://app.formbricks.com/auth/signup" inverted /> - ); 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/pages/pricing.tsx b/apps/formbricks-com/pages/pricing.tsx new file mode 100644 index 0000000000..64429f3389 --- /dev/null +++ b/apps/formbricks-com/pages/pricing.tsx @@ -0,0 +1,144 @@ +import HeroTitle from "@/components/shared/HeroTitle"; +import Layout from "@/components/shared/Layout"; +import { OpenSourceInfo } from "@/components/shared/OpenSourceInfo"; +import { GetStartedWithPricing } from "@/components/shared/PricingGetStarted"; +import { PricingCalculator } from "../components/shared/PricingCalculator"; +import { PricingTable } from "../components/shared/PricingTable"; + +const inProductSurveys = { + leadRow: { + title: "In-Product Surveys", + free: ( +
+ 5000 tracked users /mo{" "} +
+ ), + paid: "Unlimited", + }, + features: [ + { name: "Unlimited Surveys", free: true, paid: true }, + { name: "Granular Targeting", free: true, paid: true }, + { name: "30+ Templates", free: true, paid: true }, + { name: "API Access", free: true, paid: true }, + { name: "Third-Party Integrations", free: true, paid: true }, + { name: "Unlimited Team Members", free: true, paid: true }, + { name: "Unlimited Responses per Survey", free: true, paid: true }, + { name: "Advanced User Targeting", free: false, paid: true }, + { name: "Multi Language", free: false, paid: true }, + + { + name: "Custom URL for Link Surveys", + free: "10$/mo", + paid: "10$/mo", + addOnText: "Free if you self-host", + }, + { + name: "Remove Formbricks Branding", + free: "10$/mo", + paid: "10$/mo", + addOnText: "Free if you self-host", + }, + ], + endRow: { + title: "In-Product Surveys Pricing", + free: "Free", + paid: ( +
+ Free up to 5000 tracked users/mo, then + $0.005 + / tracked user +
+ ), + }, +}; + +const linkSurveys = { + leadRow: { + title: "Link Surveys", + free: Unlimited, + paid: "Unlimited", + }, + + features: [ + { name: "Unlimited Surveys", free: true, paid: true }, + { name: "Unlimited Responses", free: true, paid: true }, + { name: "Partial Responses", free: true, paid: true }, + { name: "⚙️ URL Shortener", free: true, paid: true }, + { name: "⚙️ Recall Information", free: true, paid: true }, + { name: "⚙️ Hidden Field Questions", free: true, paid: true }, + { name: "⚙️ Time to Complete Metadata", free: true, paid: true }, + { name: "⚙️ File Upload", free: true, paid: true }, + { name: "⚙️ Signature Question", free: true, paid: true }, + { name: "⚙️ Question Grouping", free: true, paid: true }, + { name: "⚙️ Add Media to Questions", free: true, paid: true }, + ], + + endRow: { + title: "Link Surveys Pricing", + free: "Free", + paid: "Free", + }, +}; + +const integrations = { + leadRow: { + title: "Integrations", + free: Unlimited, + paid: "Unlimited", + }, + features: [ + { name: "Webhooks", free: true, paid: true }, + { name: "Zapier", free: true, paid: true }, + { name: "Google Sheets", free: true, paid: true }, + { name: "n8n", free: true, paid: true }, + { name: "Make", free: true, paid: true }, + ], + endRow: { + title: "Integrations Pricing", + free: "Free", + paid: "Free", + }, +}; + +const PricingPage = () => { + return ( + + +
+
+ + + +
+ + + + +
+ + +
+
+
+ ); +}; + +export default PricingPage; diff --git a/apps/web/app/(app)/FormbricksClient.tsx b/apps/web/app/(app)/FormbricksClient.tsx index ebc694e2da..6289105b52 100644 --- a/apps/web/app/(app)/FormbricksClient.tsx +++ b/apps/web/app/(app)/FormbricksClient.tsx @@ -7,7 +7,6 @@ import { useEffect } from "react"; type UsageAttributesUpdaterProps = { numSurveys: number; - totalSubmissions: number; }; export default function FormbricksClient({ session }) { @@ -19,31 +18,23 @@ export default function FormbricksClient({ session }) { }); formbricks.setUserId(session.user.id); formbricks.setEmail(session.user.email); - if (session.user.teams?.length > 0) { - formbricks.setAttribute("Plan", session.user.teams[0].plan); - formbricks.setAttribute("Name", session?.user?.name); - } } }, [session]); return null; } -const updateUsageAttributes = (numSurveys, totalSubmissions) => { +const updateUsageAttributes = (numSurveys) => { if (!formbricksEnabled) return; if (numSurveys >= 3) { formbricks.setAttribute("HasThreeSurveys", "true"); } - - if (totalSubmissions >= 20) { - formbricks.setAttribute("HasTwentySubmissions", "true"); - } }; -export function UsageAttributesUpdater({ numSurveys, totalSubmissions }: UsageAttributesUpdaterProps) { +export function UsageAttributesUpdater({ numSurveys }: UsageAttributesUpdaterProps) { useEffect(() => { - updateUsageAttributes(numSurveys, totalSubmissions); - }, [numSurveys, totalSubmissions]); + updateUsageAttributes(numSurveys); + }, [numSurveys]); return null; } diff --git a/apps/web/app/(app)/PosthogIdentify.tsx b/apps/web/app/(app)/PosthogIdentify.tsx index b022fb4056..dc055535d5 100644 --- a/apps/web/app/(app)/PosthogIdentify.tsx +++ b/apps/web/app/(app)/PosthogIdentify.tsx @@ -12,9 +12,6 @@ export default function PosthogIdentify({ session }: { session: Session }) { useEffect(() => { if (posthogEnabled && session.user && posthog) { posthog.identify(session.user.id); - if (session.user.teams?.length > 0) { - posthog?.group("team", session.user.teams[0].id); - } } }, [session, posthog]); diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs.tsx index 7c21c62163..104a0f59dc 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/ActionsAttributesTabs.tsx @@ -22,5 +22,5 @@ export default function ActionsAttributesTabs({ activeId, environmentId }: Actio }, ]; - return ; + return ; } diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionActivityTab.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionActivityTab.tsx index c6beed9eaf..54e13b363a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionActivityTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionActivityTab.tsx @@ -1,21 +1,67 @@ +"use client"; + import LoadingSpinner from "@/components/shared/LoadingSpinner"; import { ErrorComponent } from "@formbricks/ui"; import { Label } from "@formbricks/ui"; -import { useEventClass } from "@/lib/eventClasses/eventClasses"; import { convertDateTimeStringShort } from "@formbricks/lib/time"; import { capitalizeFirstLetter } from "@/lib/utils"; import { CodeBracketIcon, CursorArrowRaysIcon, SparklesIcon } from "@heroicons/react/24/solid"; - +import { TActionClass } from "@formbricks/types/v1/actionClasses"; +import { useEffect, useState } from "react"; +import { + getActionCountInLastHourAction, + getActionCountInLast24HoursAction, + getActionCountInLast7DaysAction, + GetActiveInactiveSurveysAction, +} from "./actions"; interface ActivityTabProps { - environmentId: string; - actionClassId: string; + actionClass: TActionClass; } -export default function EventActivityTab({ environmentId, actionClassId }: ActivityTabProps) { - const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClassId); +export default function EventActivityTab({ actionClass }: ActivityTabProps) { + // const { eventClass, isLoadingEventClass, isErrorEventClass } = useEventClass(environmentId, actionClass.id); - if (isLoadingEventClass) return ; - if (isErrorEventClass) return ; + const [numEventsLastHour, setNumEventsLastHour] = useState(); + const [numEventsLast24Hours, setNumEventsLast24Hours] = useState(); + const [numEventsLast7Days, setNumEventsLast7Days] = useState(); + const [activeSurveys, setActiveSurveys] = useState(); + const [inactiveSurveys, setInactiveSurveys] = useState(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + setLoading(true); + updateState(); + + async function updateState() { + try { + setLoading(true); + const [ + numEventsLastHourData, + numEventsLast24HoursData, + numEventsLast7DaysData, + activeInactiveSurveys, + ] = await Promise.all([ + getActionCountInLastHourAction(actionClass.id), + getActionCountInLast24HoursAction(actionClass.id), + getActionCountInLast7DaysAction(actionClass.id), + GetActiveInactiveSurveysAction(actionClass.id), + ]); + setNumEventsLastHour(numEventsLastHourData); + setNumEventsLast24Hours(numEventsLast24HoursData); + setNumEventsLast7Days(numEventsLast7DaysData); + setActiveSurveys(activeInactiveSurveys.activeSurveys); + setInactiveSurveys(activeInactiveSurveys.inactiveSurveys); + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + } + }, [actionClass.id]); + + if (loading) return ; + if (error) return ; return (
@@ -24,15 +70,15 @@ export default function EventActivityTab({ environmentId, actionClassId }: Activ
-

{eventClass.numEventsLastHour}

+

{numEventsLastHour}

last hour

-

{eventClass.numEventsLast24Hours}

+

{numEventsLast24Hours}

last 24 hours

-

{eventClass.numEventsLast7Days}

+

{numEventsLast7Days}

last week

@@ -40,8 +86,8 @@ export default function EventActivityTab({ environmentId, actionClassId }: Activ
- {eventClass.activeSurveys.length === 0 &&

-

} - {eventClass.activeSurveys.map((surveyName) => ( + {activeSurveys?.length === 0 &&

-

} + {activeSurveys?.map((surveyName) => (

{surveyName}

@@ -49,8 +95,8 @@ export default function EventActivityTab({ environmentId, actionClassId }: Activ
- {eventClass.inactiveSurveys.length === 0 &&

-

} - {eventClass.inactiveSurveys.map((surveyName) => ( + {inactiveSurveys?.length === 0 &&

-

} + {inactiveSurveys?.map((surveyName) => (

{surveyName}

@@ -61,28 +107,28 @@ export default function EventActivityTab({ environmentId, actionClassId }: Activ

- {convertDateTimeStringShort(eventClass.createdAt?.toString())} + {convertDateTimeStringShort(actionClass.createdAt?.toString())}

{" "}

- {convertDateTimeStringShort(eventClass.updatedAt?.toString())} + {convertDateTimeStringShort(actionClass.updatedAt?.toString())}

- {eventClass.type === "code" ? ( + {actionClass.type === "code" ? ( - ) : eventClass.type === "noCode" ? ( + ) : actionClass.type === "noCode" ? ( - ) : eventClass.type === "automatic" ? ( + ) : actionClass.type === "automatic" ? ( ) : null}
-

{capitalizeFirstLetter(eventClass.type)}

+

{capitalizeFirstLetter(actionClass.type)}

diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionDetailModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionDetailModal.tsx index 1017545e98..f0362ac33c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionDetailModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionDetailModal.tsx @@ -20,7 +20,7 @@ export default function ActionDetailModal({ const tabs = [ { title: "Activity", - children: , + children: , }, { title: "Settings", diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionSettingsTab.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionSettingsTab.tsx index ea4f37db2b..da87f38a60 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionSettingsTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionSettingsTab.tsx @@ -9,11 +9,14 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "react-hot-toast"; import { testURLmatch } from "./testURLmatch"; -import { deleteActionClass, updateActionClass } from "@formbricks/lib/services/actionClass"; import { TActionClassInput, TActionClassNoCodeConfig } from "@formbricks/types/v1/actionClasses"; import { CssSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/CssSelector"; import { PageUrlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/PageUrlSelector"; import { InnerHtmlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/InnerHtmlSelector"; +import { + deleteActionClassAction, + updateActionClassAction, +} from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions"; interface ActionSettingsTabProps { environmentId: string; @@ -77,10 +80,11 @@ export default function ActionSettingsTab({ environmentId, actionClass, setOpen const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as NoCodeConfig); const updatedData: TActionClassInput = { ...data, + environmentId, noCodeConfig: filteredNoCodeConfig, type: "noCode", } as TActionClassInput; - await updateActionClass(environmentId, actionClass.id, updatedData); + await updateActionClassAction(environmentId, actionClass.id, updatedData); setOpen(false); router.refresh(); toast.success("Action updated successfully"); @@ -94,7 +98,7 @@ export default function ActionSettingsTab({ environmentId, actionClass, setOpen const handleDeleteAction = async () => { try { setIsDeletingAction(true); - await deleteActionClass(environmentId, actionClass.id); + await deleteActionClassAction(environmentId, actionClass.id); router.refresh(); toast.success("Action deleted successfully"); setOpen(false); diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx index e8cb2db5c8..ab39bb7279 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/AddNoCodeActionModal.tsx @@ -7,7 +7,6 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import toast from "react-hot-toast"; import { testURLmatch } from "./testURLmatch"; -import { createActionClass } from "@formbricks/lib/services/actionClass"; import { TActionClassInput, TActionClassNoCodeConfig, @@ -16,6 +15,7 @@ import { import { CssSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/CssSelector"; import { PageUrlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/PageUrlSelector"; import { InnerHtmlSelector } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/(selectors)/InnerHtmlSelector"; +import { createActionClassAction } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions"; interface AddNoCodeActionModalProps { environmentId: string; @@ -79,11 +79,12 @@ export default function AddNoCodeActionModal({ const filteredNoCodeConfig = filterNoCodeConfig(data.noCodeConfig as TActionClassNoCodeConfig); const updatedData: TActionClassInput = { ...data, + environmentId, noCodeConfig: filteredNoCodeConfig, type: "noCode", } as TActionClassInput; - const newActionClass: TActionClass = await createActionClass(environmentId, updatedData); + const newActionClass: TActionClass = await createActionClassAction(updatedData); if (setActionClassArray) { setActionClassArray((prevActionClassArray: TActionClass[]) => [ ...prevActionClassArray, diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts new file mode 100644 index 0000000000..11dd1b590f --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/actions.ts @@ -0,0 +1,97 @@ +"use server"; + +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +import { createActionClass, deleteActionClass, updateActionClass } from "@formbricks/lib/actionClass/service"; +import { canUserAccessActionClass } from "@formbricks/lib/actionClass/auth"; +import { getServerSession } from "next-auth"; +import { TActionClassInput } from "@formbricks/types/v1/actionClasses"; + +import { + getActionCountInLast24Hours, + getActionCountInLast7Days, + getActionCountInLastHour, +} from "@formbricks/lib/services/actions"; +import { getSurveysByActionClassId } from "@formbricks/lib/services/survey"; +import { AuthorizationError } from "@formbricks/types/v1/errors"; + +export async function deleteActionClassAction(environmentId, actionClassId: string) { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessActionClass(session.user.id, actionClassId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + + await deleteActionClass(environmentId, actionClassId); +} + +export async function updateActionClassAction( + environmentId: string, + actionClassId: string, + updatedAction: Partial +) { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessActionClass(session.user.id, actionClassId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + + return await updateActionClass(environmentId, actionClassId, updatedAction); +} + +export async function createActionClassAction(action: TActionClassInput) { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await hasUserEnvironmentAccess(session.user.id, action.environmentId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + + return await createActionClass(action.environmentId, action); +} + +export const getActionCountInLastHourAction = async (actionClassId: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessActionClass(session.user.id, actionClassId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + + return await getActionCountInLastHour(actionClassId); +}; + +export const getActionCountInLast24HoursAction = async (actionClassId: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessActionClass(session.user.id, actionClassId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + + return await getActionCountInLast24Hours(actionClassId); +}; + +export const getActionCountInLast7DaysAction = async (actionClassId: string) => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessActionClass(session.user.id, actionClassId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + + return await getActionCountInLast7Days(actionClassId); +}; + +export const GetActiveInactiveSurveysAction = async ( + actionClassId: string +): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessActionClass(session.user.id, actionClassId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + + const surveys = await getSurveysByActionClassId(actionClassId); + const response = { + activeSurveys: surveys.filter((s) => s.status === "inProgress").map((survey) => survey.name), + inactiveSurveys: surveys.filter((s) => s.status !== "inProgress").map((survey) => survey.name), + }; + return response; +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/page.tsx index 27dbd8caf6..2e39e09f4a 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/page.tsx @@ -4,7 +4,7 @@ import ActionClassesTable from "@/app/(app)/environments/[environmentId]/(action import ActionClassDataRow from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionRowData"; import ActionTableHeading from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/actions/ActionTableHeading"; import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; -import { getActionClasses } from "@formbricks/lib/services/actionClass"; +import { getActionClasses } from "@formbricks/lib/actionClass/service"; import { Metadata } from "next"; export const metadata: Metadata = { diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeActivityTab.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeActivityTab.tsx index 0b66b3d820..54eb7d067c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeActivityTab.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeActivityTab.tsx @@ -1,31 +1,53 @@ +"use client"; + +import { GetActiveInactiveSurveysAction } from "@/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions"; import LoadingSpinner from "@/components/shared/LoadingSpinner"; -import { useAttributeClass } from "@/lib/attributeClasses/attributeClasses"; import { capitalizeFirstLetter } from "@/lib/utils"; import { convertDateTimeStringShort } from "@formbricks/lib/time"; +import { TAttributeClass } from "@formbricks/types/v1/attributeClasses"; import { ErrorComponent, Label } from "@formbricks/ui"; import { TagIcon } from "@heroicons/react/24/solid"; +import { useEffect, useState } from "react"; interface EventActivityTabProps { - attributeClassId: string; - environmentId: string; + attributeClass: TAttributeClass; } -export default function AttributeActivityTab({ environmentId, attributeClassId }: EventActivityTabProps) { - const { attributeClass, isLoadingAttributeClass, isErrorAttributeClass } = useAttributeClass( - environmentId, - attributeClassId - ); +export default function AttributeActivityTab({ attributeClass }: EventActivityTabProps) { + const [activeSurveys, setActiveSurveys] = useState(); + const [inactiveSurveys, setInactiveSurveys] = useState(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - if (isLoadingAttributeClass) return ; - if (isErrorAttributeClass) return ; + useEffect(() => { + setLoading(true); + + getSurveys(); + + async function getSurveys() { + try { + setLoading(true); + const activeInactive = await GetActiveInactiveSurveysAction(attributeClass.id); + setActiveSurveys(activeInactive.activeSurveys); + setInactiveSurveys(activeInactive.inactiveSurveys); + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + } + }, [attributeClass.id]); + + if (loading) return ; + if (error) return ; return (
- {attributeClass.activeSurveys.length === 0 &&

-

} - {attributeClass.activeSurveys.map((surveyName) => ( + {activeSurveys?.length === 0 &&

-

} + {activeSurveys?.map((surveyName) => (

{surveyName}

@@ -33,8 +55,8 @@ export default function AttributeActivityTab({ environmentId, attributeClassId }
- {attributeClass.inactiveSurveys.length === 0 &&

-

} - {attributeClass.inactiveSurveys.map((surveyName) => ( + {inactiveSurveys?.length === 0 &&

-

} + {inactiveSurveys?.map((surveyName) => (

{surveyName}

diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable.tsx index cfce41c9d9..f76eecf980 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/AttributeClassesTable.tsx @@ -8,11 +8,9 @@ import { useMemo } from "react"; import { TAttributeClass } from "@formbricks/types/v1/attributeClasses"; export default function AttributeClassesTable({ - environmentId, attributeClasses, children: [TableHeading, howToAddAttributeButton, attributeRows], }: { - environmentId: string; attributeClasses: TAttributeClass[]; children: [JSX.Element, JSX.Element, JSX.Element[]]; }) { @@ -69,7 +67,6 @@ export default function AttributeClassesTable({ ))}
void; - attributeClass: AttributeClass; + attributeClass: TAttributeClass; } -export default function AttributeDetailModal({ - environmentId, - open, - setOpen, - attributeClass, -}: AttributeDetailModalProps) { +export default function AttributeDetailModal({ open, setOpen, attributeClass }: AttributeDetailModalProps) { const tabs = [ { title: "Activity", - children: , + children: , }, { title: "Settings", diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions.ts new file mode 100644 index 0000000000..55f9b34315 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/actions.ts @@ -0,0 +1,14 @@ +"use server"; + +import { getSurveysByAttributeClassId } from "@formbricks/lib/services/survey"; + +export const GetActiveInactiveSurveysAction = async ( + attributeClassId: string +): Promise<{ activeSurveys: string[]; inactiveSurveys: string[] }> => { + const surveys = await getSurveysByAttributeClassId(attributeClassId); + const response = { + activeSurveys: surveys.filter((s) => s.status === "inProgress").map((survey) => survey.name), + inactiveSurveys: surveys.filter((s) => s.status !== "inProgress").map((survey) => survey.name), + }; + return response; +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/page.tsx index 37cea27263..6a37ff53bf 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(actionsAndAttributes)/attributes/page.tsx @@ -14,9 +14,10 @@ export const metadata: Metadata = { export default async function AttributesPage({ params }) { let attributeClasses = await getAttributeClasses(params.environmentId); + return ( <> - + diff --git a/apps/web/app/(app)/environments/[environmentId]/AddProductModal.tsx b/apps/web/app/(app)/environments/[environmentId]/AddProductModal.tsx index 2548948f04..2a0dab6026 100644 --- a/apps/web/app/(app)/environments/[environmentId]/AddProductModal.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/AddProductModal.tsx @@ -1,7 +1,7 @@ "use client"; +import { createProductAction } from "@/app/(app)/environments/[environmentId]/actions"; import Modal from "@/components/shared/Modal"; -import { createProduct } from "@/lib/products/products"; import { Button, Input, Label } from "@formbricks/ui"; import { PlusCircleIcon } from "@heroicons/react/24/outline"; import { useRouter } from "next/navigation"; @@ -19,9 +19,9 @@ export default function AddProductModal({ environmentId, open, setOpen }: AddPro const [loading, setLoading] = useState(false); const { register, handleSubmit } = useForm(); - const submitProduct = async (data) => { + const submitProduct = async (data: { name: string }) => { setLoading(true); - const newEnv = await createProduct(environmentId, data); + const newEnv = await createProductAction(environmentId, data.name); router.push(`/environments/${newEnv.id}/`); setOpen(false); setLoading(false); diff --git a/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx b/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx index 701c29c1de..8e5296e288 100644 --- a/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/EnvironmentsNavbar.tsx @@ -1,7 +1,7 @@ export const revalidate = REVALIDATION_INTERVAL; import Navigation from "@/app/(app)/environments/[environmentId]/Navigation"; -import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { IS_FORMBRICKS_CLOUD, REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; import { getEnvironment, getEnvironments } from "@formbricks/lib/services/environment"; import { getProducts } from "@formbricks/lib/services/product"; import { getTeamByEnvironmentId, getTeamsByUserId } from "@formbricks/lib/services/team"; @@ -11,6 +11,7 @@ import type { Session } from "next-auth"; interface EnvironmentsNavbarProps { environmentId: string; session: Session; + isFormbricksCloud: boolean; } export default async function EnvironmentsNavbar({ environmentId, session }: EnvironmentsNavbarProps) { @@ -41,6 +42,7 @@ export default async function EnvironmentsNavbar({ environmentId, session }: Env products={products} environments={environments} session={session} + isFormbricksCloud={IS_FORMBRICKS_CLOUD} /> ); } diff --git a/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx b/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx index c2790c8e35..00a054f784 100644 --- a/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/Navigation.tsx @@ -21,7 +21,6 @@ import { formbricksLogout } from "@/lib/formbricks"; import { capitalizeFirstLetter, truncate } from "@/lib/utils"; import formbricks from "@formbricks/js"; import { cn } from "@formbricks/lib/cn"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { TEnvironment } from "@formbricks/types/v1/environment"; import { TProduct } from "@formbricks/types/v1/product"; import { TTeam } from "@formbricks/types/v1/teams"; @@ -71,6 +70,7 @@ interface NavigationProps { team: TTeam; products: TProduct[]; environments: TEnvironment[]; + isFormbricksCloud: boolean; } export default function Navigation({ @@ -80,6 +80,7 @@ export default function Navigation({ session, products, environments, + isFormbricksCloud, }: NavigationProps) { const router = useRouter(); const pathname = usePathname(); @@ -171,7 +172,7 @@ export default function Navigation({ icon: CreditCardIcon, label: "Billing & Plan", href: `/environments/${environment.id}/settings/billing`, - hidden: !IS_FORMBRICKS_CLOUD, + hidden: !isFormbricksCloud, }, ], }, @@ -453,7 +454,7 @@ export default function Navigation({ ))} - {IS_FORMBRICKS_CLOUD && ( + {isFormbricksCloud && (
- +
); } diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed.tsx index 9914eb783d..243a5bb899 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponsesFeed.tsx @@ -3,20 +3,21 @@ import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller"; import SurveyStatusIndicator from "@/components/shared/SurveyStatusIndicator"; import { TResponseWithSurvey } from "@formbricks/types/v1/responses"; import Link from "next/link"; +import { TEnvironment } from "@formbricks/types/v1/environment"; export default function ResponseFeed({ responses, sortByDate, - environmentId, + environment, }: { responses: TResponseWithSurvey[]; sortByDate: boolean; - environmentId: string; + environment: TEnvironment; }) { return ( <> {responses.length === 0 ? ( - + ) : (
{responses @@ -53,12 +54,12 @@ export default function ResponseFeed({
+ href={`/environments/${environment.id}/surveys/${response.survey.id}/summary`}> {response.survey.name}
diff --git a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/page.tsx b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/page.tsx index 02fd8bc4db..9bb2fbf8fd 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/[personId]/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/[personId]/page.tsx @@ -5,8 +5,13 @@ import AttributesSection from "@/app/(app)/environments/[environmentId]/people/[ import ResponseSection from "@/app/(app)/environments/[environmentId]/people/[personId]/(responseSection)/ResponseSection"; import HeadingSection from "@/app/(app)/environments/[environmentId]/people/[personId]/HeadingSection"; import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { getEnvironment } from "@formbricks/lib/services/environment"; export default async function PersonPage({ params }) { + const environment = await getEnvironment(params.environmentId); + if (!environment) { + throw new Error("Environment not found"); + } return (
@@ -15,7 +20,7 @@ export default async function PersonPage({ params }) {
- +
diff --git a/apps/web/app/(app)/environments/[environmentId]/people/page.tsx b/apps/web/app/(app)/environments/[environmentId]/people/page.tsx index 25d2dd64c4..18e234457d 100644 --- a/apps/web/app/(app)/environments/[environmentId]/people/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/people/page.tsx @@ -2,24 +2,49 @@ export const revalidate = REVALIDATION_INTERVAL; import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller"; import { truncateMiddle } from "@/lib/utils"; -import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; -import { getPeople } from "@formbricks/lib/services/person"; +import { PEOPLE_PER_PAGE, REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { getEnvironment } from "@formbricks/lib/services/environment"; +import { getPeople, getPeopleCount } from "@formbricks/lib/services/person"; import { TPerson } from "@formbricks/types/v1/people"; -import { PersonAvatar } from "@formbricks/ui"; +import { Pagination, PersonAvatar } from "@formbricks/ui"; import Link from "next/link"; const getAttributeValue = (person: TPerson, attributeName: string) => 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 [environment, totalPeople] = await Promise.all([ + getEnvironment(params.environmentId), + getPeopleCount(params.environmentId), + ]); + if (!environment) { + throw new Error("Environment not found"); + } + 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 ( <> {people.length === 0 ? ( ) : ( @@ -64,6 +89,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]/settings/SettingsNavbar.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/SettingsNavbar.tsx index 82fd341905..de2e6975ca 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/SettingsNavbar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/SettingsNavbar.tsx @@ -1,9 +1,8 @@ "use client"; -import { useProduct } from "@/lib/products/products"; -import { useTeam } from "@/lib/teams/teams"; import { truncate } from "@/lib/utils"; -import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { TProduct } from "@formbricks/types/v1/product"; +import { TTeam } from "@formbricks/types/v1/teams"; import { Popover, PopoverContent, PopoverTrigger } from "@formbricks/ui"; import { ChevronDownIcon } from "@heroicons/react/20/solid"; import { @@ -25,10 +24,18 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { useMemo, useState } from "react"; -export default function SettingsNavbar({ environmentId }: { environmentId: string }) { +export default function SettingsNavbar({ + environmentId, + isFormbricksCloud, + team, + product, +}: { + environmentId: string; + isFormbricksCloud: boolean; + team: TTeam; + product: TProduct; +}) { const pathname = usePathname(); - const { team } = useTeam(environmentId); - const { product } = useProduct(environmentId); const [mobileNavMenuOpen, setMobileNavMenuOpen] = useState(false); interface NavigationLink { @@ -113,7 +120,7 @@ export default function SettingsNavbar({ environmentId }: { environmentId: strin name: "Billing & Plan", href: `/environments/${environmentId}/settings/billing`, icon: CreditCardIcon, - hidden: !IS_FORMBRICKS_CLOUD, + hidden: !isFormbricksCloud, current: pathname?.includes("/billing"), }, ], @@ -152,21 +159,21 @@ export default function SettingsNavbar({ environmentId }: { environmentId: strin href: "https://formbricks.com/gdpr", icon: LinkIcon, target: "_blank", - hidden: !IS_FORMBRICKS_CLOUD, + hidden: !isFormbricksCloud, }, { name: "Privacy", href: "https://formbricks.com/privacy", icon: LinkIcon, target: "_blank", - hidden: !IS_FORMBRICKS_CLOUD, + hidden: !isFormbricksCloud, }, { name: "Terms", href: "https://formbricks.com/terms", icon: LinkIcon, target: "_blank", - hidden: !IS_FORMBRICKS_CLOUD, + hidden: !isFormbricksCloud, }, { name: "License", @@ -178,7 +185,7 @@ export default function SettingsNavbar({ environmentId }: { environmentId: strin ], }, ], - [environmentId, pathname] + [environmentId, isFormbricksCloud, pathname] ); if (!navigation) return null; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx index 1ad90e1cd5..0ab082d63c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/ApiKeyList.tsx @@ -1,6 +1,6 @@ import EditApiKeys from "./EditApiKeys"; import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; -import { getApiKeys } from "@formbricks/lib/services/apiKey"; +import { getApiKeys } from "@formbricks/lib/apiKey/service"; import { getEnvironments } from "@formbricks/lib/services/environment"; export default async function ApiKeyList({ diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/EditApiKeys.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/EditApiKeys.tsx index c8fd9f4670..1a4fbd04d7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/EditApiKeys.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/EditApiKeys.tsx @@ -34,19 +34,29 @@ export default function EditAPIKeys({ }; const handleDeleteKey = async () => { - await deleteApiKeyAction(activeKey.id); - const updatedApiKeys = apiKeysLocal?.filter((apiKey) => apiKey.id !== activeKey.id) || []; - setApiKeysLocal(updatedApiKeys); - setOpenDeleteKeyModal(false); - toast.success("API Key deleted"); + try { + await deleteApiKeyAction(activeKey.id); + const updatedApiKeys = apiKeysLocal?.filter((apiKey) => apiKey.id !== activeKey.id) || []; + setApiKeysLocal(updatedApiKeys); + toast.success("API Key deleted"); + } catch (e) { + toast.error("Unable to delete API Key"); + } finally { + setOpenDeleteKeyModal(false); + } }; const handleAddAPIKey = async (data) => { - const apiKey = await createApiKeyAction(environmentTypeId, { label: data.label }); - const updatedApiKeys = [...apiKeysLocal!, apiKey]; - setApiKeysLocal(updatedApiKeys); - setOpenAddAPIKeyModal(false); - toast.success("API key created"); + try { + const apiKey = await createApiKeyAction(environmentTypeId, { label: data.label }); + const updatedApiKeys = [...apiKeysLocal!, apiKey]; + setApiKeysLocal(updatedApiKeys); + toast.success("API key created"); + } catch (e) { + toast.error("Unable to create API Key"); + } finally { + setOpenAddAPIKeyModal(false); + } }; return ( diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/actions.ts b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/actions.ts index 3220686f37..1db54a5815 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/actions.ts +++ b/apps/web/app/(app)/environments/[environmentId]/settings/api-keys/actions.ts @@ -1,11 +1,28 @@ "use server"; -import { deleteApiKey, createApiKey } from "@formbricks/lib/services/apiKey"; +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +import { deleteApiKey, createApiKey } from "@formbricks/lib/apiKey/service"; +import { canUserAccessApiKey } from "@formbricks/lib/apiKey/auth"; import { TApiKeyCreateInput } from "@formbricks/types/v1/apiKeys"; +import { getServerSession } from "next-auth"; +import { AuthorizationError } from "@formbricks/types/v1/errors"; export async function deleteApiKeyAction(id: string) { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await canUserAccessApiKey(session.user.id, id); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + return await deleteApiKey(id); } export async function createApiKeyAction(environmentId: string, apiKeyData: TApiKeyCreateInput) { + const session = await getServerSession(authOptions); + if (!session) throw new AuthorizationError("Not authorized"); + + const isAuthorized = await hasUserEnvironmentAccess(session.user.id, environmentId); + if (!isAuthorized) throw new AuthorizationError("Not authorized"); + return await createApiKey(environmentId, apiKeyData); } diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx index d2d21c1c9e..d71594bf91 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/billing/page.tsx @@ -1,6 +1,7 @@ export const revalidate = REVALIDATION_INTERVAL; -import { IS_FORMBRICKS_CLOUD, REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { getTeamByEnvironmentId } from "@formbricks/lib/services/team"; diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/layout.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/layout.tsx index d990621ed0..08561f7430 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/layout.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/layout.tsx @@ -1,15 +1,33 @@ import { Metadata } from "next"; import SettingsNavbar from "./SettingsNavbar"; +import { IS_FORMBRICKS_CLOUD } from "@formbricks/lib/constants"; +import { getTeamByEnvironmentId } from "@formbricks/lib/services/team"; +import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; export const metadata: Metadata = { title: "Settings", }; -export default function SettingsLayout({ children, params }) { +export default async function SettingsLayout({ children, params }) { + const [team, product] = await Promise.all([ + getTeamByEnvironmentId(params.environmentId), + getProductByEnvironmentId(params.environmentId), + ]); + if (!team) { + throw new Error("Team not found"); + } + if (!product) { + throw new Error("Product not found"); + } return ( <>
- +
{children}
diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/EditHighlightBorder.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/EditHighlightBorder.tsx index 093d950c81..71d68b838c 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/EditHighlightBorder.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/EditHighlightBorder.tsx @@ -1,7 +1,6 @@ "use client"; import { cn } from "@formbricks/lib/cn"; -import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants"; import { TProduct } from "@formbricks/types/v1/product"; import { Button, ColorPicker, Label, Switch } from "@formbricks/ui"; import { useState } from "react"; @@ -10,11 +9,12 @@ import { updateProductAction } from "./actions"; interface EditHighlightBorderProps { product: TProduct; + defaultBrandColor: string; } -export const EditHighlightBorder = ({ product }: EditHighlightBorderProps) => { +export const EditHighlightBorder = ({ product, defaultBrandColor }: EditHighlightBorderProps) => { const [showHighlightBorder, setShowHighlightBorder] = useState(product.highlightBorderColor ? true : false); - const [color, setColor] = useState(product.highlightBorderColor || DEFAULT_BRAND_COLOR); + const [color, setColor] = useState(product.highlightBorderColor || defaultBrandColor); const [updatingBorder, setUpdatingBorder] = useState(false); const handleUpdateHighlightBorder = async () => { @@ -32,7 +32,7 @@ export const EditHighlightBorder = ({ product }: EditHighlightBorderProps) => { const handleSwitch = (checked: boolean) => { if (checked) { if (!color) { - setColor(DEFAULT_BRAND_COLOR); + setColor(defaultBrandColor); setShowHighlightBorder(true); } else { setShowHighlightBorder(true); diff --git a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx index f1ce4391ef..8c725ae937 100644 --- a/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/settings/lookandfeel/page.tsx @@ -8,6 +8,7 @@ import { EditFormbricksSignature } from "./EditSignature"; import { EditBrandColor } from "./EditBrandColor"; import { EditPlacement } from "./EditPlacement"; import { EditHighlightBorder } from "./EditHighlightBorder"; +import { DEFAULT_BRAND_COLOR } from "@formbricks/lib/constants"; export default async function ProfileSettingsPage({ params }: { params: { environmentId: string } }) { const product = await getProductByEnvironmentId(params.environmentId); @@ -29,7 +30,7 @@ export default async function ProfileSettingsPage({ params }: { params: { enviro noPadding title="Highlight Border" description="Make sure your users notice the survey you display"> - + Create New Team - {env.NEXT_PUBLIC_INVITE_DISABLED !== "1" && isAdminOrOwner && ( + {!isInviteDisabled && isAdminOrOwner && ( @@ -94,7 +83,7 @@ const SummaryHeader = ({ surveyId, environmentId, survey, surveyBaseUrl }: Summa disabled={isStatusChangeDisabled} style={isStatusChangeDisabled ? { pointerEvents: "none", opacity: 0.5 } : {}}>
- + {survey.status === "inProgress" && "In-progress"} {survey.status === "paused" && "Paused"} @@ -107,7 +96,8 @@ const SummaryHeader = ({ surveyId, environmentId, survey, surveyBaseUrl }: Summa { - triggerSurveyMutate({ status: value }) + const castedValue = value as "draft" | "inProgress" | "paused" | "completed"; + surveyMutateAction({ ...survey, status: castedValue }) .then(() => { toast.success( value === "inProgress" @@ -152,14 +142,14 @@ const SummaryHeader = ({ surveyId, environmentId, survey, surveyBaseUrl }: Summa variant="darkCTA" size="sm" className="flex h-full w-full justify-center px-3 lg:px-6" - href={`/environments/${environmentId}/surveys/${surveyId}/edit`}> + href={`/environments/${environment.id}/surveys/${surveyId}/edit`}> Edit
- +
); }; diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/AddQuestionButton.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/AddQuestionButton.tsx index 8911641ef4..1271d3a195 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/AddQuestionButton.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/AddQuestionButton.tsx @@ -1,10 +1,8 @@ "use client"; -import LoadingSpinner from "@/components/shared/LoadingSpinner"; -import { useProduct } from "@/lib/products/products"; import { getQuestionDefaults, questionTypes, universalQuestionPresets } from "@/lib/questions"; import { cn } from "@formbricks/lib/cn"; -import { ErrorComponent } from "@formbricks/ui"; +import { TProduct } from "@formbricks/types/v1/product"; import { PlusIcon } from "@heroicons/react/24/solid"; import { createId } from "@paralleldrive/cuid2"; import * as Collapsible from "@radix-ui/react-collapsible"; @@ -12,17 +10,12 @@ import { useState } from "react"; interface AddQuestionButtonProps { addQuestion: (question: any) => void; - environmentId: string; + product: TProduct; } -export default function AddQuestionButton({ addQuestion, environmentId }: AddQuestionButtonProps) { - const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId); - +export default function AddQuestionButton({ addQuestion, product }: AddQuestionButtonProps) { const [open, setOpen] = useState(false); - if (isLoadingProduct) return ; - if (isErrorProduct) return ; - return ( void; activeQuestionId: string | null; setActiveQuestionId: (questionId: string | null) => void; - environmentId: string; + product: TProduct; invalidQuestions: String[] | null; setInvalidQuestions: (invalidQuestions: String[] | null) => void; } @@ -28,7 +29,7 @@ export default function QuestionsView({ setActiveQuestionId, localSurvey, setLocalSurvey, - environmentId, + product, invalidQuestions, setInvalidQuestions, }: QuestionsViewProps) { @@ -58,7 +59,7 @@ export default function QuestionsView({ }; // function to validate individual questions - const validateSurvey = (question: Question) => { + const validateSurvey = (question: TSurveyQuestion) => { // prevent this function to execute further if user hasnt still tried to save the survey if (invalidQuestions === null) { return; @@ -212,7 +213,7 @@ export default function QuestionsView({
- +
{ if (autoComplete) { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx index ead7a41912..6f3d1f29d9 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx @@ -46,7 +46,7 @@ export default function SurveyEditor({ // when the survey type changes, we need to reset the active question id to the first question useEffect(() => { - if (localSurvey && localSurvey.questions?.length > 0) { + if (localSurvey?.questions?.length && localSurvey.questions.length > 0) { setActiveQuestionId(localSurvey.questions[0].id); } }, [localSurvey?.type]); @@ -77,7 +77,7 @@ export default function SurveyEditor({ setLocalSurvey={setLocalSurvey} activeQuestionId={activeQuestionId} setActiveQuestionId={setActiveQuestionId} - environmentId={environment.id} + product={product} invalidQuestions={invalidQuestions} setInvalidQuestions={setInvalidQuestions} /> diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx index 822ff1c9fe..5fe3abef71 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyMenuBar.tsx @@ -1,10 +1,12 @@ "use client"; -import React from "react"; import AlertDialog from "@/components/shared/AlertDialog"; import DeleteDialog from "@/components/shared/DeleteDialog"; import SurveyStatusDropdown from "@/components/shared/SurveyStatusDropdown"; import type { Survey } from "@formbricks/types/surveys"; +import { TEnvironment } from "@formbricks/types/v1/environment"; +import { TProduct } from "@formbricks/types/v1/product"; +import { TSurvey, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { Button, Input } from "@formbricks/ui"; import { ArrowLeftIcon, Cog8ToothIcon, ExclamationTriangleIcon } from "@heroicons/react/24/solid"; import { isEqual } from "lodash"; @@ -12,10 +14,7 @@ import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import toast from "react-hot-toast"; import { validateQuestion } from "./Validation"; -import { TSurvey, TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { deleteSurveyAction, surveyMutateAction } from "./actions"; -import { TProduct } from "@formbricks/types/v1/product"; -import { TEnvironment } from "@formbricks/types/v1/environment"; interface SurveyMenuBarProps { localSurvey: TSurveyWithAnalytics; @@ -197,16 +196,16 @@ export default function SurveyMenuBar({ {!!localSurvey.analytics.responseRate && (
-

- This survey received responses. To keep the data consistent, make changes with caution. +

+ This survey received responses, make changes with caution.

)}
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 5b46da1f36..6db7a9c3f7 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 @@ -16,7 +16,7 @@ import { } from "@formbricks/ui"; import { CheckCircleIcon, PlusIcon, TrashIcon } from "@heroicons/react/24/solid"; import * as Collapsible from "@radix-ui/react-collapsible"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { TSurveyWithAnalytics } from "@formbricks/types/v1/surveys"; import { TActionClass } from "@formbricks/types/v1/actionClasses"; @@ -40,30 +40,36 @@ export default function WhenToSendCard({ const autoClose = localSurvey.autoClose !== null; - let newTrigger = { - id: "", // Set the appropriate value for the id - createdAt: new Date(), - updatedAt: new Date(), - name: "", - type: "code" as const, // Set the appropriate value for the type - environmentId: "", - description: null, - noCodeConfig: null, - }; + let newTrigger = useMemo( + () => ({ + id: "", // Set the appropriate value for the id + createdAt: new Date(), + updatedAt: new Date(), + name: "", + type: "code" as const, // Set the appropriate value for the type + environmentId: "", + description: null, + noCodeConfig: null, + }), + [] + ); - const addTriggerEvent = () => { + const addTriggerEvent = useCallback(() => { const updatedSurvey = { ...localSurvey }; updatedSurvey.triggers = [...localSurvey.triggers, newTrigger]; setLocalSurvey(updatedSurvey); - }; + }, [newTrigger, localSurvey, setLocalSurvey]); - const setTriggerEvent = (idx: number, actionClassId: string) => { - const updatedSurvey = { ...localSurvey }; - updatedSurvey.triggers[idx] = actionClassArray!.find((actionClass) => { - return actionClass.id === actionClassId; - })!; - setLocalSurvey(updatedSurvey); - }; + const setTriggerEvent = useCallback( + (idx: number, actionClassId: string) => { + const updatedSurvey = { ...localSurvey }; + updatedSurvey.triggers[idx] = actionClassArray!.find((actionClass) => { + return actionClass.id === actionClassId; + })!; + setLocalSurvey(updatedSurvey); + }, + [actionClassArray, localSurvey, setLocalSurvey] + ); const removeTriggerEvent = (idx: number) => { const updatedSurvey = { ...localSurvey }; @@ -95,11 +101,19 @@ export default function WhenToSendCard({ const updatedSurvey: TSurveyWithAnalytics = { ...localSurvey, delay: value }; setLocalSurvey(updatedSurvey); }; + useEffect(() => { if (activeIndex !== null) { - setTriggerEvent(activeIndex, actionClassArray[actionClassArray.length - 1].id); + const newActionClassId = actionClassArray[actionClassArray.length - 1].id; + const currentActionClassId = localSurvey.triggers[activeIndex]?.id; + + if (newActionClassId !== currentActionClassId) { + setTriggerEvent(activeIndex, newActionClassId); + } + + setActiveIndex(null); } - }, [actionClassArray]); + }, [actionClassArray, activeIndex, setTriggerEvent]); useEffect(() => { if (localSurvey.type === "link") { @@ -112,7 +126,7 @@ export default function WhenToSendCard({ if (localSurvey.triggers.length === 0) { addTriggerEvent(); } - }, []); + }, [addTriggerEvent, localSurvey.triggers.length]); return ( <> @@ -166,45 +180,46 @@ export default function WhenToSendCard({
- {localSurvey.triggers?.map((triggerEventClass, idx) => ( -
-
-

{idx === 0 ? "When" : "or"}

- -

action is performed

- + {!isAddEventModalOpen && + localSurvey.triggers?.map((triggerEventClass, idx) => ( +
+
+

{idx === 0 ? "When" : "or"}

+ +

action is performed

+ +
-
- ))} + ))}
- +
diff --git a/apps/web/app/(auth)/auth/signup/page.tsx b/apps/web/app/(auth)/auth/signup/page.tsx index a5f519967d..76f3b02d47 100644 --- a/apps/web/app/(auth)/auth/signup/page.tsx +++ b/apps/web/app/(auth)/auth/signup/page.tsx @@ -1,15 +1,25 @@ -"use client"; - import Link from "next/link"; -import { useSearchParams } from "next/navigation"; import { SignupForm } from "@/components/auth/SignupForm"; import FormWrapper from "@/components/auth/FormWrapper"; import Testimonial from "@/components/auth/Testimonial"; -import { env } from "@/env.mjs"; +import { + EMAIL_VERIFICATION_DISABLED, + GITHUB_OAUTH_ENABLED, + GOOGLE_OAUTH_ENABLED, + INVITE_DISABLED, + PASSWORD_RESET_DISABLED, + PRIVACY_URL, + SIGNUP_ENABLED, + TERMS_URL, + WEBAPP_URL, +} from "@formbricks/lib/constants"; -export default function SignUpPage() { - const searchParams = useSearchParams(); - const inviteToken = searchParams?.get("inviteToken"); +export default function SignUpPage({ + searchParams, +}: { + searchParams: { [key: string]: string | string[] | undefined }; +}) { + const inviteToken = searchParams["inviteToken"] ?? null; return (
@@ -18,9 +28,7 @@ export default function SignUpPage() {
- {( - inviteToken ? env.NEXT_PUBLIC_INVITE_DISABLED === "1" : env.NEXT_PUBLIC_SIGNUP_DISABLED === "1" - ) ? ( + {(inviteToken ? INVITE_DISABLED : !SIGNUP_ENABLED) ? ( <>

Sign up disabled

@@ -35,7 +43,15 @@ export default function SignUpPage() { ) : ( - + )}

diff --git a/apps/web/app/(auth)/invite/page.tsx b/apps/web/app/(auth)/invite/page.tsx index 67a8a4dc81..473168ce96 100644 --- a/apps/web/app/(auth)/invite/page.tsx +++ b/apps/web/app/(auth)/invite/page.tsx @@ -2,7 +2,6 @@ import { sendInviteAcceptedEmail } from "@/lib/email"; import { verifyInviteToken } from "@formbricks/lib/jwt"; import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; import { getServerSession } from "next-auth"; -import { env } from "process"; import { prisma } from "@formbricks/database"; import { NotLoggedInContent, @@ -11,6 +10,7 @@ import { UsedContent, RightAccountContent, } from "./InviteContentComponents"; +import { env } from "@/env.mjs"; export default async function JoinTeam({ searchParams }) { const currentUser = await getServerSession(authOptions); 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/auth/[...nextauth]/authOptions.ts b/apps/web/app/api/auth/[...nextauth]/authOptions.ts index 3c61f999d4..5dbdefac8b 100644 --- a/apps/web/app/api/auth/[...nextauth]/authOptions.ts +++ b/apps/web/app/api/auth/[...nextauth]/authOptions.ts @@ -1,8 +1,9 @@ import { env } from "@/env.mjs"; import { verifyPassword } from "@/lib/auth"; -import { verifyToken } from "@formbricks/lib/jwt"; import { prisma } from "@formbricks/database"; -import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; +import { EMAIL_VERIFICATION_DISABLED, INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; +import { verifyToken } from "@formbricks/lib/jwt"; +import { getProfileByEmail } from "@formbricks/lib/services/profile"; import type { IdentityProvider } from "@prisma/client"; import type { NextAuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; @@ -133,42 +134,16 @@ export const authOptions: NextAuthOptions = { ], callbacks: { async jwt({ token }) { - const existingUser = await prisma.user.findFirst({ - where: { email: token.email! }, - select: { - id: true, - createdAt: true, - onboardingCompleted: true, - memberships: { - select: { - teamId: true, - role: true, - team: { - select: { - plan: true, - }, - }, - }, - }, - name: true, - }, - }); + const existingUser = await getProfileByEmail(token?.email!); if (!existingUser) { return token; } - const teams = existingUser.memberships.map((membership) => ({ - id: membership.teamId, - role: membership.role, - plan: membership.team.plan, - })); - const additionalAttributs = { id: existingUser.id, createdAt: existingUser.createdAt, onboardingCompleted: existingUser.onboardingCompleted, - teams, name: existingUser.name, }; @@ -185,14 +160,13 @@ export const authOptions: NextAuthOptions = { // @ts-ignore session.user.onboardingCompleted = token?.onboardingCompleted; // @ts-ignore - session.user.teams = token?.teams; session.user.name = token.name || ""; return session; }, async signIn({ user, account }: any) { if (account.provider === "credentials" || account.provider === "token") { - if (!user.emailVerified && env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED !== "1") { + if (!user.emailVerified && !EMAIL_VERIFICATION_DISABLED) { return `/auth/verification-requested?email=${encodeURIComponent(user.email)}`; } return true; diff --git a/apps/web/app/api/cron/close_surveys/route.ts b/apps/web/app/api/cron/close_surveys/route.ts index 320571e0bf..41b06ddfb3 100644 --- a/apps/web/app/api/cron/close_surveys/route.ts +++ b/apps/web/app/api/cron/close_surveys/route.ts @@ -1,13 +1,13 @@ -import { env } from "@/env.mjs"; import { responses } from "@/lib/api/response"; import { prisma } from "@formbricks/database"; +import { CRON_SECRET } from "@formbricks/lib/constants"; import { headers } from "next/headers"; export async function POST() { const headersList = headers(); const apiKey = headersList.get("x-api-key"); - if (!apiKey || apiKey !== env.CRON_SECRET) { + if (!apiKey || apiKey !== CRON_SECRET) { return responses.notAuthenticatedResponse(); } diff --git a/apps/web/app/api/google-sheet/callback/route.ts b/apps/web/app/api/google-sheet/callback/route.ts index 8644ce18d8..e2c4540adc 100644 --- a/apps/web/app/api/google-sheet/callback/route.ts +++ b/apps/web/app/api/google-sheet/callback/route.ts @@ -1,6 +1,10 @@ -import { env } from "@/env.mjs"; import { prisma } from "@formbricks/database"; -import { WEBAPP_URL } from "@formbricks/lib/constants"; +import { + GOOGLE_SHEETS_CLIENT_ID, + WEBAPP_URL, + GOOGLE_SHEETS_CLIENT_SECRET, + GOOGLE_SHEETS_REDIRECT_URL, +} from "@formbricks/lib/constants"; import { google } from "googleapis"; import { NextRequest, NextResponse } from "next/server"; @@ -18,9 +22,9 @@ export async function GET(req: NextRequest) { return NextResponse.json({ message: "`code` must be a string" }, { status: 400 }); } - const client_id = env.GOOGLE_SHEETS_CLIENT_ID; - const client_secret = env.GOOGLE_SHEETS_CLIENT_SECRET; - const redirect_uri = env.GOOGLE_SHEETS_REDIRECT_URL; + const client_id = GOOGLE_SHEETS_CLIENT_ID; + const client_secret = GOOGLE_SHEETS_CLIENT_SECRET; + const redirect_uri = GOOGLE_SHEETS_REDIRECT_URL; if (!client_id) return NextResponse.json({ Error: "Google client id is missing" }, { status: 400 }); if (!client_secret) return NextResponse.json({ Error: "Google client secret is missing" }, { status: 400 }); if (!redirect_uri) return NextResponse.json({ Error: "Google redirect url is missing" }, { status: 400 }); diff --git a/apps/web/app/api/google-sheet/route.ts b/apps/web/app/api/google-sheet/route.ts index 63bd97d0a2..9e3864a54d 100644 --- a/apps/web/app/api/google-sheet/route.ts +++ b/apps/web/app/api/google-sheet/route.ts @@ -1,5 +1,9 @@ -import { hasUserEnvironmentAccess } from "@/lib/api/apiHelper"; -import { env } from "@/env.mjs"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +import { + GOOGLE_SHEETS_CLIENT_ID, + GOOGLE_SHEETS_CLIENT_SECRET, + GOOGLE_SHEETS_REDIRECT_URL, +} from "@formbricks/lib/constants"; import { google } from "googleapis"; import { NextRequest, NextResponse } from "next/server"; import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; @@ -15,18 +19,22 @@ export async function GET(req: NextRequest) { const environmentId = req.headers.get("environmentId"); const session = await getServerSession(authOptions); + if (!environmentId) { + return NextResponse.json({ Error: "environmentId is missing" }, { status: 400 }); + } + if (!session) { return NextResponse.json({ Error: "Invalid session" }, { status: 400 }); } - const canUserAccessEnvironment = await hasUserEnvironmentAccess(session?.user, environmentId); + const canUserAccessEnvironment = await hasUserEnvironmentAccess(session?.user.id, environmentId); if (!canUserAccessEnvironment) { return NextResponse.json({ Error: "You dont have access to environment" }, { status: 401 }); } - const client_id = env.GOOGLE_SHEETS_CLIENT_ID; - const client_secret = env.GOOGLE_SHEETS_CLIENT_SECRET; - const redirect_uri = env.GOOGLE_SHEETS_REDIRECT_URL; + const client_id = GOOGLE_SHEETS_CLIENT_ID; + const client_secret = GOOGLE_SHEETS_CLIENT_SECRET; + const redirect_uri = GOOGLE_SHEETS_REDIRECT_URL; if (!client_id) return NextResponse.json({ Error: "Google client id is missing" }, { status: 400 }); if (!client_secret) return NextResponse.json({ Error: "Google client secret is missing" }, { status: 400 }); if (!redirect_uri) return NextResponse.json({ Error: "Google redirect url is missing" }, { status: 400 }); diff --git a/apps/web/app/api/csv-conversion/route.ts b/apps/web/app/api/internal/csv-conversion/route.ts old mode 100644 new mode 100755 similarity index 69% rename from apps/web/app/api/csv-conversion/route.ts rename to apps/web/app/api/internal/csv-conversion/route.ts index 697c9cc15b..96eeb42546 --- a/apps/web/app/api/csv-conversion/route.ts +++ b/apps/web/app/api/internal/csv-conversion/route.ts @@ -1,7 +1,16 @@ import { NextRequest, NextResponse } from "next/server"; import { AsyncParser } from "@json2csv/node"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { responses } from "@/lib/api/response"; export async function POST(request: NextRequest) { + const session = await getServerSession(authOptions); + + if (!session) { + return responses.unauthorizedResponse(); + } + const data = await request.json(); let csv: string = ""; @@ -24,7 +33,7 @@ export async function POST(request: NextRequest) { return NextResponse.json( { - csvResponse: csv, + fileResponse: csv, }, { headers, diff --git a/apps/web/app/api/internal/excel-conversion/route.ts b/apps/web/app/api/internal/excel-conversion/route.ts new file mode 100755 index 0000000000..d6a1263e04 --- /dev/null +++ b/apps/web/app/api/internal/excel-conversion/route.ts @@ -0,0 +1,39 @@ +import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; +import { responses } from "@/lib/api/response"; +import { getServerSession } from "next-auth"; +import { NextRequest, NextResponse } from "next/server"; +import * as xlsx from "xlsx"; + +export async function POST(request: NextRequest) { + const session = await getServerSession(authOptions); + + if (!session) { + return responses.unauthorizedResponse(); + } + + const data = await request.json(); + + const { json, fields, fileName } = data; + + const wb = xlsx.utils.book_new(); + const ws = xlsx.utils.json_to_sheet(json, { header: fields }); + xlsx.utils.book_append_sheet(wb, ws, "Sheet1"); + + const buffer = xlsx.write(wb, { type: "buffer", bookType: "xlsx" }); + + const binaryString = String.fromCharCode.apply(null, buffer); + const base64String = btoa(binaryString); + + const headers = new Headers(); + headers.set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + headers.set("Content-Disposition", `attachment; filename=${fileName ?? "converted"}.xlsx`); + + return NextResponse.json( + { + fileResponse: base64String, + }, + { + headers, + } + ); +} diff --git a/apps/web/app/api/v1/auth.ts b/apps/web/app/api/v1/auth.ts new file mode 100644 index 0000000000..c8403b90bb --- /dev/null +++ b/apps/web/app/api/v1/auth.ts @@ -0,0 +1,39 @@ +import { getApiKeyFromKey } from "@formbricks/lib/apiKey/service"; +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/client/responses/[responseId]/route.ts b/apps/web/app/api/v1/client/responses/[responseId]/route.ts index 8b20b70ff8..64eafcc30a 100644 --- a/apps/web/app/api/v1/client/responses/[responseId]/route.ts +++ b/apps/web/app/api/v1/client/responses/[responseId]/route.ts @@ -2,7 +2,7 @@ import { responses } from "@/lib/api/response"; import { transformErrorToDetails } from "@/lib/api/validator"; import { sendToPipeline } from "@/lib/pipelines"; import { DatabaseError, InvalidInputError, ResourceNotFoundError } from "@formbricks/types/v1/errors"; -import { updateResponse } from "@formbricks/lib/services/response"; +import { updateResponse } from "@formbricks/lib/response/service"; import { getSurvey } from "@formbricks/lib/services/survey"; import { ZResponseUpdateInput } from "@formbricks/types/v1/responses"; import { NextResponse } from "next/server"; diff --git a/apps/web/app/api/v1/client/responses/route.ts b/apps/web/app/api/v1/client/responses/route.ts index a9898b1b92..aac5cbbabc 100644 --- a/apps/web/app/api/v1/client/responses/route.ts +++ b/apps/web/app/api/v1/client/responses/route.ts @@ -3,7 +3,7 @@ import { transformErrorToDetails } from "@/lib/api/validator"; import { sendToPipeline } from "@/lib/pipelines"; import { InvalidInputError } from "@formbricks/types/v1/errors"; import { capturePosthogEvent } from "@formbricks/lib/posthogServer"; -import { createResponse } from "@formbricks/lib/services/response"; +import { createResponse } from "@formbricks/lib/response/service"; import { getSurvey } from "@formbricks/lib/services/survey"; import { getTeamDetails } from "@formbricks/lib/services/teamDetails"; import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/v1/responses"; diff --git a/apps/web/app/api/v1/js/surveys.ts b/apps/web/app/api/v1/js/surveys.ts index 8b53d212ce..21095d928a 100644 --- a/apps/web/app/api/v1/js/surveys.ts +++ b/apps/web/app/api/v1/js/surveys.ts @@ -5,13 +5,13 @@ import { TSurvey } from "@formbricks/types/v1/surveys"; import { unstable_cache } from "next/cache"; const getSurveysCacheTags = (environmentId: string, personId: string): string[] => [ - `env-${environmentId}-surveys`, - `env-${environmentId}-product`, + `environments-${environmentId}-surveys`, + `environments-${environmentId}-product`, personId, ]; const getSurveysCacheKey = (environmentId: string, personId: string): string[] => [ - `env-${environmentId}-person-${personId}-syncSurveys`, + `environments-${environmentId}-person-${personId}-syncSurveys`, ]; export const getSurveysCached = (environmentId: string, person: TPerson) => diff --git a/apps/web/app/api/v1/js/sync/lib/sync.ts b/apps/web/app/api/v1/js/sync/lib/sync.ts index e1b4385c31..e865231f6e 100644 --- a/apps/web/app/api/v1/js/sync/lib/sync.ts +++ b/apps/web/app/api/v1/js/sync/lib/sync.ts @@ -1,7 +1,7 @@ import { getSurveysCached } from "@/app/api/v1/js/surveys"; import { MAU_LIMIT } from "@formbricks/lib/constants"; -import { getActionClassesCached } from "@formbricks/lib/services/actionClass"; -import { getEnvironmentCached } from "@formbricks/lib/services/environment"; +import { getActionClasses } from "@formbricks/lib/actionClass/service"; +import { getEnvironment } from "@formbricks/lib/services/environment"; import { createPerson, getMonthlyActivePeopleCount, getPersonCached } from "@formbricks/lib/services/person"; import { getProductByEnvironmentIdCached } from "@formbricks/lib/services/product"; import { createSession, extendSession, getSessionCached } from "@formbricks/lib/services/session"; @@ -26,7 +26,7 @@ export const getUpdatedState = async ( let session: TSession | null; // check if environment exists - environment = await getEnvironmentCached(environmentId); + environment = await getEnvironment(environmentId); if (!environment) { throw new Error("Environment does not exist"); @@ -101,7 +101,7 @@ export const getUpdatedState = async ( // get/create rest of the state const [surveys, noCodeActionClasses, product] = await Promise.all([ getSurveysCached(environmentId, person), - getActionClassesCached(environmentId), + getActionClasses(environmentId), getProductByEnvironmentIdCached(environmentId), ]); 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..8c1d26eb11 --- /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/actionClass/service"; +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..ac8d492c06 --- /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/actionClass/service"; +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..7941a82eea --- /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/response/service"; +import { TResponse, ZResponseUpdateInput } from "@formbricks/types/v1/responses"; +import { hasUserEnvironmentAccess } from "@formbricks/lib/environment/auth"; +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.id, 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..3fab5f3a3b --- /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/response/service"; +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/app/api/v1/users/route.ts b/apps/web/app/api/v1/users/route.ts index 148df1729e..16fefd3f2c 100644 --- a/apps/web/app/api/v1/users/route.ts +++ b/apps/web/app/api/v1/users/route.ts @@ -3,13 +3,18 @@ import { verifyInviteToken } from "@formbricks/lib/jwt"; import { populateEnvironment } from "@/lib/populate"; import { prisma } from "@formbricks/database"; import { NextResponse } from "next/server"; -import { env } from "@/env.mjs"; import { Prisma } from "@prisma/client"; -import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants"; +import { + EMAIL_VERIFICATION_DISABLED, + INTERNAL_SECRET, + INVITE_DISABLED, + SIGNUP_ENABLED, + WEBAPP_URL, +} from "@formbricks/lib/constants"; export async function POST(request: Request) { let { inviteToken, ...user } = await request.json(); - if (inviteToken ? env.NEXT_PUBLIC_INVITE_DISABLED === "1" : env.NEXT_PUBLIC_SIGNUP_DISABLED === "1") { + if (inviteToken ? INVITE_DISABLED : !SIGNUP_ENABLED) { return NextResponse.json({ error: "Signup disabled" }, { status: 403 }); } user = { ...user, ...{ email: user.email.toLowerCase() } }; @@ -117,7 +122,7 @@ export async function POST(request: Request) { await prisma.invite.delete({ where: { id: inviteId } }); } - if (env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED !== "1") { + if (!EMAIL_VERIFICATION_DISABLED) { await sendVerificationEmail(userData); } return NextResponse.json(userData); diff --git a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts index e500401fcd..f0d51dcba9 100644 --- a/apps/web/app/api/v1/webhooks/[webhookId]/route.ts +++ b/apps/web/app/api/v1/webhooks/[webhookId]/route.ts @@ -1,5 +1,5 @@ import { responses } from "@/lib/api/response"; -import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey"; +import { getApiKeyFromKey } from "@formbricks/lib/apiKey/service"; import { deleteWebhook, getWebhook } from "@formbricks/lib/services/webhook"; import { headers } from "next/headers"; diff --git a/apps/web/app/api/v1/webhooks/route.ts b/apps/web/app/api/v1/webhooks/route.ts index 31d2d77a88..ae1fd52d05 100644 --- a/apps/web/app/api/v1/webhooks/route.ts +++ b/apps/web/app/api/v1/webhooks/route.ts @@ -1,7 +1,7 @@ import { responses } from "@/lib/api/response"; import { transformErrorToDetails } from "@/lib/api/validator"; +import { getApiKeyFromKey } from "@formbricks/lib/apiKey/service"; import { DatabaseError, InvalidInputError } from "@formbricks/types/v1/errors"; -import { getApiKeyFromKey } from "@formbricks/lib/services/apiKey"; import { createWebhook, getWebhooks } from "@formbricks/lib/services/webhook"; import { ZWebhookInput } from "@formbricks/types/v1/webhooks"; import { headers } from "next/headers"; diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 8c0a36a8b7..30ac126219 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,28 +1,10 @@ import ClientLogout from "@/app/ClientLogout"; import { authOptions } from "@/app/api/auth/[...nextauth]/authOptions"; -import { WEBAPP_URL } from "@formbricks/lib/constants"; +import { getEnvironmentByUser } from "@formbricks/lib/services/environment"; import type { Session } from "next-auth"; import { getServerSession } from "next-auth"; -import { headers } from "next/headers"; import { redirect } from "next/navigation"; -async function getEnvironment() { - const cookie = headers().get("cookie") || ""; - const res = await fetch(`${WEBAPP_URL}/api/v1/environments/find-first`, { - headers: { - cookie, - }, - }); - - if (!res.ok) { - const error = await res.json(); - console.error(error); - throw new Error("Failed to fetch data"); - } - - return res.json(); -} - export default async function Home() { const session: Session | null = await getServerSession(authOptions); @@ -36,7 +18,7 @@ export default async function Home() { let environment; try { - environment = await getEnvironment(); + environment = await getEnvironmentByUser(session?.user); } catch (error) { console.error("error getting environment", error); } diff --git a/apps/web/app/s/[surveyId]/LegalFooter.tsx b/apps/web/app/s/[surveyId]/LegalFooter.tsx index 583d4582ef..dbe33b4815 100644 --- a/apps/web/app/s/[surveyId]/LegalFooter.tsx +++ b/apps/web/app/s/[surveyId]/LegalFooter.tsx @@ -1,19 +1,19 @@ -import { env } from "@/env.mjs"; +import { IMPRINT_URL, PRIVACY_URL } from "@formbricks/lib/constants"; import Link from "next/link"; export default function LegalFooter() { - if (!env.NEXT_PUBLIC_IMPRINT_URL && !env.NEXT_PUBLIC_PRIVACY_URL) return null; + if (!IMPRINT_URL && !PRIVACY_URL) return null; return (
- {env.NEXT_PUBLIC_IMPRINT_URL && ( - + {IMPRINT_URL && ( + Imprint )} - {env.NEXT_PUBLIC_IMPRINT_URL && env.NEXT_PUBLIC_PRIVACY_URL && | } - {env.NEXT_PUBLIC_PRIVACY_URL && ( - + {IMPRINT_URL && PRIVACY_URL && | } + {PRIVACY_URL && ( + Privacy Policy )} diff --git a/apps/web/app/s/[surveyId]/LinkSurvey.tsx b/apps/web/app/s/[surveyId]/LinkSurvey.tsx index 1b7e815d51..c3b1cc28d8 100644 --- a/apps/web/app/s/[surveyId]/LinkSurvey.tsx +++ b/apps/web/app/s/[surveyId]/LinkSurvey.tsx @@ -3,7 +3,6 @@ import ContentWrapper from "@/components/shared/ContentWrapper"; import { SurveyInline } from "@/components/shared/Survey"; import { createDisplay } from "@formbricks/lib/client/display"; -import { WEBAPP_URL } from "@formbricks/lib/constants"; import { ResponseQueue } from "@formbricks/lib/responseQueue"; import { SurveyState } from "@formbricks/lib/surveyState"; import { TProduct } from "@formbricks/types/v1/product"; @@ -21,6 +20,7 @@ interface LinkSurveyProps { personId?: string; emailVerificationStatus?: string; prefillAnswer?: string; + webAppUrl: string; } export default function LinkSurvey({ @@ -29,6 +29,7 @@ export default function LinkSurvey({ personId, emailVerificationStatus, prefillAnswer, + webAppUrl, }: LinkSurveyProps) { const searchParams = useSearchParams(); const isPreview = searchParams?.get("preview") === "true"; @@ -43,7 +44,7 @@ export default function LinkSurvey({ () => new ResponseQueue( { - apiHost: WEBAPP_URL, + apiHost: webAppUrl, retryAttempts: 2, onResponseSendingFailed: (response) => { alert(`Failed to send response: ${JSON.stringify(response, null, 2)}`); @@ -53,7 +54,7 @@ export default function LinkSurvey({ }, surveyState ), - [] + [personId, webAppUrl] ); const [autoFocus, setAutofocus] = useState(false); @@ -64,6 +65,10 @@ export default function LinkSurvey({ } }, []); + useEffect(() => { + responseQueue.updateSurveyState(surveyState); + }, [responseQueue, surveyState]); + if (emailVerificationStatus && emailVerificationStatus !== "verified") { if (emailVerificationStatus === "fishy") { return ; @@ -91,12 +96,17 @@ export default function LinkSurvey({ brandColor={product.brandColor} formbricksSignature={product.formbricksSignature} onDisplay={async () => { - const display = await createDisplay({ surveyId: survey.id }, window?.location?.origin); - setDisplayId(display.id); + if (!isPreview) { + const { id } = await createDisplay({ surveyId: survey.id }, window?.location?.origin); + setDisplayId(id); + const newSurveyState = surveyState.copy(); + newSurveyState.updateDisplayId(id); + setSurveyState(newSurveyState); + } }} onResponse={(responseUpdate: TResponseUpdate) => { responseUpdate.displayId = displayId!; - responseQueue.add(responseUpdate); + !isPreview && responseQueue.add(responseUpdate); }} onActiveQuestionChange={(questionId) => setActiveQuestionId(questionId)} activeQuestionId={activeQuestionId} diff --git a/apps/web/app/s/[surveyId]/page.tsx b/apps/web/app/s/[surveyId]/page.tsx index 5a427d9936..c9fba5ec42 100644 --- a/apps/web/app/s/[surveyId]/page.tsx +++ b/apps/web/app/s/[surveyId]/page.tsx @@ -2,7 +2,7 @@ export const revalidate = REVALIDATION_INTERVAL; import LinkSurvey from "@/app/s/[surveyId]/LinkSurvey"; import SurveyInactive from "@/app/s/[surveyId]/SurveyInactive"; -import { REVALIDATION_INTERVAL } from "@formbricks/lib/constants"; +import { REVALIDATION_INTERVAL, WEBAPP_URL } from "@formbricks/lib/constants"; import { getOrCreatePersonByUserId } from "@formbricks/lib/services/person"; import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; import { getSurvey } from "@formbricks/lib/services/survey"; @@ -59,6 +59,7 @@ export default async function LinkSurveyPage({ params, searchParams }) { personId={person?.id} emailVerificationStatus={emailVerificationStatus} prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null} + webAppUrl={WEBAPP_URL} /> ); } diff --git a/apps/web/components/auth/SigninForm.tsx b/apps/web/components/auth/SigninForm.tsx index 6f0c15fe61..8f205693dd 100644 --- a/apps/web/components/auth/SigninForm.tsx +++ b/apps/web/components/auth/SigninForm.tsx @@ -1,7 +1,6 @@ "use client"; import { GoogleButton } from "@/components/auth/GoogleButton"; -import { env } from "@/env.mjs"; import { Button, PasswordInput } from "@formbricks/ui"; import { XCircleIcon } from "@heroicons/react/24/solid"; import { signIn } from "next-auth/react"; @@ -10,7 +9,17 @@ import { useSearchParams } from "next/navigation"; import { useRef, useState } from "react"; import { GithubButton } from "./GithubButton"; -export const SigninForm = () => { +export const SigninForm = ({ + publicSignUpEnabled, + passwordResetEnabled, + googleOAuthEnabled, + githubOAuthEnabled, +}: { + publicSignUpEnabled: boolean; + passwordResetEnabled: boolean; + googleOAuthEnabled: boolean; + githubOAuthEnabled: boolean; +}) => { const searchParams = useSearchParams(); const emailRef = useRef(null); @@ -79,7 +88,7 @@ export const SigninForm = () => { className="focus:border-brand focus:ring-brand block w-full rounded-md border-slate-300 shadow-sm sm:text-sm" />
- {env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && isPasswordFocused && ( + {passwordResetEnabled && isPasswordFocused && (
{ - {env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED === "1" && ( + {googleOAuthEnabled && ( <> )} - {env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED === "1" && ( + {githubOAuthEnabled && ( <> )}
- {env.NEXT_PUBLIC_SIGNUP_DISABLED !== "1" && ( + {publicSignUpEnabled && (
New to Formbricks?
diff --git a/apps/web/components/auth/SignupForm.tsx b/apps/web/components/auth/SignupForm.tsx index 4e69aced10..9ec41de945 100644 --- a/apps/web/components/auth/SignupForm.tsx +++ b/apps/web/components/auth/SignupForm.tsx @@ -2,7 +2,6 @@ import { GoogleButton } from "@/components/auth/GoogleButton"; import IsPasswordValid from "@/components/auth/IsPasswordValid"; -import { env } from "@/env.mjs"; import { createUser } from "@/lib/users/users"; import { Button, PasswordInput } from "@formbricks/ui"; import { XCircleIcon } from "@heroicons/react/24/solid"; @@ -11,7 +10,23 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useMemo, useRef, useState } from "react"; import { GithubButton } from "./GithubButton"; -export const SignupForm = () => { +export const SignupForm = ({ + webAppUrl, + privacyUrl, + termsUrl, + passwordResetEnabled, + emailVerificationDisabled, + googleOAuthEnabled, + githubOAuthEnabled, +}: { + webAppUrl: string; + privacyUrl: string | undefined; + termsUrl: string | undefined; + passwordResetEnabled: boolean; + emailVerificationDisabled: boolean; + googleOAuthEnabled: boolean; + githubOAuthEnabled: boolean; +}) => { const searchParams = useSearchParams(); const router = useRouter(); const [error, setError] = useState(""); @@ -19,15 +34,13 @@ export const SignupForm = () => { const nameRef = useRef(null); const inviteToken = searchParams?.get("inviteToken"); - // const callbackUrl = env.NEXT_PUBLIC_WEBAPP_URL + "/invite?token=" + inviteToken; - const callbackUrl = useMemo(() => { if (inviteToken) { - return env.NEXT_PUBLIC_WEBAPP_URL + "/invite?token=" + inviteToken; + return webAppUrl + "/invite?token=" + inviteToken; } else { - return env.NEXT_PUBLIC_WEBAPP_URL; + return webAppUrl; } - }, [inviteToken]); + }, [inviteToken, webAppUrl]); const handleSubmit = async (e: any) => { e.preventDefault(); @@ -45,10 +58,9 @@ export const SignupForm = () => { e.target.elements.password.value, inviteToken ); - const url = - env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED === "1" - ? `/auth/signup-without-verification-success` - : `/auth/verification-requested?email=${encodeURIComponent(e.target.elements.email.value)}`; + const url = emailVerificationDisabled + ? `/auth/signup-without-verification-success` + : `/auth/verification-requested?email=${encodeURIComponent(e.target.elements.email.value)}`; router.push(url); } catch (e: any) { @@ -144,7 +156,7 @@ export const SignupForm = () => { className="focus:border-brand focus:ring-brand block w-full rounded-md shadow-sm sm:text-sm" />
- {env.NEXT_PUBLIC_PASSWORD_RESET_DISABLED !== "1" && isPasswordFocused && ( + {passwordResetEnabled && isPasswordFocused && (
{ - {env.NEXT_PUBLIC_GOOGLE_AUTH_ENABLED === "1" && ( + {googleOAuthEnabled && ( <> )} - {env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED === "1" && ( + {githubOAuthEnabled && ( <> )}
- {(env.NEXT_PUBLIC_TERMS_URL || env.NEXT_PUBLIC_PRIVACY_URL) && ( + {(termsUrl || privacyUrl) && (
By signing up, you agree to our
- {env.NEXT_PUBLIC_TERMS_URL && ( - + {termsUrl && ( + Terms of Service )} - {env.NEXT_PUBLIC_TERMS_URL && env.NEXT_PUBLIC_PRIVACY_URL && and } - {env.NEXT_PUBLIC_PRIVACY_URL && ( - + {termsUrl && privacyUrl && and } + {privacyUrl && ( + Privacy Policy. )} diff --git a/apps/web/components/environments/SecondNavBar.tsx b/apps/web/components/environments/SecondNavBar.tsx index 96b3b32e8c..ec553fcf10 100644 --- a/apps/web/components/environments/SecondNavBar.tsx +++ b/apps/web/components/environments/SecondNavBar.tsx @@ -1,27 +1,39 @@ import { cn } from "@formbricks/lib/cn"; import SurveyNavBarName from "@/components/shared/SurveyNavBarName"; import Link from "next/link"; +import { getProductByEnvironmentId } from "@formbricks/lib/services/product"; +import { getSurvey } from "@formbricks/lib/services/survey"; interface SecondNavbarProps { tabs: { id: string; label: string; href: string; icon?: React.ReactNode }[]; activeId: string; surveyId?: string; - environmentId?: string; + environmentId: string; } -export default function SecondNavbar({ +export default async function SecondNavbar({ tabs, activeId, surveyId, environmentId, ...props }: SecondNavbarProps) { + const product = await getProductByEnvironmentId(environmentId!); + if (!product) { + throw new Error("Product not found"); + } + + let survey; + if (surveyId) { + survey = await getSurvey(surveyId); + } + return (
- {surveyId && environmentId && ( - + {survey && environmentId && ( + )}
{" "}