Compare commits

..

15 Commits

Author SHA1 Message Date
review-agent-prime[bot]
3a642bfdd0 Edit apps/web/app/(app)/environments/[environmentId]/surveys/components/Modal.tsx 2024-03-04 16:40:16 +00:00
pandeymangg
7a1af85141 fix: product settings 2024-03-04 22:07:13 +05:30
pandeymangg
2586a3ba3a feat: surveys package styling changes 2024-03-04 15:39:24 +05:30
pandeymangg
77408bf0b0 Merge branch 'main' of https://github.com/formbricks/formbricks into feat/form-styling 2024-03-04 13:19:00 +05:30
pandeymangg
d5b183155b feat: product settings page styling UI and service 2024-03-04 13:18:39 +05:30
pandeymangg
d7fc7995bc wip 2024-02-29 18:37:11 +05:30
pandeymangg
a9d8239a25 UI 2024-02-29 14:07:10 +05:30
pandeymangg
71bdb5095a fix: schema 2024-02-29 11:51:45 +05:30
pandeymangg
b84e322eee Merge branch 'main' into feat/form-styling 2024-02-29 11:49:41 +05:30
pandeymangg
08ccb954f3 fix: styling object 2024-02-28 14:43:10 +05:30
pandeymangg
38c6cb01df fix: styling object 2024-02-28 14:42:38 +05:30
pandeymangg
2c13121487 fix: data migration and product types 2024-02-28 12:04:06 +05:30
pandeymangg
73f1d09dc8 Merge branch 'main' into feat/form-styling 2024-02-28 11:24:42 +05:30
pandeymangg
1f884a408c feat: styling zod schema and data migration 2024-02-28 11:24:27 +05:30
pandeymangg
ed2253dcfc feat: styling zod schema and data migration 2024-02-28 11:23:58 +05:30
516 changed files with 10514 additions and 18107 deletions

View File

@@ -56,14 +56,11 @@ SMTP_PASSWORD=smtpPassword
# Uncomment the variables you would like to use and customize the values.
# Custom local storage path for file uploads
#UPLOADS_DIR=
##############
# S3 STORAGE #
##############
# S3 Storage is required for the file upload in serverless environments like Vercel
# S3 Storage is required for the file uplaod in serverless environments like Vercel
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_REGION=
@@ -165,6 +162,3 @@ ENTERPRISE_LICENSE_KEY=
# Ignore Rate Limiting across the Formbricks app
# RATE_LIMITING_DISABLED=1
# OpenTelemetry URL for tracing
# OPENTELEMETRY_LISTENER_URL=http://localhost:4318/v1/traces

View File

@@ -6,8 +6,6 @@ runs:
- name: Checkout repo
uses: actions/checkout@v3
- uses: ./.github/actions/dangerous-git-checkout
- name: Cache Build
uses: actions/cache@v3
id: cache-build

View File

@@ -24,6 +24,11 @@ jobs:
id-token: write # Only necessary for sigstore/fulcio outside PRs
steps:
- name: Generate Secrets
run: |
echo "NEXTAUTH_SECRET=$(openssl rand -hex 32)" >> $GITHUB_ENV
echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v3
@@ -73,6 +78,9 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
DATABASE_URL=${{ env.DATABASE_URL }}
ENCRYPTION_KEY=${{ env.ENCRYPTION_KEY }}
NEXT_PUBLIC_SENTRY_DSN=${{ env.NEXT_PUBLIC_SENTRY_DSN }}
- name: Sign the images with GitHub OIDC Token

View File

@@ -1,121 +0,0 @@
name: Kamal Deploy
concurrency:
group: deploy-to-kamal
cancel-in-progress: false
on:
push:
branches:
- main
jobs:
Deploy:
runs-on: ubuntu-latest
environment: production
env:
DOCKER_BUILDKIT: 1
IS_FORMBRICKS_CLOUD: ${{ vars.IS_FORMBRICKS_CLOUD }}
WEBAPP_URL: ${{ vars.WEBAPP_URL }}
NEXTAUTH_URL: ${{ vars.NEXTAUTH_URL }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
SHORT_URL_BASE: ${{ vars.SHORT_URL_BASE }}
MAIL_FROM: ${{ secrets.MAIL_FROM }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
PRIVACY_URL: ${{ vars.PRIVACY_URL }}
TERMS_URL: ${{ vars.TERMS_URL }}
IMPRINT_URL: ${{ vars.IMPRINT_URL }}
GITHUB_ID: ${{ secrets.FB_GITHUB_ID }}
GITHUB_SECRET: ${{ secrets.FB_GITHUB_SECRET }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
AZUREAD_CLIENT_ID: ${{ secrets.AZUREAD_CLIENT_ID }}
AZUREAD_CLIENT_SECRET: ${{ secrets.AZUREAD_CLIENT_SECRET }}
AZUREAD_TENANT_ID: ${{ secrets.AZUREAD_TENANT_ID }}
OIDC_CLIENT_ID: ${{ secrets.OIDC_CLIENT_ID }}
OIDC_CLIENT_SECRET: ${{ secrets.OIDC_CLIENT_SECRET }}
OIDC_ISSUER: ${{ secrets.OIDC_ISSUER }}
OIDC_DISPLAY_NAME: ${{ secrets.OIDC_DISPLAY_NAME }}
OIDC_SIGNING_ALGORITHM: ${{ secrets.OIDC_SIGNING_ALGORITHM }}
CRON_SECRET: ${{ secrets.CRON_SECRET }}
ASSET_PREFIX_URL: ${{ vars.ASSET_PREFIX_URL }}
NOTION_OAUTH_CLIENT_ID: ${{ secrets.NOTION_OAUTH_CLIENT_ID }}
NOTION_OAUTH_CLIENT_SECRET: ${{ secrets.NOTION_OAUTH_CLIENT_SECRET }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
GOOGLE_SHEETS_CLIENT_ID: ${{ secrets.GOOGLE_SHEETS_CLIENT_ID }}
GOOGLE_SHEETS_CLIENT_SECRET: ${{ secrets.GOOGLE_SHEETS_CLIENT_SECRET }}
GOOGLE_SHEETS_REDIRECT_URL: ${{ secrets.GOOGLE_SHEETS_REDIRECT_URL }}
AIRTABLE_CLIENT_ID: ${{ secrets.AIRTABLE_CLIENT_ID }}
ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }}
DEFAULT_TEAM_ID: ${{ vars.DEFAULT_TEAM_ID }}
ONBOARDING_DISABLED: ${{ vars.ONBOARDING_DISABLED }}
CUSTOMER_IO_API_KEY: ${{ secrets.CUSTOMER_IO_API_KEY }}
CUSTOMER_IO_SITE_ID: ${{ secrets.CUSTOMER_IO_SITE_ID }}
NEXT_PUBLIC_POSTHOG_API_KEY: ${{ vars.NEXT_PUBLIC_POSTHOG_API_KEY }}
NEXT_PUBLIC_POSTHOG_API_HOST: ${{ vars.NEXT_PUBLIC_POSTHOG_API_HOST }}
NEXT_PUBLIC_FORMBRICKS_API_HOST: ${{ vars.NEXT_PUBLIC_FORMBRICKS_API_HOST }}
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID }}
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID: ${{ vars.NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID }}
NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
NODE_ENV: production
CLOUDFLARE_EMAIL: ${{ secrets.CLOUDFLARE_EMAIL }}
CLOUDFLARE_DNS_API_TOKEN: ${{ secrets.CLOUDFLARE_DNS_API_TOKEN }}
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
S3_SECRET_KEY: ${{ secrets.S3_SECRET_KEY }}
S3_REGION: ${{ vars.S3_REGION }}
S3_BUCKET_NAME: ${{ vars.S3_BUCKET_NAME }}
OPENTELEMETRY_LISTENER_URL: ${{ vars.OPENTELEMETRY_LISTENER_URL }}
RATE_LIMITING_DISABLED: ${{ vars.RATE_LIMITING_DISABLED }}
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true
- name: Install dependencies
run: |
gem install kamal
- uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Create builder
run: docker buildx create --use --name formbricks-gh-actions-builder
if: steps.buildx.outputs.should_create_builder == 'true'
- name: Push env variables to Kamal
run: |
kamal() { command kamal "$@" -c kamal/deploy.yml; }
kamal env push
- name: Run deploy command
run: |
kamal() { command kamal "$@" -c kamal/deploy.yml; }
set +e
DEPLOY_OUTPUT=$(kamal setup 2>&1)
DEPLOY_EXIT_CODE=$?
echo "$DEPLOY_OUTPUT"
if [[ "$DEPLOY_OUTPUT" == *"container not unhealthy (healthy)"* ]]; then
echo "Deployment reported healthy container. Considering as success."
kamal lock release
exit 0
else
exit $DEPLOY_EXIT_CODE
fi
shell: bash

View File

@@ -1,7 +1,7 @@
name: PR Update
on:
pull_request:
pull_request_target:
branches:
- main
merge_group:
@@ -30,7 +30,7 @@ jobs:
- "!(**.md|.github/CODEOWNERS)"
test:
name: Run Unit Tests
name: Run Tests
needs: [changes]
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
uses: ./.github/workflows/test.yml
@@ -58,7 +58,6 @@ jobs:
secrets: inherit
required:
name: PR Check Summary
needs: [lint, test, build, e2e-test]
if: always()
runs-on: ubuntu-latest

View File

@@ -31,6 +31,16 @@ jobs:
id-token: write
steps:
- name: Generate Random NEXTAUTH_SECRET
run: |
SECRET=$(openssl rand -hex 32)
echo "NEXTAUTH_SECRET=$SECRET" >> $GITHUB_ENV
- name: Generate Random ENCRYPTION_KEY
run: |
SECRET=$(openssl rand -hex 32)
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v3
@@ -79,6 +89,10 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
DATABASE_URL=${{ env.DATABASE_URL }}
ENCRYPTION_KEY=${{ env.ENCRYPTION_KEY }}
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker

View File

@@ -14,6 +14,16 @@ jobs:
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/formbricks?schema=public"
steps:
- name: Generate Random NEXTAUTH_SECRET
run: |
SECRET=$(openssl rand -hex 32)
echo "NEXTAUTH_SECRET=$SECRET" >> $GITHUB_ENV
- name: Generate Random ENCRYPTION_KEY
run: |
SECRET=$(openssl rand -hex 32)
echo "ENCRYPTION_KEY=$SECRET" >> $GITHUB_ENV
- name: Checkout Repo
uses: actions/checkout@v2
@@ -42,3 +52,7 @@ jobs:
tags: |
${{ secrets.DOCKER_USERNAME }}/formbricks:${{ env.RELEASE_TAG }}
${{ secrets.DOCKER_USERNAME }}/formbricks:latest
build-args: |
NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }}
DATABASE_URL=${{ env.DATABASE_URL }}
ENCRYPTION_KEY=${{ env.ENCRYPTION_KEY }}

View File

@@ -3,7 +3,7 @@ on:
workflow_call:
jobs:
build:
name: Unit Tests
name: Tests
runs-on: ubuntu-latest
timeout-minutes: 15

View File

@@ -1,14 +0,0 @@
#!/bin/sh
# A sample post-deploy hook
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLE (if set)
# KAMAL_DESTINATION (if set)
# KAMAL_RUNTIME
echo "$KAMAL_PERFORMER deployed $KAMAL_VERSION to $KAMAL_DESTINATION in $KAMAL_RUNTIME seconds"

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooted Traefik on $KAMAL_HOSTS"

View File

@@ -1,51 +0,0 @@
#!/bin/sh
# A sample pre-build hook
#
# Checks:
# 1. We have a clean checkout
# 2. A remote is configured
# 3. The branch has been pushed to the remote
# 4. The version we are deploying matches the remote
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLE (if set)
# KAMAL_DESTINATION (if set)
if [ -n "$(git status --porcelain)" ]; then
echo "Git checkout is not clean, aborting..." >&2
git status --porcelain >&2
exit 1
fi
first_remote=$(git remote)
if [ -z "$first_remote" ]; then
echo "No git remote set, aborting..." >&2
exit 1
fi
current_branch=$(git branch --show-current)
if [ -z "$current_branch" ]; then
echo "Not on a git branch, aborting..." >&2
exit 1
fi
remote_head=$(git ls-remote $first_remote --tags $current_branch | cut -f1)
if [ -z "$remote_head" ]; then
echo "Branch not pushed to remote, aborting..." >&2
exit 1
fi
if [ "$KAMAL_VERSION" != "$remote_head" ]; then
echo "Version ($KAMAL_VERSION) does not match remote HEAD ($remote_head), aborting..." >&2
exit 1
fi
exit 0

View File

@@ -1,47 +0,0 @@
#!/usr/bin/env ruby
# A sample pre-connect check
#
# Warms DNS before connecting to hosts in parallel
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_ROLE (if set)
# KAMAL_DESTINATION (if set)
# KAMAL_RUNTIME
hosts = ENV["KAMAL_HOSTS"].split(",")
results = nil
max = 3
elapsed = Benchmark.realtime do
results = hosts.map do |host|
Thread.new do
tries = 1
begin
Socket.getaddrinfo(host, 0, Socket::AF_UNSPEC, Socket::SOCK_STREAM, nil, Socket::AI_CANONNAME)
rescue SocketError
if tries < max
puts "Retrying DNS warmup: #{host}"
tries += 1
sleep rand
retry
else
puts "DNS warmup failed: #{host}"
host
end
end
tries
end
end.map(&:value)
end
retries = results.sum - hosts.size
nopes = results.count { |r| r == max }
puts "Prewarmed %d DNS lookups in %.2f sec: %d retries, %d failures" % [ hosts.size, elapsed, retries, nopes ]

View File

@@ -1,109 +0,0 @@
#!/usr/bin/env ruby
# A sample pre-deploy hook
#
# Checks the Github status of the build, waiting for a pending build to complete for up to 720 seconds.
#
# Fails unless the combined status is "success"
#
# These environment variables are available:
# KAMAL_RECORDED_AT
# KAMAL_PERFORMER
# KAMAL_VERSION
# KAMAL_HOSTS
# KAMAL_COMMAND
# KAMAL_SUBCOMMAND
# KAMAL_ROLE (if set)
# KAMAL_DESTINATION (if set)
# Only check the build status for production deployments
if ENV["KAMAL_COMMAND"] == "rollback" || ENV["KAMAL_DESTINATION"] != "production"
exit 0
end
require "bundler/inline"
# true = install gems so this is fast on repeat invocations
gemfile(true, quiet: true) do
source "https://rubygems.org"
gem "octokit"
gem "faraday-retry"
end
MAX_ATTEMPTS = 72
ATTEMPTS_GAP = 10
def exit_with_error(message)
$stderr.puts message
exit 1
end
class GithubStatusChecks
attr_reader :remote_url, :git_sha, :github_client, :combined_status
def initialize
@remote_url = `git config --get remote.origin.url`.strip.delete_prefix("https://github.com/")
@git_sha = `git rev-parse HEAD`.strip
@github_client = Octokit::Client.new(access_token: ENV["GITHUB_TOKEN"])
refresh!
end
def refresh!
@combined_status = github_client.combined_status(remote_url, git_sha)
end
def state
combined_status[:state]
end
def first_status_url
first_status = combined_status[:statuses].find { |status| status[:state] == state }
first_status && first_status[:target_url]
end
def complete_count
combined_status[:statuses].count { |status| status[:state] != "pending"}
end
def total_count
combined_status[:statuses].count
end
def current_status
if total_count > 0
"Completed #{complete_count}/#{total_count} checks, see #{first_status_url} ..."
else
"Build not started..."
end
end
end
$stdout.sync = true
puts "Checking build status..."
attempts = 0
checks = GithubStatusChecks.new
begin
loop do
case checks.state
when "success"
puts "Checks passed, see #{checks.first_status_url}"
exit 0
when "failure"
exit_with_error "Checks failed, see #{checks.first_status_url}"
when "pending"
attempts += 1
end
exit_with_error "Checks are still pending, gave up after #{MAX_ATTEMPTS * ATTEMPTS_GAP} seconds" if attempts == MAX_ATTEMPTS
puts checks.current_status
sleep(ATTEMPTS_GAP)
checks.refresh!
end
rescue Octokit::NotFound
exit_with_error "Build status could not be found"
end

View File

@@ -1,3 +0,0 @@
#!/bin/sh
echo "Rebooting Traefik on $KAMAL_HOSTS..."

View File

@@ -3,25 +3,25 @@ import {
ClockIcon,
CogIcon,
CreditCardIcon,
FileBarChartIcon,
HelpCircleIcon,
DocumentChartBarIcon,
HomeIcon,
QuestionMarkCircleIcon,
ScaleIcon,
ShieldCheckIcon,
UsersIcon,
} from "lucide-react";
UserGroupIcon,
} from "@heroicons/react/24/outline";
const navigation = [
{ name: "Home", href: "#", icon: HomeIcon, current: true },
{ name: "History", href: "#", icon: ClockIcon, current: false },
{ name: "Balances", href: "#", icon: ScaleIcon, current: false },
{ name: "Cards", href: "#", icon: CreditCardIcon, current: false },
{ name: "Recipients", href: "#", icon: UsersIcon, current: false },
{ name: "Reports", href: "#", icon: FileBarChartIcon, current: false },
{ name: "Recipients", href: "#", icon: UserGroupIcon, current: false },
{ name: "Reports", href: "#", icon: DocumentChartBarIcon, current: false },
];
const secondaryNavigation = [
{ name: "Settings", href: "#", icon: CogIcon },
{ name: "Help", href: "#", icon: HelpCircleIcon },
{ name: "Help", href: "#", icon: QuestionMarkCircleIcon },
{ name: "Privacy", href: "#", icon: ShieldCheckIcon },
];

View File

@@ -12,8 +12,8 @@
},
"dependencies": {
"@formbricks/js": "workspace:*",
"lucide-react": "^0.356.0",
"next": "14.1.3",
"@heroicons/react": "^2.1.1",
"next": "14.1.1",
"react": "18.2.0",
"react-dom": "18.2.0"
},

View File

@@ -21,25 +21,9 @@ export default function AppPage({}) {
}, [darkMode]);
useEffect(() => {
// enable Formbricks debug mode by adding formbricksDebug=true GET parameter
const addFormbricksDebugParam = () => {
const urlParams = new URLSearchParams(window.location.search);
if (!urlParams.has("formbricksDebug")) {
urlParams.set("formbricksDebug", "true");
const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
window.history.replaceState({}, "", newUrl);
}
};
addFormbricksDebugParam();
if (process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID && process.env.NEXT_PUBLIC_FORMBRICKS_API_HOST) {
const isUserId = window.location.href.includes("userId=true");
const defaultAttributes = {
language: "gu",
};
const userInitAttributes = { "Init Attribute 1": "eight", "Init Attribute 2": "two" };
const attributes = isUserId ? { ...defaultAttributes, ...userInitAttributes } : defaultAttributes;
const attributes = isUserId ? { "Init Attribute 1": "eight", "Init Attribute 2": "two" } : undefined;
const userId = isUserId ? "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING" : undefined;
formbricks.init({
environmentId: process.env.NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID,
@@ -85,7 +69,7 @@ export default function AppPage({}) {
<div className="rounded-lg border border-slate-300 bg-slate-100 p-6 dark:border-slate-600 dark:bg-slate-900">
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">1. Setup .env</h3>
<p className="text-slate-700 dark:text-slate-300">
Copy the environment ID of your Formbricks app to the env variable in /apps/demo/.env
Copy the environment ID of your Formbricks app to the env variable in demo/.env
</p>
<Image src={fbsetup} alt="fb setup" className="mt-4 rounded" priority />

View File

@@ -1,14 +1,13 @@
import { Fence } from "@/components/shared/Fence";
import { generateManagementApiMetadata } from "@/lib/utils";
import {generateManagementApiMetadata} from "@/lib/utils"
export const metadata = generateManagementApiMetadata("Surveys", ["Fetch", "Create", "Update", "Delete"]);
export const metadata = generateManagementApiMetadata("Surveys",["Fetch","Create","Update","Delete"])
#### Management API
# Surveys API
This set of API can be used to
- [List All Surveys](#list-all-surveys)
- [Get Survey](#get-survey-by-id)
- [Create Survey](#create-survey)
@@ -23,7 +22,8 @@ This set of API can be used to
<Row>
<Col>
Retrieve all the surveys you have for the environment with pagination.
Retrieve all the surveys you have for the environment.
### Mandatory Headers
@@ -33,26 +33,14 @@ This set of API can be used to
</Property>
</Properties>
### Query Parameters
<Properties>
<Property name="offset" type="number">
The number of surveys to skip before returning the results.
</Property>
<Property name="limit" type="number">
The number of surveys to return.
</Property>
</Properties>
</Col>
<Col sticky>
<CodeGroup title="Request" tag="GET" label="/api/v1/management/surveys">
```bash {{ title: 'cURL' }}
curl --location \
'https://app.formbricks.com/api/v1/management/surveys?offset=20&limit=10' \
'https://app.formbricks.com/api/v1/management/surveys' \
--header \
'x-api-key: <your-api-key>'
```
@@ -415,6 +403,7 @@ This set of API can be used to
```
</CodeGroup>
</Col>
<Col sticky>
@@ -464,7 +453,7 @@ This set of API can be used to
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
@@ -508,6 +497,7 @@ This set of API can be used to
```
</CodeGroup>
</Col>
<Col sticky>
@@ -578,7 +568,7 @@ This set of API can be used to
}
}
```
```json {{ title: '401 Not Authenticated' }}
{
"code": "not_authenticated",
@@ -595,6 +585,7 @@ This set of API can be used to
---
## Delete Survey by ID {{ tag: 'DELETE', label: '/api/v1/management/surveys/<survey-id>' }}
<Row>

View File

@@ -99,6 +99,7 @@ if (typeof window !== "undefined") {
formbricks.init({
environmentId: "<environment-id>",
apiHost: "<api-host>",
debug: true, // remove when in production
});
}
@@ -130,7 +131,7 @@ The app initializes 'formbricks' when it's loaded in a browser environment (due
<Image
src={ReactApp}
alt="In-app survey in React app for micro surveys"
alt="In app survey in React app for micro surveys"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
@@ -181,6 +182,7 @@ useEffect(() => {
formbricks.init({
environmentId: "<environment-id>",
apiHost: "<api-host>",
debug: true, // remove when in production
});
}, []);
@@ -230,6 +232,7 @@ if (typeof window !== "undefined") {
formbricks.init({
environmentId: "<environment-id>",
apiHost: "<api-host>",
debug: true, // remove when in production
});
}
@@ -266,6 +269,14 @@ Refer to our [Example NextJS Pages Directory project](https://github.com/formbri
</Property>
</Properties>
### Optional Customizations to be Made
<Properties>
<Property name="debug" type="boolean">
Whether you want to see debug messages from Formbricks on your client-side console.
</Property>
</Properties>
### What are we doing here?
First we need to initialize the Formbricks SDK, making sure it only runs on the client side.
@@ -347,6 +358,14 @@ router.afterEach((to, from) => {
</Property>
</Properties>
### Optional Customizations to be Made
<Properties>
<Property name="debug" type="boolean">
Whether you want to see debug messages from Formbricks on your client-side console.
</Property>
</Properties>
Refer to our [Example VueJs project](https://github.com/formbricks/examples/tree/main/vuejs) for more help! Now visit the [Validate your Setup](#validate-your-setup) section to verify your setup!
## Validate your setup
@@ -377,9 +396,10 @@ Enabling Formbricks debug mode in your browser is a useful troubleshooting step
To activate Formbricks debug mode:
1. **Via URL Parameter:**
1. **In Your Integration Code:**
- Enable debug mode mode by adding `?formbricksDebug=true` to your application's URL (e.g. `https://example.com?formbricksDebug=true` or `https://example.com?page=123&formbricksDebug=true`). This parameter will enable debugging for the current page.
- Locate the initialization code for Formbricks in your application (HTML, ReactJS, NextJS, VueJS).
- Set the `debug` option to `true` when initializing Formbricks.
2. **View Debug Logs:**
@@ -393,21 +413,29 @@ To activate Formbricks debug mode:
- **Safari:** Press `Option + Command + C` to open the developer tools and navigate to the "Console" tab.
- **Edge:** Press `F12` or right-click, select "Inspect Element," and go to the "Console" tab.
3. **Via URL Parameter:**
- For quick activation, add `?formbricksDebug=true` to your application's URL.
This parameter will enable debugging for the current session.
### Common Use Cases
Debug mode is beneficial for scenarios such as:
- Verifying Formbricks initialization.
- Identifying survey trigger issues.
- Verifying Formbricks functionality.
- Identifying integration issues.
- Troubleshooting unexpected behavior.
### Debug Log Messages
Debug log messages provide insights into:
Specific debug log messages may provide insights into:
- API calls and responses.
- Event tracking, survey triggers and form interactions.
- Initialization errors.
- Event tracking and form interactions.
- Integration errors.
**Note:** Disable debugging in production to prevent unnecessary logs and improve performance.
## Overwrite CSS Styles for In-App Surveys

View File

@@ -1,6 +1,5 @@
import Image from "next/image";
import ReactApp from "../framework-guides/react-in-app-survey-app-popup-form.webp";
import I1 from "./1-in-app-survey-or-popup-survey-setup.webp";
import I2 from "./2-settings-for-survey-popup-in-app-for-feedback.webp";
import I3 from "./3-web-app-survey-settings-for-in-app-survey-popup.webp";
@@ -9,6 +8,7 @@ import I5 from "./5-options-survey-popup-in-app-for-feedback.webp";
import I6 from "./6-setup-in-app-survey-popup-feedback-box.webp";
import I7 from "./7-in-app-survey-popup-for-feedback.webp";
import I8 from "./8-pop-up-form-in-web-app-survey.webp";
import ReactApp from "../framework-guides/react-in-app-survey-app-popup-form.webp";
export const metadata = {
title: "Formbricks Quickstart Guide: In-App Surveys Made Simple",
@@ -20,7 +20,7 @@ export const metadata = {
# Quickstart
In-app surveys have 6-10x better conversion rates than emailed out surveys. This tutorial explains how to run an in-app survey in your web app in 10 to 15 minutes. Lets go!
In app surveys have 6-10x better conversion rates than emailed out surveys. This tutorial explains how to run an in app survey in your web app in 10 to 15 minutes. Lets go!
## Create a free Formbricks Cloud account
@@ -28,7 +28,7 @@ While you can [self-host](/docs/self-hosting/deployment) Formbricks, the quickes
<Image
src={I1}
alt="Choose in-app survey template"
alt="Choose in app survey template"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl "
/>
@@ -59,7 +59,7 @@ Scroll down to Survey Trigger and choose “New Session”. This will cause this
<Image
src={I4}
alt="In-app survey trigger for feedback popup micro survey"
alt="In app survey trigger for feedback popup micro survey"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
@@ -68,7 +68,7 @@ In **Recontact Options** we choose the following settings, so that we can play a
<Image
src={I5}
alt="Options for survey popup in-app micro survey"
alt="Options for survey popup in app micro survey"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
@@ -88,7 +88,7 @@ On the Setup Checklist you have two elements. At the top you find the Widget Sta
<Image
src={I7}
alt="feedback popup in-app survey"
alt="feedback popup in app survey"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
@@ -100,7 +100,7 @@ In the manual below, this code snippet contains all the information you need:
<Image
src={I8}
alt="settings for in-app survey popping up"
alt="settings for in app survey popping up"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
@@ -122,7 +122,7 @@ Now, restart your app in your terminal to make sure the widget is loaded. Once i
<Image
src={ReactApp}
alt="In-app survey in React app for micro surveys"
alt="In app survey in React app for micro surveys"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>

View File

@@ -22,7 +22,7 @@ Go back to [app.formbricks.com](http://app.formbricks.com) or your self-hosted i
<Image
src={I1}
alt="setup checklist ui of survey popup for in-app surveys"
alt="setup checklist ui of survey popup for in app surveys"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
@@ -39,7 +39,7 @@ If your app is connected with Formbricks Cloud, the survey might have not been l
<Image
src={I3}
alt="survey logs for in-app survey pop up micro"
alt="survey logs for in app survey pop up micro"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>
@@ -50,7 +50,7 @@ The widget only loads surveys which are **public** and **in progress**. Go to Fo
<Image
src={I2}
alt="ui of survey popup for in-app micro surveys"
alt="ui of survey popup for in app micro surveys"
quality="100"
className="max-w-full rounded-lg sm:max-w-3xl"
/>

View File

@@ -1,6 +1,7 @@
export const metadata = {
title: "Enterprise License to unlock advanced functionality",
description: "Request a enterprise licenses to unlock advanced enterprise functionality",
description:
"Request a self-hosting licenses to unlock advanced enterprise functionality",
};
#### Self-Hosting
@@ -13,17 +14,13 @@ Additional to the AGPL licensed Formbricks core, the Formbricks repository conta
**Please note:** Sooner than later we will introduce a enterprise license pricing. For a free beta key, fill out this form:
<div
style={{
position: "relative",
height: "100vh",
maxHeight: "100vh",
overflow: "auto",
borderRadius: "12px",
}}>
<div style={{ position: 'relative', height: '100vh', maxHeight: '100vh', overflow: 'auto', borderRadius:'12px' }}>
<iframe
src="https://app.formbricks.com/s/clrf4z8zg1u3912250j7shqb5"
style={{ position: "absolute", left: 0, top: 0, width: "100%", height: "100%", border: 0 }}></iframe>
style={{ position: 'absolute', left: 0, top: 0, width: '100%', height: '100%', border: 0 }}
>
</iframe>
</div>
**Cant figure it out?**: [Join our Discord!](https://formbricks.com/discord)

View File

@@ -76,27 +76,6 @@ GOOGLE_CLIENT_SECRET=your-client-secret-here
- Navigate to your Docker setup directory where your `docker-compose.yml` file is located.
- Run the following command to bring down your current Docker containers and then bring them back up with the updated environment configuration:
## Azure SSO Integration
Have an Azure Active Directory (AAD) instance? Integrate it with your Formbricks instance to allow users to log in using their existing AAD credentials. This guide will walk you through the process of setting up Azure SSO for your Formbricks instance.
### Requirements
- An Azure Active Directory (AAD) instance.
- A Formbricks instance running and accessible.
### Steps
1. Create a new Tenant in Azure Active Directory as per their [official documentation](https://learn.microsoft.com/en-us/entra/fundamentals/create-new-tenant).
2. Add Users & Groups to your AAD instance.
3. Now we need to fill the below environment variables in our Formbricks instance so get them from your AD configuration:
- `AZUREAD_CLIENT_ID`
- `AZUREAD_CLIENT_SECRET`
- `AZUREAD_TENANT_ID`
4. Update these environment variables in your `docker-compose.yml` or pass it like your other environment variables to the Formbricks container.
5. Restart your Formbricks instance.
6. You're all set! Users can now signup & log in using their AAD credentials.
## OpenID Integration
Integrating your own OIDC (OpenID Connect) instance with your Formbricks instance allows users to log in using their OIDC credentials, ensuring a secure and streamlined user experience. Please follow the steps below to set up OIDC for your Formbricks instance.
@@ -136,54 +115,52 @@ OIDC_SIGNING_ALGORITHM=HS256
These variables can be provided at the runtime i.e. in your docker-compose file.
| Variable | Description | Required | Default |
|-----------------------------|----------------------------------------------------------------------------------------------|---------------------------------------------------------|---------------------------|
| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
| DATABASE_URL | Database URL with credentials. | required | |
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
| ENCRYPTION_KEY | Secret for used by Formbricks for data 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` |
| UPLOADS_DIR | Local directory for storing uploads. | optional | `./uploads` |
| S3_ACCESS_KEY | Access key for S3. | optional | (resolved by the AWS SDK) |
| S3_SECRET_KEY | Secret key for S3. | optional | (resolved by the AWS SDK) |
| S3_REGION | Region for S3. | optional | (resolved by the AWS SDK) |
| S3_BUCKET | Bucket name for S3. | optional (required if S3 is enabled) | |
| S3_ENDPOINT | Endpoint for S3. | optional | (resolved by the AWS SDK) |
| 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 | |
| EMAIL_AUTH_DISABLED | Disables the ability for users to signup or login via email and password 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 | |
| RATE_LIMITING_DISABLED | Disables rate limiting 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_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 | |
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | `#64748b` |
| DEFAULT_TEAM_ID | Automatically assign new users to a specific team when joining | optional | |
| DEFAULT_TEAM_ROLE | Role of the user in the default team. | optional | `admin` |
| ONBOARDING_DISABLED | Disables onboarding for new users if set to `1` | optional | |
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have `.well-known` configured at this) | optional (required if OIDC auth is enabled) | |
| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | `RS256` |
| OPENTELEMETRY_LISTENER_URL | URL for OpenTelemetry listener inside Formbricks. | optional | | |
| Variable | Description | Required | Default |
| --------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------- |
| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
| DATABASE_URL | Database URL with credentials. | required | |
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
| ENCRYPTION_KEY | Secret for used by Formbricks for data 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` |
| S3_ACCESS_KEY | Access key for S3. | optional (required if S3 is enabled) | |
| S3_SECRET_KEY | Secret key for S3. | optional (required if S3 is enabled) | |
| S3_REGION | Region for S3. | optional (required if S3 is enabled) | |
| S3_BUCKET | Bucket name for S3. | optional (required if S3 is enabled) | |
| S3_ENDPOINT | Endpoint for S3. | optional (required if S3 is enabled) | |
| 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 | |
| EMAIL_AUTH_DISABLED | Disables the ability for users to signup or login via email and password 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 | |
| RATE_LIMITING_DISABLED | Disables rate limiting 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_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 | |
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | `#64748b` |
| DEFAULT_TEAM_ID | Automatically assign new users to a specific team when joining | optional | |
| DEFAULT_TEAM_ROLE | Role of the user in the default team. | optional | `admin` |
| ONBOARDING_DISABLED | Disables onboarding for new users if set to `1` | optional | |
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have `.well-known` configured at this) | optional (required if OIDC auth is enabled) | |
| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | `RS256` |
## Build-time Variables

View File

@@ -0,0 +1,45 @@
import { Popover } from "@headlessui/react";
import { usePlausible } from "next-plausible";
import Link from "next/link";
import { useRouter } from "next/router";
import { Button } from "@formbricks/ui/Button";
import { FooterLogo } from "../shared/Logo";
export default function HeaderLight() {
const plausible = usePlausible();
const router = useRouter();
return (
<Popover className="relative" as="header">
<div className="max-w-8xl mx-auto flex items-center justify-between py-6 sm:px-2 md:justify-start lg:px-8 xl:px-12">
<div className="flex w-0 flex-1 justify-start">
<Link href="/">
<span className="sr-only">Formbricks</span>
<FooterLogo className="ml-7 h-8 w-auto sm:h-10" />
</Link>
</div>
<div className="hidden flex-1 items-center justify-end md:flex">
<Button
variant="secondary"
onClick={() => {
router.push("https://cal.com/johannes/formbricks-demo");
plausible("Demo_CTA_TalkToUs");
}}>
Talk to us
</Button>
<Button
variant="highlight"
className="ml-2"
onClick={() => {
router.push("https://app.formbricks.com/auth/signup");
plausible("Demo_CTA_TryForFree");
}}>
Start for free
</Button>
</div>
</div>
</Popover>
);
}

View File

@@ -8,14 +8,16 @@ interface LayoutProps {
description: string;
}
export default function LayoutLight({ title, description, children }: LayoutProps) {
export default function Layout({ title, description, children }: LayoutProps) {
return (
<div className="mx-auto w-full">
<MetaInformation title={title} description={description} />
<HeaderLight />
<main className="max-w-8xl relative mx-auto flex w-full flex-col justify-center space-y-24 px-6 lg:space-y-40 lg:px-24 xl:px-36 ">
{children}
</main>
{
<main className="max-w-8xl relative mx-auto flex w-full flex-col justify-center px-2 lg:px-8 xl:px-12">
{children}
</main>
}
<Footer />
</div>
);

View File

@@ -1,4 +1,4 @@
import { PlusIcon, TrashIcon } from "lucide-react";
import { PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
import { useState } from "react";
import { Button } from "@formbricks/ui/Button";

View File

@@ -1,4 +1,4 @@
import { MousePointerClickIcon } from "lucide-react";
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
import { Button } from "@formbricks/ui/Button";
import { Input } from "@formbricks/ui/Input";
@@ -20,7 +20,7 @@ export const AddNoCodeEventModalDummy: React.FC<EventDetailModalProps> = ({ open
<div className="p-4 sm:p-6">
<div className="flex items-center space-x-2">
<div className="h-6 w-6 text-slate-500">
<MousePointerClickIcon className="h-5 w-5" />
<CursorArrowRaysIcon />
</div>
<div>
<div className="text-lg font-medium text-slate-700 dark:text-slate-300">Add Action</div>

View File

@@ -1,6 +1,7 @@
import { TSurveyCTAQuestion } from "@formbricks/types/surveys";
import Headline from "./Headline";
import HtmlBody from "./HtmlBody";
import { TSurveyCTAQuestion } from "./types";
interface CTAQuestionProps {
question: TSurveyCTAQuestion;

View File

@@ -2,9 +2,10 @@
import React, { useEffect, useState } from "react";
import { TTemplate } from "@formbricks/types/templates";
import PreviewSurvey from "./PreviewSurvey";
import { findTemplateByName } from "./templates";
import { TTemplate } from "./types";
interface DemoPreviewProps {
template: string;

View File

@@ -1,9 +1,10 @@
import { useEffect, useState } from "react";
import { TTemplate } from "@formbricks/types/templates";
import PreviewSurvey from "./PreviewSurvey";
import TemplateList from "./TemplateList";
import { templates } from "./templates";
import { TTemplate } from "./types";
export default function SurveyTemplatesPage({}) {
const [activeTemplate, setActiveTemplate] = useState<TTemplate | null>(null);

View File

@@ -1,8 +1,10 @@
import * as DOMPurify from "dompurify";
export default function HtmlBody({ htmlString, questionId }: { htmlString: string; questionId: string }) {
return (
<label
htmlFor={questionId}
className="fb-block fb-font-normal fb-leading-6 text-sm text-slate-500 dark:text-slate-300"
dangerouslySetInnerHTML={{ __html: htmlString }}></label>
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(htmlString) }}></label>
);
}

View File

@@ -1,10 +1,10 @@
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TSurveyMultipleChoiceMultiQuestion } from "@formbricks/types/surveys";
import Headline from "./Headline";
import Subheader from "./Subheader";
import { TSurveyMultipleChoiceMultiQuestion } from "./types";
interface MultipleChoiceMultiProps {
question: TSurveyMultipleChoiceMultiQuestion;

View File

@@ -1,10 +1,10 @@
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TSurveyMultipleChoiceSingleQuestion } from "@formbricks/types/surveys";
import Headline from "./Headline";
import Subheader from "./Subheader";
import { TSurveyMultipleChoiceSingleQuestion } from "./types";
interface MultipleChoiceSingleProps {
question: TSurveyMultipleChoiceSingleQuestion;
@@ -20,7 +20,6 @@ export default function MultipleChoiceSingleQuestion({
brandColor,
}: MultipleChoiceSingleProps) {
const [selectedChoice, setSelectedChoice] = useState<string | null>(null);
return (
<form
onSubmit={(e) => {

View File

@@ -1,10 +1,10 @@
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TSurveyNPSQuestion } from "@formbricks/types/surveys";
import Headline from "./Headline";
import Subheader from "./Subheader";
import { TSurveyNPSQuestion } from "./types";
interface NPSQuestionProps {
question: TSurveyNPSQuestion;

View File

@@ -1,8 +1,9 @@
import { useState } from "react";
import { TSurveyOpenTextQuestion } from "@formbricks/types/surveys";
import Headline from "./Headline";
import Subheader from "./Subheader";
import { TSurveyOpenTextQuestion } from "./types";
interface OpenTextQuestionProps {
question: TSurveyOpenTextQuestion;

View File

@@ -1,9 +1,10 @@
import { useState } from "react";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
import Modal from "./Modal";
import QuestionConditional from "./QuestionConditional";
import ThankYouCard from "./ThankYouCard";
import { TSurvey, TSurveyQuestion } from "./types";
interface PreviewSurveyProps {
localSurvey?: TSurvey;
@@ -66,8 +67,8 @@ export default function PreviewSurvey({
{activeQuestionId == "thank-you-card" ? (
<ThankYouCard
brandColor={brandColor}
headline={localSurvey?.thankYouCard?.headline!}
subheader={localSurvey?.thankYouCard?.subheader!}
headline={localSurvey?.thankYouCard?.headline || ""}
subheader={localSurvey?.thankYouCard?.subheader || ""}
/>
) : (
questions.map(

View File

@@ -1,10 +1,11 @@
import { TSurveyQuestion, TSurveyQuestionType } from "@formbricks/types/surveys";
import CTAQuestion from "./CTAQuestion";
import MultipleChoiceMultiQuestion from "./MultipleChoiceMultiQuestion";
import MultipleChoiceSingleQuestion from "./MultipleChoiceSingleQuestion";
import NPSQuestion from "./NPSQuestion";
import OpenTextQuestion from "./OpenTextQuestion";
import RatingQuestion from "./RatingQuestion";
import { TSurveyQuestion, TSurveyQuestionType } from "./types";
interface QuestionConditionalProps {
question: TSurveyQuestion;

View File

@@ -1,10 +1,10 @@
import { useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TSurveyRatingQuestion } from "@formbricks/types/surveys";
import Headline from "./Headline";
import Subheader from "./Subheader";
import { TSurveyRatingQuestion } from "./types";
interface RatingQuestionProps {
question: TSurveyRatingQuestion;

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from "react";
import { cn } from "@formbricks/lib/cn";
import { TTemplate } from "@formbricks/types/templates";
import { templates } from "./templates";
import { TTemplate } from "./types";
type TemplateList = {
onTemplateClick: (template: TTemplate) => void;

View File

@@ -1,6 +1,7 @@
import { createId } from "@paralleldrive/cuid2";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import { TTemplate } from "@formbricks/types/templates";
import {
AppPieChartIcon,
ArrowRightCircleIcon,
@@ -26,17 +27,14 @@ import {
VideoTabletAdjustIcon,
} from "@formbricks/ui/icons";
import { TTemplate } from "./types";
const thankYouCardDefault = {
enabled: true,
headline: "Thank you!",
subheader: "TWe appreciate your feedback.",
subheader: "We appreciate your feedback.",
};
const welcomeCardDefault = {
enabled: true,
headline: "Welcome!",
timeToFinish: false,
showResponseCount: false,
};

View File

@@ -1,501 +0,0 @@
import z from "zod";
export enum TSurveyQuestionType {
FileUpload = "fileUpload",
OpenText = "openText",
MultipleChoiceSingle = "multipleChoiceSingle",
MultipleChoiceMulti = "multipleChoiceMulti",
NPS = "nps",
CTA = "cta",
Rating = "rating",
Consent = "consent",
PictureSelection = "pictureSelection",
Cal = "cal",
Date = "date",
}
export const ZAllowedFileExtension = z.enum([
"png",
"jpeg",
"jpg",
"pdf",
"doc",
"docx",
"xls",
"xlsx",
"ppt",
"pptx",
"plain",
"csv",
"mp4",
"mov",
"avi",
"mkv",
"webm",
"zip",
"rar",
"7z",
"tar",
]);
export type TAllowedFileExtension = z.infer<typeof ZAllowedFileExtension>;
export const ZUserObjective = z.enum([
"increase_conversion",
"improve_user_retention",
"increase_user_adoption",
"sharpen_marketing_messaging",
"support_sales",
"other",
]);
export type TUserObjective = z.infer<typeof ZUserObjective>;
export const ZSurveyWelcomeCard = z.object({
enabled: z.boolean(),
headline: z.string().optional(),
html: z.string().optional(),
fileUrl: z.string().optional(),
buttonLabel: z.string().optional(),
timeToFinish: z.boolean().default(true),
showResponseCount: z.boolean().default(false),
});
export type TSurveyWelcomeCard = z.infer<typeof ZSurveyWelcomeCard>;
export const ZSurveyThankYouCard = z.object({
enabled: z.boolean(),
headline: z.optional(z.string()),
subheader: z.optional(z.string()),
buttonLabel: z.optional(z.string()),
buttonLink: z.optional(z.string()),
imageUrl: z.string().optional(),
});
export type TSurveyThankYouCard = z.infer<typeof ZSurveyThankYouCard>;
export const ZSurveyHiddenFields = z.object({
enabled: z.boolean(),
fieldIds: z.optional(z.array(z.string())),
});
export type TSurveyHiddenFields = z.infer<typeof ZSurveyHiddenFields>;
export const ZSurveyChoice = z.object({
id: z.string(),
label: z.string(),
});
export type TSurveyChoice = z.infer<typeof ZSurveyChoice>;
export const ZSurveyPictureChoice = z.object({
id: z.string(),
imageUrl: z.string(),
});
export type TSurveyPictureChoice = z.infer<typeof ZSurveyPictureChoice>;
export const ZSurveyLogicCondition = z.enum([
"accepted",
"clicked",
"submitted",
"skipped",
"equals",
"notEquals",
"lessThan",
"lessEqual",
"greaterThan",
"greaterEqual",
"includesAll",
"includesOne",
"uploaded",
"notUploaded",
"booked",
]);
export type TSurveyLogicCondition = z.infer<typeof ZSurveyLogicCondition>;
export const ZSurveyLogicBase = z.object({
condition: ZSurveyLogicCondition.optional(),
value: z.union([z.string(), z.array(z.string())]).optional(),
destination: z.union([z.string(), z.literal("end")]).optional(),
});
export const ZSurveyFileUploadLogic = ZSurveyLogicBase.extend({
condition: z.enum(["uploaded", "notUploaded"]).optional(),
value: z.undefined(),
});
export const ZSurveyOpenTextLogic = ZSurveyLogicBase.extend({
condition: z.enum(["submitted", "skipped"]).optional(),
value: z.undefined(),
});
export const ZSurveyConsentLogic = ZSurveyLogicBase.extend({
condition: z.enum(["skipped", "accepted"]).optional(),
value: z.undefined(),
});
export const ZSurveyMultipleChoiceSingleLogic = ZSurveyLogicBase.extend({
condition: z.enum(["submitted", "skipped", "equals", "notEquals"]).optional(),
value: z.string().optional(),
});
export const ZSurveyMultipleChoiceMultiLogic = ZSurveyLogicBase.extend({
condition: z.enum(["submitted", "skipped", "includesAll", "includesOne", "equals"]).optional(),
value: z.union([z.array(z.string()), z.string()]).optional(),
});
export const ZSurveyNPSLogic = ZSurveyLogicBase.extend({
condition: z
.enum([
"equals",
"notEquals",
"lessThan",
"lessEqual",
"greaterThan",
"greaterEqual",
"submitted",
"skipped",
])
.optional(),
value: z.union([z.string(), z.number()]).optional(),
});
const ZSurveyCTALogic = ZSurveyLogicBase.extend({
// "submitted" condition is legacy and should be removed later
condition: z.enum(["clicked", "submitted", "skipped"]).optional(),
value: z.undefined(),
});
const ZSurveyRatingLogic = ZSurveyLogicBase.extend({
condition: z
.enum([
"equals",
"notEquals",
"lessThan",
"lessEqual",
"greaterThan",
"greaterEqual",
"submitted",
"skipped",
])
.optional(),
value: z.union([z.string(), z.number()]).optional(),
});
const ZSurveyPictureSelectionLogic = ZSurveyLogicBase.extend({
condition: z.enum(["submitted", "skipped"]).optional(),
value: z.undefined(),
});
const ZSurveyCalLogic = ZSurveyLogicBase.extend({
condition: z.enum(["booked", "skipped"]).optional(),
value: z.undefined(),
});
export const ZSurveyLogic = z.union([
ZSurveyOpenTextLogic,
ZSurveyConsentLogic,
ZSurveyMultipleChoiceSingleLogic,
ZSurveyMultipleChoiceMultiLogic,
ZSurveyNPSLogic,
ZSurveyCTALogic,
ZSurveyRatingLogic,
ZSurveyPictureSelectionLogic,
ZSurveyFileUploadLogic,
ZSurveyCalLogic,
]);
export type TSurveyLogic = z.infer<typeof ZSurveyLogic>;
const ZSurveyQuestionBase = z.object({
id: z.string(),
type: z.string(),
headline: z.string(),
subheader: z.string().optional(),
imageUrl: z.string().optional(),
required: z.boolean(),
buttonLabel: z.string().optional(),
backButtonLabel: z.string().optional(),
scale: z.enum(["number", "smiley", "star"]).optional(),
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]).optional(),
logic: z.array(ZSurveyLogic).optional(),
isDraft: z.boolean().optional(),
});
export const ZSurveyOpenTextQuestionInputType = z.enum(["text", "email", "url", "number", "phone"]);
export type TSurveyOpenTextQuestionInputType = z.infer<typeof ZSurveyOpenTextQuestionInputType>;
export const ZSurveyOpenTextQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.OpenText),
placeholder: z.string().optional(),
longAnswer: z.boolean().optional(),
logic: z.array(ZSurveyOpenTextLogic).optional(),
inputType: ZSurveyOpenTextQuestionInputType.optional().default("text"),
});
export type TSurveyOpenTextQuestion = z.infer<typeof ZSurveyOpenTextQuestion>;
export const ZSurveyConsentQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.Consent),
html: z.string().optional(),
label: z.string(),
dismissButtonLabel: z.string().optional(),
placeholder: z.string().optional(),
logic: z.array(ZSurveyConsentLogic).optional(),
});
export type TSurveyConsentQuestion = z.infer<typeof ZSurveyConsentQuestion>;
export const ZSurveyMultipleChoiceSingleQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.MultipleChoiceSingle),
choices: z.array(ZSurveyChoice),
logic: z.array(ZSurveyMultipleChoiceSingleLogic).optional(),
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
otherOptionPlaceholder: z.string().optional(),
});
export type TSurveyMultipleChoiceSingleQuestion = z.infer<typeof ZSurveyMultipleChoiceSingleQuestion>;
export const ZSurveyMultipleChoiceMultiQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.MultipleChoiceMulti),
choices: z.array(ZSurveyChoice),
logic: z.array(ZSurveyMultipleChoiceMultiLogic).optional(),
shuffleOption: z.enum(["none", "all", "exceptLast"]).optional(),
otherOptionPlaceholder: z.string().optional(),
});
export type TSurveyMultipleChoiceMultiQuestion = z.infer<typeof ZSurveyMultipleChoiceMultiQuestion>;
export const ZSurveyNPSQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.NPS),
lowerLabel: z.string(),
upperLabel: z.string(),
logic: z.array(ZSurveyNPSLogic).optional(),
});
export type TSurveyNPSQuestion = z.infer<typeof ZSurveyNPSQuestion>;
export const ZSurveyCTAQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.CTA),
html: z.string().optional(),
buttonUrl: z.string().optional(),
buttonExternal: z.boolean(),
dismissButtonLabel: z.string().optional(),
logic: z.array(ZSurveyCTALogic).optional(),
});
export type TSurveyCTAQuestion = z.infer<typeof ZSurveyCTAQuestion>;
// export const ZSurveyWelcomeQuestion = ZSurveyQuestionBase.extend({
// type: z.literal(TSurveyQuestionType.Welcome),
// html: z.string().optional(),
// fileUrl: z.string().optional(),
// buttonUrl: z.string().optional(),
// timeToFinish: z.boolean().default(false),
// logic: z.array(ZSurveyCTALogic).optional(),
// });
// export type TSurveyWelcomeQuestion = z.infer<typeof ZSurveyWelcomeQuestion>;
export const ZSurveyRatingQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.Rating),
scale: z.enum(["number", "smiley", "star"]),
range: z.union([z.literal(5), z.literal(3), z.literal(4), z.literal(7), z.literal(10)]),
lowerLabel: z.string(),
upperLabel: z.string(),
logic: z.array(ZSurveyRatingLogic).optional(),
});
export const ZSurveyDateQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.Date),
html: z.string().optional(),
format: z.enum(["M-d-y", "d-M-y", "y-M-d"]),
});
export type TSurveyDateQuestion = z.infer<typeof ZSurveyDateQuestion>;
export type TSurveyRatingQuestion = z.infer<typeof ZSurveyRatingQuestion>;
export const ZSurveyPictureSelectionQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.PictureSelection),
allowMulti: z.boolean().optional().default(false),
choices: z.array(ZSurveyPictureChoice),
logic: z.array(ZSurveyPictureSelectionLogic).optional(),
});
export type TSurveyPictureSelectionQuestion = z.infer<typeof ZSurveyPictureSelectionQuestion>;
export const ZSurveyFileUploadQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.FileUpload),
allowMultipleFiles: z.boolean(),
maxSizeInMB: z.number().optional(),
allowedFileExtensions: z.array(ZAllowedFileExtension).optional(),
logic: z.array(ZSurveyFileUploadLogic).optional(),
});
export type TSurveyFileUploadQuestion = z.infer<typeof ZSurveyFileUploadQuestion>;
export const ZSurveyCalQuestion = ZSurveyQuestionBase.extend({
type: z.literal(TSurveyQuestionType.Cal),
calUserName: z.string(),
logic: z.array(ZSurveyCalLogic).optional(),
});
export type TSurveyCalQuestion = z.infer<typeof ZSurveyCalQuestion>;
export const ZSurveyQuestion = z.union([
ZSurveyOpenTextQuestion,
ZSurveyConsentQuestion,
ZSurveyMultipleChoiceSingleQuestion,
ZSurveyMultipleChoiceMultiQuestion,
ZSurveyNPSQuestion,
ZSurveyCTAQuestion,
ZSurveyRatingQuestion,
ZSurveyPictureSelectionQuestion,
ZSurveyDateQuestion,
ZSurveyFileUploadQuestion,
ZSurveyCalQuestion,
]);
export type TSurveyQuestion = z.infer<typeof ZSurveyQuestion>;
export const ZSurveyQuestions = z.array(ZSurveyQuestion);
export type TSurveyQuestions = z.infer<typeof ZSurveyQuestions>;
export const ZSurveyClosedMessage = z
.object({
enabled: z.boolean().optional(),
heading: z.string().optional(),
subheading: z.string().optional(),
})
.nullable()
.optional();
export type TSurveyClosedMessage = z.infer<typeof ZSurveyClosedMessage>;
export const ZSurveyAttributeFilter = z.object({
attributeClassId: z.string().cuid2(),
condition: z.enum(["equals", "notEquals"]),
value: z.string(),
});
export type TSurveyAttributeFilter = z.infer<typeof ZSurveyAttributeFilter>;
export const ZSurveyType = z.enum(["web", "email", "link", "mobile"]);
export type TSurveyType = z.infer<typeof ZSurveyType>;
const ZSurveyStatus = z.enum(["draft", "inProgress", "paused", "completed"]);
export type TSurveyStatus = z.infer<typeof ZSurveyStatus>;
const ZSurveyDisplayOption = z.enum(["displayOnce", "displayMultiple", "respondMultiple"]);
export type TSurveyDisplayOption = z.infer<typeof ZSurveyDisplayOption>;
export const ZColor = z.string().regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/);
export const ZPlacement = z.enum(["bottomLeft", "bottomRight", "topLeft", "topRight", "center"]);
export type TPlacement = z.infer<typeof ZPlacement>;
export const ZSurveyProductOverwrites = z.object({
brandColor: ZColor.nullish(),
highlightBorderColor: ZColor.nullish(),
placement: ZPlacement.nullish(),
clickOutsideClose: z.boolean().nullish(),
darkOverlay: z.boolean().nullish(),
});
export type TSurveyProductOverwrites = z.infer<typeof ZSurveyProductOverwrites>;
export const ZSurveyStylingBackground = z.object({
bg: z.string().nullish(),
bgType: z.enum(["animation", "color", "image"]).nullish(),
brightness: z.number().nullish(),
});
export type TSurveyStylingBackground = z.infer<typeof ZSurveyStylingBackground>;
export const ZSurveyStyling = z.object({
background: ZSurveyStylingBackground.nullish(),
hideProgressBar: z.boolean().nullish(),
});
export type TSurveyStyling = z.infer<typeof ZSurveyStyling>;
export const ZSurveySingleUse = z
.object({
enabled: z.boolean(),
heading: z.optional(z.string()),
subheading: z.optional(z.string()),
isEncrypted: z.boolean(),
})
.nullable();
export type TSurveySingleUse = z.infer<typeof ZSurveySingleUse>;
export const ZSurveyVerifyEmail = z
.object({
name: z.optional(z.string()),
subheading: z.optional(z.string()),
})
.optional();
export type TSurveyVerifyEmail = z.infer<typeof ZSurveyVerifyEmail>;
export const ZSurvey = z.object({
id: z.string().cuid2(),
createdAt: z.date(),
updatedAt: z.date(),
name: z.string(),
type: ZSurveyType,
environmentId: z.string(),
createdBy: z.string().nullable(),
status: ZSurveyStatus,
attributeFilters: z.array(ZSurveyAttributeFilter),
displayOption: ZSurveyDisplayOption,
autoClose: z.number().nullable(),
triggers: z.array(z.string()),
redirectUrl: z.string().url().nullable(),
recontactDays: z.number().nullable(),
welcomeCard: ZSurveyWelcomeCard,
questions: ZSurveyQuestions,
thankYouCard: ZSurveyThankYouCard,
hiddenFields: ZSurveyHiddenFields,
delay: z.number(),
autoComplete: z.number().nullable(),
closeOnDate: z.date().nullable(),
productOverwrites: ZSurveyProductOverwrites.nullable(),
styling: ZSurveyStyling.nullable(),
surveyClosedMessage: ZSurveyClosedMessage.nullable(),
singleUse: ZSurveySingleUse.nullable(),
verifyEmail: ZSurveyVerifyEmail.nullable(),
pin: z.string().nullable().optional(),
resultShareKey: z.string().nullable(),
displayPercentage: z.number().min(1).max(100).nullable(),
});
export type TSurvey = z.infer<typeof ZSurvey>;
export const ZTemplate = z.object({
name: z.string(),
description: z.string(),
icon: z.any().optional(),
category: z
.enum(["Product Experience", "Exploration", "Growth", "Increase Revenue", "Customer Success"])
.optional(),
objectives: z.array(ZUserObjective).optional(),
preset: z.object({
name: z.string(),
welcomeCard: ZSurveyWelcomeCard,
questions: ZSurveyQuestions,
thankYouCard: ZSurveyThankYouCard,
hiddenFields: ZSurveyHiddenFields,
}),
});
export type TTemplate = z.infer<typeof ZTemplate>;

View File

@@ -1,66 +1,87 @@
import HeadingCentered from "@/components/shared/HeadingCentered";
import SeoFaq from "@/components/shared/seo/SeoFaq";
import { FAQPageJsonLd } from "next-seo";
const FAQs = [
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@formbricks/ui/Accordion";
const FAQ_DATA = [
{
question: "What is Formbricks?",
answer:
"Formbricks is an experience management platform built on top of the fastest growing open source survey infrastructure out there. It aims to assist businesses in capturing and understanding customer insights and emotions towards their products and services. Designed to integrate seamlessly with various platforms, Formbricks focuses on user research, emphasizing data privacy and requiring minimal development effort for integration.",
answer: () => (
<>
Formbricks is an open-source Experience Management tool that helps businesses understand what
customers think and feel about their products. It integrates natively into your platform to conduct
user research with a focus on data privacy and minimal development intervention.
</>
),
},
{
question: "How do I integrate Formbricks into my application?",
answer:
"Integrating Formbricks into an application is effortless. For web applications, it involves adding a simple script tag to the HTML head. For applications built with modern frameworks such as React, Vue, or Svelte, Formbricks can be installed via NPM. Initialization with specific environment details completes the setup. Detailed instructions and framework guides are readily available in the detailed Formbricks documentation.",
answer: () => (
<>
Integrating Formbricks is a breeze. Simply copy a script tag to your HTML head, or use NPM to install
Formbricks for platforms like React, Vue, Svelte, etc. Once installed, initialize Formbricks with your
environment details. Learn more with our framework guides{" "}
<a href="/docs/getting-started/framework-guides" className="text-brand-dark dark:text-brand-light">
here
</a>
.
</>
),
},
{
question: "Is Formbricks GDPR compliant?",
answer:
"Indeed, Formbricks ensures full GDPR compliance, emphasizing the protection of user data privacy. It offers both cloud-based solutions and self-hosting options, adhering to data privacy regulations and making it a trusted choice for secure open source survey tool deployment.",
answer: () => (
<>
Yes, Formbricks is fully GDPR compliant. Whether you use our cloud solution or decide to self-host, we
ensure compliance with all data privacy regulations.
</>
),
},
{
question: "Can I self-host Formbricks?",
answer:
"Certainly! Formbricks encourages self-hosting, providing users with greater control over their data and compliance. This option underscores Formbricks' commitment to offering versatile and free open source experience management software, ensuring users can adapt the platform to their unique requirements. Detailed self-hosting documentation is available for users seeking to leverage this capability.",
answer: () => (
<>
Absolutely! We provide an option for users to host Formbricks on their own server, ensuring even more
control over data and compliance. And the best part? Self-hosting is available for free, always. For
documentation on self hosting, click{" "}
<a href="/docs/self-hosting/deployment" className="text-brand-dark dark:text-brand-light">
here
</a>
.
</>
),
},
{
question: "How does Formbricks pricing work?",
answer:
"Formbricks introduces a 'Free forever' plan, showcasing its commitment to making open source survey platforms universally accessible. This plan features unlimited surveys and in-product surveys, among other functionalities. Self-hosting users can enjoy all the benefits of the free plan with additional features at no extra cost. For those seeking advanced features Formbricks invites you to explore the pricing section for more information.",
},
{
question: "How does Formbricks make money?",
answer:
"Formbricks employs the 'Open Core' business model. The core of the Formbricks application is offered for free. Formbricks monetizes by providing advanced features and services, typically catering to the needs of larger clients, thereby generating revenue.",
},
{
question: "What is the best open source survey software available?",
answer:
"Identifying the best open source survey software requires evaluating features, flexibility, and support. Formbricks is a noteworthy contender, offering comprehensive experience management solutions. This platform excels in enabling businesses to delve into customer insights and feedback, offering versatility and ease of system integration.",
},
{
question: "Can open source survey platforms be customized for my business needs?",
answer:
"Definitely. Platforms like Formbricks exemplify the customizability of open source survey tools, allowing for extensive tailoring to meet specific business requirements. Access to the source code enables deep customization, from branding adjustments to complex integrations with existing systems, underscoring the flexibility of open source experience management solutions.",
},
{
question:
"What advantages does using an experience management platform offer over traditional survey tools?",
answer:
"Experience management platforms, especially those built on open source foundations, offer a more holistic view of customer interactions compared to traditional survey tools. They enable real-time collection, analysis, and application of customer feedback, ensuring a thorough understanding of the customer journey. This comprehensive insight facilitates informed decision-making and boosts customer satisfaction.",
answer: () => (
<>
Formbricks offers a Free forever plan on the cloud that includes unlimited surveys, in-product
surveys, and more. We also provide a self-hosting option which includes all free features and more,
available at no cost. If you require additional features or responses, check out our pricing section
above for more details.
</>
),
},
];
const faqJsonLdData = FAQ_DATA.map((faq) => ({
questionName: faq.question,
acceptedAnswerText: faq.answer(),
}));
export default function FAQ() {
return (
<div>
<HeadingCentered heading="Frequently asked questions" teaser="FAQ" />
<SeoFaq
faqs={FAQs}
headline="Open Source Experience Management Platform"
description="Formbricks is an Experience Management Platform built of top of the largest open source survey infrastructure worldwide."
datePublished="2023-10-11"
dateModified="2024-03-12"
/>
<div className="max-w-7xl py-4 sm:px-6 sm:pb-6 lg:px-8" id="faq">
<FAQPageJsonLd mainEntity={faqJsonLdData} />
<HeadingCentered heading="Frequently Asked Questions" teaser="FAQ" closer />
<Accordion type="single" collapsible className="px-4 sm:px-0">
{FAQ_DATA.map((faq, index) => (
<AccordionItem key={`item-${index}`} value={`item-${index + 1}`}>
<AccordionTrigger>{faq.question}</AccordionTrigger>
<AccordionContent>{faq.answer()}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</div>
);
}

View File

@@ -24,39 +24,42 @@ const features = [
];
export const Features: React.FC = () => {
return (
<div className="relative">
<HeadingCentered
teaser="Data Privacy at heart"
heading="The only open-source solution"
subheading="Comply with all data privacy regulation with ease. Self-host if you want."
/>
<div className="relative px-4 pb-10 sm:px-6 lg:px-8 lg:pb-14 lg:pt-24">
<div className="relative mx-auto max-w-7xl">
<HeadingCentered
closer
teaser="Data Privacy at heart"
heading="The only open-source solution"
subheading="Comply with all data privacy regulation with ease. Self-host if you want."
/>
<ul role="list" className="grid grid-cols-1 gap-4 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:gap-10">
{features.map((feature) => {
const IconComponent: React.ElementType = feature.icon;
<ul role="list" className="grid grid-cols-1 gap-4 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:gap-10">
{features.map((feature) => {
const IconComponent: React.ElementType = feature.icon;
return (
<li
key={feature.id}
className="relative col-span-1 mt-16 flex flex-col rounded-xl bg-slate-100 text-center dark:bg-slate-700">
<div className="absolute -mt-12 w-full">
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-3xl bg-slate-200 shadow dark:bg-slate-800">
<IconComponent className="text-brand-dark dark:text-brand-light mx-auto h-10 w-10 flex-shrink-0" />
return (
<li
key={feature.id}
className="relative col-span-1 mt-16 flex flex-col rounded-xl bg-slate-100 text-center dark:bg-slate-700">
<div className="absolute -mt-12 w-full">
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-3xl bg-slate-200 shadow dark:bg-slate-800">
<IconComponent className="text-brand-dark dark:text-brand-light mx-auto h-10 w-10 flex-shrink-0" />
</div>
</div>
</div>
<div className="flex flex-1 flex-col p-10">
<h3 className="my-4 text-lg font-medium text-slate-800 dark:text-slate-200">
{feature.name}
</h3>
<dl className="mt-1 flex flex-grow flex-col justify-between">
<dt className="sr-only">Description</dt>
<dd className="text-sm text-slate-600 dark:text-slate-400">{feature.description}</dd>
</dl>
</div>
</li>
);
})}
</ul>
<div className="flex flex-1 flex-col p-10">
<h3 className="my-4 text-lg font-medium text-slate-800 dark:text-slate-200">
{feature.name}
</h3>
<dl className="mt-1 flex flex-grow flex-col justify-between">
<dt className="sr-only">Description</dt>
<dd className="text-sm text-slate-600 dark:text-slate-400">{feature.description}</dd>
</dl>
</div>
</li>
);
})}
</ul>
</div>
</div>
);
};

View File

@@ -1,22 +1,28 @@
import CalLogoDark from "@/images/clients/cal-logo-dark.svg";
import CalLogoLight from "@/images/clients/cal-logo-light.svg";
import CrowdLogoDark from "@/images/clients/crowd-logo-dark.svg";
import CrowdLogoLight from "@/images/clients/crowd-logo-light.svg";
import FlixbusLogo from "@/images/clients/flixbus-white.svg";
import NILogoDark from "@/images/clients/niLogoDark.svg";
import NILogoLight from "@/images/clients/niLogoWhite.svg";
import OptimoleLogo from "@/images/clients/optimole-logo.svg";
import ThemeisleLogo from "@/images/clients/themeisle-logo.webp";
import { ShieldCheckIcon, StarIcon } from "lucide-react";
import AnimationFallback from "@/public/animations/opensource-xm-platform-formbricks-fallback.png";
import { ShieldCheckIcon, StarIcon } from "@heroicons/react/24/outline";
import { usePlausible } from "next-plausible";
import Image from "next/image";
import { useRouter } from "next/router";
import { Button } from "@formbricks/ui/Button";
import HeroAnimation from "./HeroAnimation";
export const Hero: React.FC = ({}) => {
const plausible = usePlausible();
const router = useRouter();
return (
<div className="relative">
<div className="text-center">
<div className="px-4 pb-20 pt-16 text-center sm:px-6 lg:px-8 lg:pb-32 lg:pt-20">
<div className="xs:text-sm flex items-center justify-center space-x-4 divide-x-2 text-xs text-slate-600">
<p>
<ShieldCheckIcon className="mb-1 inline h-4 w-4" /> Privacy-first
@@ -40,8 +46,9 @@ export const Hero: React.FC = ({}) => {
know what your customers need.
</span>
</p>
<div className="mx-auto mt-5 max-w-xl items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0 lg:max-w-3xl">
<div className="grid grid-cols-2 items-center gap-8 pt-2 md:grid-cols-3 md:gap-10 lg:grid-cols-6">
<div className="mx-auto mt-5 max-w-3xl items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0">
<div className="grid grid-cols-6 items-center gap-6 pt-2 md:gap-8">
<Image
src={FlixbusLogo}
alt="Flixbus Flix Flixtrain Logo"
@@ -49,18 +56,37 @@ export const Hero: React.FC = ({}) => {
width={200}
/>
<Image src={CalLogoLight} alt="Cal Logo" className="block rounded-lg dark:hidden" width={170} />
<Image src={ThemeisleLogo} alt="ThemeIsle Logo" className="pb-1" width={200} />
<Image src={CalLogoDark} alt="Cal Logo" className="hidden rounded-lg dark:block" width={170} />
<Image src={ThemeisleLogo} alt="Neverinstall Logo" className="pb-1" width={200} />
<Image
src={CrowdLogoLight}
alt="Crowd.dev Logo"
className="block rounded-lg pb-1 dark:hidden"
width={200}
/>
<Image src={OptimoleLogo} alt="Optimole Logo" className="pb-1" width={200} />
<Image
src={CrowdLogoDark}
alt="Crowd.dev Logo"
className="hidden rounded-lg pb-1 dark:block"
width={200}
/>
<Image src={OptimoleLogo} alt="Neverinstall Logo" className="pb-1" width={200} />
<Image src={NILogoDark} alt="Neverinstall Logo" className="block pb-1 dark:hidden" width={200} />
<Image
src={NILogoLight}
alt="Neverinstall Logo"
className="hidden pb-1 dark:block"
width={200}
/>
<Image
src={NILogoLight}
alt="Neverinstall Logo"
className="hidden pb-1 dark:block"
width={200}
/>
</div>
</div>
<div className="hidden pt-14 md:block">
<Button
variant="highlight"
@@ -69,7 +95,7 @@ export const Hero: React.FC = ({}) => {
router.push("https://app.formbricks.com/auth/signup");
plausible("Hero_CTA_GetStartedItsFree");
}}>
Get Started
Get Started, it&apos;s Free
</Button>
<Button
variant="secondary"
@@ -82,6 +108,9 @@ export const Hero: React.FC = ({}) => {
</Button>
</div>
</div>
<div className="relative px-2 md:px-0">
<HeroAnimation fallbackImage={AnimationFallback} />
</div>
</div>
);
};

View File

@@ -30,7 +30,7 @@ export const HeroAnimation: React.FC<any> = ({ fallbackImage, ...props }) => {
}, [lottie]);
return (
<div className="relative hidden md:block" {...props}>
<div className="relative" {...props}>
<div ref={ref} />
{!loaded && (
<div className="absolute inset-0">

View File

@@ -6,43 +6,62 @@ import Image from "next/image";
export const Highlights: React.FC = ({}) => {
return (
<div className="space-y-16">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 md:pb-0">
<h2 className="xs:text-3xl text-2xl font-bold leading-7 tracking-tight text-slate-800 dark:text-slate-200">
Ask at the right moment,
<br />
<span className="font-light">get the data you need.</span>
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Follow up emails are so 2010. Ask users as they experience your product - and leverage a
significantly higher conversion rate.
</p>
</div>
<div className="rounded-lg bg-slate-100 py-6 pr-4 sm:py-16 sm:pr-8 dark:bg-slate-800">
<Image src={ImageEventTriggerLight} alt="react library" className="block rounded-lg dark:hidden" />
<Image src={ImageEventTriggerDark} alt="react library" className="hidden rounded-lg dark:block" />
<>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 md:pb-0">
<h2 className="xs:text-3xl text-2xl font-bold leading-7 tracking-tight text-slate-800 dark:text-slate-200">
Ask at the right moment,
<br />
<span className="font-light">get the data you need.</span>
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Follow up emails are so 2010. Ask users as they experience your product - and leverage a
significantly higher conversion rate.
</p>
</div>
<div className="rounded-lg bg-slate-100 py-6 pr-4 sm:py-16 sm:pr-8 dark:bg-slate-800">
<Image
src={ImageEventTriggerLight}
alt="react library"
className="block rounded-lg dark:hidden"
/>
<Image
src={ImageEventTriggerDark}
alt="react library"
className="hidden rounded-lg dark:block"
/>
</div>
</div>
</div>
</div>
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="order-last rounded-lg bg-slate-100 p-4 sm:p-8 md:order-first dark:bg-slate-800">
<Image src={ImageAttributesLight} alt="react library" className="block rounded-lg dark:hidden" />
<Image src={ImageAttributesDark} alt="react library" className="hidden rounded-lg dark:block" />
</div>
<div className="pb-8 md:pb-0">
<h2 className="xs:text-3xl text-2xl font-bold leading-7 tracking-tight text-slate-800 sm:text-3xl dark:text-slate-100">
Don&apos;t Spray and pray.
<br />
<span className="font-light">Pre-segment granularly.</span>
</h2>
<p className="text-md mt-6 max-w-md leading-7 text-slate-500 dark:text-slate-400">
Pre-segment who sees your survey based on custom attributes. Keep the signal, cancel out the
noise.
</p>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="order-last rounded-lg bg-slate-100 p-4 sm:p-8 md:order-first dark:bg-slate-800">
<Image
src={ImageAttributesLight}
alt="react library"
className="block rounded-lg dark:hidden"
/>
<Image src={ImageAttributesDark} alt="react library" className="hidden rounded-lg dark:block" />
</div>
<div className="pb-8 md:pb-0">
<h2 className="xs:text-3xl text-2xl font-bold leading-7 tracking-tight text-slate-800 sm:text-3xl dark:text-slate-100">
Don&apos;t Spray and pray.
<br />
<span className="font-light">Pre-segment granularly.</span>
</h2>
<p className="text-md mt-6 max-w-md leading-7 text-slate-500 dark:text-slate-400">
Pre-segment who sees your survey based on custom attributes. Keep the signal, cancel out the
noise.
</p>
</div>
</div>
</div>
</div>
</div>
</>
);
};

View File

@@ -1,5 +1,5 @@
import { ArrowUpIcon } from "@heroicons/react/24/solid";
import throttle from "lodash/throttle";
import { ArrowUpIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Button } from "@formbricks/ui/Button";

View File

@@ -1,123 +1,144 @@
import DemoPreview from "@/components/dummyUI/DemoPreview";
import DashboardMockupDark from "@/images/dashboard-mockup-dark.png";
import DashboardMockup from "@/images/dashboard-mockup.png";
import { MousePointerClickIcon } from "lucide-react";
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import { useState } from "react";
import { Button } from "@formbricks/ui/Button";
import AddEventDummy from "../dummyUI/AddEventDummy";
import AddNoCodeEventModalDummy from "../dummyUI/AddNoCodeEventModalDummy";
import HeadingCentered from "../shared/HeadingCentered";
import SetupTabs from "./SetupTabs";
export const Steps: React.FC = () => {
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
return (
<div>
<>
<HeadingCentered
closer
teaser="Leave your engineers in peace"
heading="Set Formbricks up in minutes"
subheading="Formbricks is designed for as little dev attention as possible. Heres how:"
/>
<div className="space-y-16">
<div className="xs:grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 sm:pl-10 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 1</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-200">
Copy + Paste
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Simply copy a &lt;script&gt; tag to your HTML head - thats about it. Or use NPM to install
Formbricks for React, Vue, Svelte, etc.
</p>
</div>
<div className="rounded-lg bg-slate-100 dark:bg-slate-800">
<SetupTabs />
</div>
</div>
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="order-last w-full rounded-lg bg-slate-100 p-4 sm:py-8 md:order-first dark:bg-slate-800">
<div className="flex h-40 items-center justify-center">
<Button variant="primary">
<MousePointerClickIcon className="mr-2 h-5 w-5 text-white" />
Add Action
</Button>
<div id="howitworks" className="xs:m-auto mb-12 mt-16 max-w-lg md:mb-0 md:mt-8 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="xs:grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 sm:pl-10 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 1</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 dark:text-slate-200">
Copy + Paste
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Simply copy a &lt;script&gt; tag to your HTML head - thats about it. Or use NPM to install
Formbricks for React, Vue, Svelte, etc.
</p>
</div>
</div>
<div className="pb-8 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 2</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-100">
No-Code: Track User Actions
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Set up user actions which can trigger your survey without writing a single line of code. Surveys
can be triggered on specific pages or after an element is clicked.
</p>
</div>
</div>
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 sm:pl-10 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 3</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-200">
Create your survey
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Start from a template - or from scratch. Ask what you want, in any language. You can also adjust
the look and feel of your survey.
</p>
</div>
<div className="relative w-full rounded-lg p-1 sm:p-8 dark:bg-slate-800">
<DemoPreview template="Product Market Fit Survey (short)" />
</div>
</div>
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="order-last w-full rounded-lg bg-slate-100 p-4 sm:py-8 md:order-first dark:bg-slate-800">
<div className="mx-auto flex flex-col items-center justify-center md:w-3/4">
<AddEventDummy />
<div className="rounded-lg bg-slate-100 dark:bg-slate-800">
<SetupTabs />
</div>
</div>
<div className="pb-8 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 4</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-100">
Set segment and trigger
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Create a custom segment for each survey. Use attributes and past user actions to only survey the
people who have answers. Trigger your survey on any user action in your app.
</p>
</div>
</div>
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 sm:pl-10 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 5</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-200">
Make better decisions
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Gather all insights you can - including partial submissions. Build conviction for the next
product decision. Better data, better business.
</p>
</div>
<div className="sm:scale-125 sm:p-8">
<Image
src={DashboardMockup}
quality="100"
alt="Data Pipelines"
className="block rounded-lg dark:hidden"
/>
<Image
src={DashboardMockupDark}
quality="100"
alt="Data Pipelines"
className="hidden dark:block"
/>
</div>
</div>
</div>
</div>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="order-last w-full rounded-lg bg-slate-100 p-4 sm:py-8 md:order-first dark:bg-slate-800">
<div className="flex h-40 items-center justify-center">
<Button variant="primary">
<CursorArrowRaysIcon className="mr-2 h-5 w-5 text-white" />
Add Action
</Button>
</div>
</div>
<div className="pb-8 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 2</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-100">
No-Code: Track User Actions
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Set up user actions which can trigger your survey without writing a single line of code.
Surveys can be triggered on specific pages or after an element is clicked.
</p>
</div>
</div>
</div>
</div>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 sm:pl-10 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 3</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-200">
Create your survey
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Start from a template - or from scratch. Ask what you want, in any language. You can also
adjust the look and feel of your survey.
</p>
</div>
<div className="relative w-full rounded-lg p-1 sm:p-8 dark:bg-slate-800">
<DemoPreview template="Product Market Fit Survey (short)" />
</div>
</div>
</div>
</div>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="order-last w-full rounded-lg bg-slate-100 p-4 sm:py-8 md:order-first dark:bg-slate-800">
<div className="mx-auto flex flex-col items-center justify-center md:w-3/4">
<AddEventDummy />
</div>
</div>
<div className="pb-8 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 4</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-100">
Set segment and trigger
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Create a custom segment for each survey. Use attributes and past user actions to only survey
the people who have answers. Trigger your survey on any user action in your app.
</p>
</div>
</div>
</div>
</div>
<div className="mx-auto mb-12 mt-8 max-w-lg md:mb-0 md:mt-32 md:max-w-none">
<div className="px-4 sm:max-w-4xl sm:px-6 lg:max-w-7xl lg:px-8">
<div className="grid md:grid-cols-2 md:items-center md:gap-16">
<div className="pb-8 sm:pl-10 md:pb-0">
<h4 className="text-brand-dark font-bold">Step 5</h4>
<h2 className="xs:text-3xl text-2xl font-bold tracking-tight text-slate-800 sm:text-3xl dark:text-slate-200">
Make better decisions
</h2>
<p className="text-md mt-6 max-w-lg leading-7 text-slate-500 dark:text-slate-400">
Gather all insights you can - including partial submissions. Build conviction for the next
product decision. Better data, better business.
</p>
</div>
<div className="sm:scale-125 sm:p-8">
<Image
src={DashboardMockup}
quality="100"
alt="Data Pipelines"
className="block rounded-lg dark:hidden"
/>
<Image
src={DashboardMockupDark}
quality="100"
alt="Data Pipelines"
className="hidden dark:block"
/>
</div>
</div>
</div>
</div>
<AddNoCodeEventModalDummy open={isAddEventModalOpen} setOpen={setAddEventModalOpen} />
</>
);
};

View File

@@ -1,19 +0,0 @@
interface TestimonialProps {
title: string;
text: string;
Icon: React.ElementType;
}
export default function SalesTestimonial({ title, text, Icon }: TestimonialProps) {
return (
<div className="flex items-center gap-4 rounded-xl border border-slate-200 bg-gradient-to-tr from-slate-100 to-slate-100 p-4 transition-colors delay-1000 duration-1000 ease-in-out hover:to-slate-50">
<div className="rounded-xl border border-slate-200 bg-white p-8">
<Icon className="h-12 w-12 text-slate-500" strokeWidth={1} />
</div>
<div>
<h3 className="text-pretty text-lg font-medium text-slate-800">{title}</h3>
<p className="text-slate-500">{text}</p>
</div>
</div>
);
}

View File

@@ -1,101 +0,0 @@
import { Popover, Transition } from "@headlessui/react";
import { Menu, X } from "lucide-react";
import { usePlausible } from "next-plausible";
import Link from "next/link";
import { useRouter } from "next/router";
import { Fragment } from "react";
import { Button } from "@formbricks/ui/Button";
import { FooterLogo } from "../shared/Logo";
const mainNav = [
{ name: "Link Surveys", href: "/open-source-form-builder", status: true },
{ name: "Website Surveys", href: "/website-survey", status: true },
{ name: "In-app Surveys", href: "/in-app-survey", status: true },
];
export default function HeaderLight() {
const plausible = usePlausible();
const router = useRouter();
return (
<header className="max-w-8xl mx-auto flex items-center justify-between px-6 py-6 lg:px-10 xl:px-12">
<Link href="/">
<span className="sr-only">Formbricks</span>
<FooterLogo className="h-8 w-auto sm:h-10" />
</Link>
<div className="hidden lg:block">
{mainNav.map((item) => (
<Link
key={item.name}
href={item.href}
className="px-8 text-sm font-medium text-slate-400 hover:text-slate-700 lg:text-base dark:hover:text-slate-300">
{item.name}
</Link>
))}
</div>
<Button
variant="highlight"
className="hidden md:px-6 lg:block"
onClick={() => {
router.push("https://app.formbricks.com/auth/signup");
plausible("Demo_CTA_TryForFree");
}}>
Get started - it&apos;s free!
</Button>
<Popover className="block lg:hidden">
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-slate-100 p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 lg:hidden dark:bg-slate-700 dark:text-slate-200">
<span className="sr-only">Open menu</span>
<Menu className="h-6 w-6" aria-hidden="true" />
</Popover.Button>
<Transition
as={Fragment}
enter="duration-200 ease-out"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-100 ease-in"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95">
<Popover.Panel
focus
className="absolute inset-x-0 top-0 z-20 origin-top-right transform p-2 transition md:hidden">
<div className="dark:divide-slate divide-y-2 divide-slate-100 rounded-lg bg-slate-200 shadow-lg ring-1 ring-black ring-opacity-5 dark:divide-slate-700 dark:bg-slate-800">
<div className="px-5 pb-6 pt-5">
<div className="flex items-center justify-between">
<div>
<FooterLogo className="h-8 w-auto" />
</div>
<div className="-mr-2">
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-white p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 dark:bg-slate-700 dark:text-slate-200">
<span className="sr-only">Close menu</span>
<X className="h-6 w-6" aria-hidden="true" />
</Popover.Button>
</div>
</div>
</div>
<div className="px-5 py-6">
<div className="flex flex-col space-y-5 text-center text-sm dark:text-slate-300">
<div className="space-y-4">
{mainNav.map((item) => (
<Link key={item.name} href={item.href} className="block text-lg text-slate-700">
{item.name}
</Link>
))}
<Button
variant="primary"
onClick={() => router.push("https://app.formbricks.com/auth/signup")}
className="flex w-full justify-center text-lg">
Get started, it&apos;s free!
</Button>
</div>
</div>
</div>
</div>
</Popover.Panel>
</Transition>
</Popover>
</header>
);
}

View File

@@ -1,37 +0,0 @@
import CalLogoLight from "@/images/clients/cal-logo-light.svg";
import CrowdLogoLight from "@/images/clients/crowd-logo-light.svg";
import FlixbusLogo from "@/images/clients/flixbus-white.svg";
import NILogoDark from "@/images/clients/niLogoDark.svg";
import OptimoleLogo from "@/images/clients/optimole-logo.svg";
import ThemeisleLogo from "@/images/clients/themeisle-logo.webp";
import Image from "next/image";
export default function LogoBar() {
return (
<div className="mx-auto max-w-4xl">
<p className="text-center text-lg text-slate-700">
10,000+ teams at the worlds best companies trust Formbricks
</p>
<div className="mt-5 items-center px-4 sm:flex sm:justify-center md:mt-6 md:space-x-8 md:px-0">
<div className="grid grid-cols-2 items-center gap-8 pt-2 md:grid-cols-2 md:gap-10 lg:grid-cols-6">
<Image
src={FlixbusLogo}
alt="Flixbus Flix Flixtrain Logo"
className="rounded-lg pb-1 "
width={200}
/>
<Image src={CalLogoLight} alt="Cal Logo" className="block rounded-lg dark:hidden" width={170} />
<Image src={ThemeisleLogo} alt="ThemeIsle Logo" className="pb-1" width={200} />
<Image
src={CrowdLogoLight}
alt="Crowd.dev Logo"
className="block rounded-lg pb-1 dark:hidden"
width={200}
/>
<Image src={OptimoleLogo} alt="Optimole Logo" className="pb-1" width={200} />
<Image src={NILogoDark} alt="Neverinstall Logo" className="block pb-1 dark:hidden" width={200} />
</div>
</div>
</div>
);
}

View File

@@ -1,25 +0,0 @@
import SalesCTA from "@/components/salespage/SalesCTA";
interface Props {
headline: string;
subheadline: string;
}
export default function SalesBreaker({ headline, subheadline }: Props) {
return (
<div className="xs:mx-auto xs:w-full mx-4 my-4 mt-28 max-w-6xl rounded-xl bg-gradient-to-br from-slate-200 to-slate-300 md:mb-0 dark:from-slate-800 dark:via-slate-800 dark:to-slate-700">
<div className="relative px-4 py-8 sm:px-6 sm:pb-12 sm:pt-8 lg:px-8 lg:pt-12">
<div className="xs:block xs:absolute xs:right-10 hidden md:top-1/2 md:-translate-y-1/2">
<SalesCTA />
</div>
<h2 className="mt-4 text-2xl font-bold tracking-tight text-slate-800 lg:text-3xl">{headline}</h2>
<h4 className="text-md mt-4 max-w-3xl text-slate-500 lg:text-lg dark:text-slate-300">
{subheadline}
</h4>
<div className="xs:hidden mt-4">
<SalesCTA />
</div>
</div>
</div>
);
}

View File

@@ -1,20 +0,0 @@
import { usePlausible } from "next-plausible";
import { useRouter } from "next/router";
import { Button } from "@formbricks/ui/Button";
export default function SalesCTA() {
const plausible = usePlausible();
const router = useRouter();
return (
<Button
variant="darkCTA"
className="w-fit"
onClick={() => {
router.push("https://app.formbricks.com/auth/signup");
plausible("SalesPage_CTA_GetStartedNow");
}}>
Get started now
</Button>
);
}

View File

@@ -1,36 +0,0 @@
import SalesCTA from "@/components/salespage/SalesCTA";
import Image, { StaticImageData } from "next/image";
interface SalesPageFeatureProps {
imgSrc: StaticImageData;
imgAlt: string;
headline: string;
subheadline: string;
imgLeft?: boolean;
}
export default function SalesPageFeature({
imgSrc,
imgAlt,
headline,
subheadline,
imgLeft,
}: SalesPageFeatureProps) {
return (
<div className="group grid content-center gap-12 lg:grid-cols-2">
<div
className={`order-last flex flex-col justify-center space-y-6 lg:order-none ${imgLeft && `!order-last`}`}>
<h2 className="text-balance text-3xl font-bold text-slate-800">{headline}</h2>
<p className="text-pretty text-lg text-slate-700">{subheadline}</p>
<SalesCTA />
</div>
<div className="relative">
<Image
src={imgSrc}
alt={imgAlt}
className="rounded-3xl border border-slate-200 bg-white transition delay-75 duration-[1500ms] group-hover:scale-[105%] group-hover:border-slate-300"
/>
</div>
</div>
);
}

View File

@@ -1,28 +0,0 @@
import SalesCTA from "@/components/salespage/SalesCTA";
import Image, { StaticImageData } from "next/image";
interface SalesPageHeroProps {
imgSrc: StaticImageData;
imgAlt: string;
headline: React.ReactNode;
subheadline: string;
}
export default function SalesPageHero({ imgSrc, imgAlt, headline, subheadline }: SalesPageHeroProps) {
return (
<div className="group grid content-center gap-12 pt-20 lg:grid-cols-2">
<div className="my-auto space-y-6">
<h1 className="text-5xl font-bold text-slate-800">{headline}</h1>
<p className="text-balance text-lg text-slate-700">{subheadline}</p>
<SalesCTA />
</div>
<div className="relative hidden lg:block">
<Image
src={imgSrc}
alt={imgAlt}
className="scale-110 rounded-3xl border border-slate-200 bg-white transition-all delay-75 duration-[1500ms] group-hover:scale-[115%] group-hover:border-slate-300"
/>
</div>
</div>
);
}

View File

@@ -1,32 +0,0 @@
interface SalesStepsProps {
steps: Array<{ id: string; name: string; description: string }>;
}
export default function SalesSteps({ steps }: SalesStepsProps) {
return (
<div className="relative">
<ul role="list" className="grid grid-cols-1 gap-4 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:gap-10">
{steps.map((step) => {
return (
<li
key={step.id}
className="relative col-span-1 flex flex-col rounded-xl border border-slate-200 bg-slate-100 text-center ">
<div className="absolute -mt-12 w-full">
<div className="mx-auto flex h-20 w-20 items-center justify-center rounded-3xl bg-slate-200 text-5xl font-bold text-slate-700 shadow ">
{step.id}
</div>
</div>
<div className="flex flex-1 flex-col p-10">
<h3 className="my-4 text-lg font-medium text-slate-800 ">{step.name}</h3>
<dl className="mt-1 flex flex-grow flex-col justify-between">
<dt className="sr-only">Description</dt>
<dd className="text-slate-600 ">{step.description}</dd>
</dl>
</div>
</li>
);
})}
</ul>
</div>
);
}

View File

@@ -1,28 +0,0 @@
import Image, { StaticImageData } from "next/image";
interface TestimonialProps {
quote: string;
author: string;
imgSrc: StaticImageData;
imgAlt: string;
textSize: "base" | "large";
}
export default function SalesTestimonial({
quote,
author,
imgAlt,
imgSrc,
textSize = "base",
}: TestimonialProps) {
return (
<div className="flex flex-col items-center space-y-4 rounded-xl border border-slate-200 bg-slate-100 p-8 text-center">
<h3
className={`text-balance font-medium text-slate-700 ${textSize === "base" ? "text-xl" : "text-xl lg:text-2xl"} `}>
{quote}
</h3>
<p className="text-lg text-slate-500">{author}</p>
<Image src={imgSrc} alt={imgAlt} width={100} height={100} className="rounded-full" />
</div>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/solid";
import clsx from "clsx";
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { useState } from "react";
interface APICallProps {

View File

@@ -79,6 +79,15 @@ export default function BestPracticeNavigation() {
description: "Give users the chance to share feedback in a single click.",
category: "Boost Retention",
},
{
name: "Improve Newsletter Content",
href: "/improve-newsletter-content",
status: true,
icon: FeedbackIcon,
description: "Improve your newsletter content by showing this survey to your readers.",
category: "Boost Retention",
},
];
return (

View File

@@ -1,14 +1,20 @@
import HeadingCentered from "@/components/shared/HeadingCentered";
import BestPracticeNavigation from "./BestPracticeNavigation";
export default function InsightOppos() {
return (
<div id="best-practices">
<HeadingCentered
heading="Get started with Best Practices"
subheading="Run battle-tested approaches for qualitative user research in minutes."
/>
<div className="pb-10 pt-12 md:pt-20">
<div className="px-4 py-20 text-center sm:px-6 lg:px-8" id="best-practices">
<h1 className="text-3xl font-bold tracking-tight text-slate-800 sm:text-4xl md:text-5xl dark:text-slate-200">
Get started with{" "}
<span className="from-brand-light to-brand-dark bg-gradient-to-b bg-clip-text text-transparent xl:inline">
Best Practices
</span>
</h1>
<p className="mx-auto mt-3 max-w-md text-base text-slate-500 sm:text-lg md:mt-5 md:max-w-3xl md:text-xl dark:text-slate-300">
Run battle-tested approaches for qualitative user research in minutes.
</p>
</div>
<BestPracticeNavigation />
</div>
);

View File

@@ -22,7 +22,7 @@ export default function BreakerCTA({ inverted = false, teaser, headline, subhead
inverted
? "from-slate-800 via-slate-800 to-slate-700 dark:from-slate-200 dark:to-slate-300"
: "from-slate-200 to-slate-300 dark:from-slate-800 dark:via-slate-800 dark:to-slate-700",
"mx-auto w-full max-w-6xl rounded-xl bg-gradient-to-br "
"xs:mx-auto xs:w-full mx-4 my-4 mt-28 max-w-6xl rounded-xl bg-gradient-to-br md:mb-0 "
)}>
<div className="relative px-4 py-8 sm:px-6 sm:pb-12 sm:pt-8 lg:px-8 lg:pt-12">
<div className="xs:block xs:absolute xs:right-10 hidden md:top-1/2 md:-translate-y-1/2">

View File

@@ -9,7 +9,7 @@ export default function CTA() {
return (
<>
<div className="mx-auto px-4 py-16 sm:px-6 lg:px-8 lg:pb-40 lg:pt-24">
<HeadingCentered teaser="Get started" heading="Ready for the last form tool you need?" />
<HeadingCentered closer teaser="Get started" heading="Ready for the last form tool you need?" />
<div className="mt-12 grid grid-cols-1 content-center md:grid-cols-2">
<div className="-mb-4 rounded-t-xl bg-gradient-to-br from-slate-300 to-slate-200 px-8 py-24 text-center text-slate-900 md:-mr-5 md:mb-0 md:ml-2.5 md:rounded-l-xl lg:p-24 dark:from-slate-800 dark:to-slate-900 dark:text-slate-100">

View File

@@ -4,39 +4,13 @@ import { FaDiscord, FaGithub, FaXTwitter } from "react-icons/fa6";
import { FooterLogo } from "./Logo";
const navigation = {
products: [
{ name: "Link Surveys", href: "/open-source-form-builder", status: true },
{ name: "Website Surveys", href: "/website-survey", status: true },
{ name: "In-app Surveys", href: "/in-app-survey", status: true },
],
comparisons: [
{ name: "vs. Google Forms", href: "/vs-google-forms", status: true },
{ name: "vs. Formspree", href: "/vs-formspree", status: true },
{ name: "vs. OhMyForm", href: "/vs-ohmyform", status: true },
],
footernav: [
other: [
{ name: "Community", href: "/community", status: true },
{ name: "Pricing", href: "/pricing", status: true },
{ name: "Blog", href: "/blog", status: true },
{ name: "Docs", href: "/blog", status: true },
],
legal: [
{ name: "Imprint", href: "/imprint", status: true },
{ name: "Privacy Policy", href: "/privacy", status: true },
{ name: "Terms", href: "/terms", status: true },
{ name: "OSS Friends", href: "/oss-friends", status: true },
{ name: "GDPR FAQ", href: "/gdpr", status: true },
{ name: "GDPR Guide", href: "/gdpr-guide", status: true },
],
bestPractices: [
{ name: "Interview Prompt", href: "/interview-prompt", status: true },
{ name: "PMF Survey", href: "/measure-product-market-fit", status: true },
{ name: "Onboarding Segments", href: "/onboarding-segmentation", status: true },
{ name: "Learn from Churn", href: "/learn-from-churn", status: true },
{ name: "Improve Trial CR", href: "/improve-trial-conversion", status: true },
{ name: "Docs Feedback", href: "/docs-feedback", status: true },
{ name: "Feature Chaser", href: "/feature-chaser", status: true },
{ name: "Feedback Box", href: "/feedback-box", status: true },
],
social: [
{
name: "Twitter",
@@ -65,84 +39,27 @@ export default function Footer() {
<h2 id="footer-heading" className="sr-only">
Footer
</h2>
<div className="mx-auto grid max-w-7xl content-center gap-12 px-4 py-12 md:grid-cols-2 lg:grid-cols-3 lg:py-16">
<div className="space-y-6">
<Link href="/">
<span className="sr-only">Formbricks</span>
<FooterLogo className="h-8 w-auto sm:h-10" />
</Link>
<p className="text-base text-slate-500 dark:text-slate-400">Privacy-first Experience Management</p>
<div className="border-slate-500">
<p className="text-sm text-slate-400 dark:text-slate-500">
Formbricks GmbH &copy; {currentYear}. All rights reserved.
<br />
<Link href="/imprint">Imprint</Link> | <Link href="/privacy">Privacy Policy</Link> |{" "}
<Link href="/terms">Terms</Link> | <Link href="/oss-friends">OSS Friends</Link>
</p>
</div>
<div className="flex space-x-6">
{navigation.social.map((item) => (
<Link key={item.name} href={item.href} className="text-slate-400 hover:text-slate-500">
<span className="sr-only">{item.name}</span>
<item.icon className="h-6 w-6" aria-hidden="true" />
</Link>
))}
</div>
<div className="mx-auto flex max-w-7xl flex-col space-y-6 px-4 py-12 text-center sm:px-6 lg:px-8 lg:py-16">
<Link href="/">
<span className="sr-only">Formbricks</span>
<FooterLogo className="mx-auto h-8 w-auto sm:h-10" />
</Link>
<p className="text-base text-slate-500 dark:text-slate-400">Privacy-first Experience Management</p>
<div className="border-slate-500">
<p className="text-sm text-slate-400 dark:text-slate-500">
Formbricks GmbH &copy; {currentYear}. All rights reserved.
<br />
<Link href="/imprint">Imprint</Link> | <Link href="/privacy">Privacy Policy</Link> |{" "}
<Link href="/terms">Terms</Link> | <Link href="/oss-friends">OSS Friends</Link>
</p>
</div>
<div className="grid grid-cols-2 gap-8 lg:col-span-2 lg:grid-cols-4">
<div>
<h4 className="mb-2 font-medium text-slate-700">Formbricks</h4>
{navigation.footernav.map((item) => (
<Link
key={item.name}
href={item.href}
className="my-1 block text-slate-500 hover:text-slate-600">
{item.name}
</Link>
))}
</div>
<div>
<h4 className="mb-2 font-medium text-slate-700">Product</h4>
{navigation.products.map((item) => (
<Link
key={item.name}
href={item.href}
className="my-1 block text-slate-500 hover:text-slate-600">
{item.name}
</Link>
))}
<h4 className="mb-2 mt-5 font-medium text-slate-700">Comparison</h4>
{navigation.comparisons.map((item) => (
<Link
key={item.name}
href={item.href}
className="my-1 block text-slate-500 hover:text-slate-600">
{item.name}
</Link>
))}
</div>
<div>
<h4 className="mb-2 font-medium text-slate-700">Best Practices</h4>
{navigation.bestPractices.map((item) => (
<Link
key={item.name}
href={item.href}
className="my-1 block text-slate-500 hover:text-slate-600">
{item.name}
</Link>
))}
</div>
<div>
<h4 className="mb-2 font-medium text-slate-700">Legal</h4>
{navigation.legal.map((item) => (
<Link
key={item.name}
href={item.href}
className="my-1 block text-slate-500 hover:text-slate-600">
{item.name}
</Link>
))}
</div>
<div className="flex justify-center space-x-6">
{navigation.social.map((item) => (
<Link key={item.name} href={item.href} className="text-slate-400 hover:text-slate-500">
<span className="sr-only">{item.name}</span>
<item.icon className="h-6 w-6" aria-hidden="true" />
</Link>
))}
</div>
</div>
</footer>

View File

@@ -1,8 +1,8 @@
import GitHubMarkWhite from "@/images/github-mark-white.svg";
import GitHubMarkDark from "@/images/github-mark.svg";
import { Popover, Transition } from "@headlessui/react";
import { Bars3Icon, ChevronDownIcon, ChevronRightIcon, XMarkIcon } from "@heroicons/react/24/outline";
import clsx from "clsx";
import { ChevronDownIcon, ChevronRightIcon, MenuIcon, XIcon } from "lucide-react";
import { usePlausible } from "next-plausible";
import Image from "next/image";
import Link from "next/link";
@@ -136,7 +136,7 @@ export default function Header() {
<div className="-my-2 -mr-2 md:hidden">
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-slate-100 p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 dark:bg-slate-700 dark:text-slate-200">
<span className="sr-only">Open menu</span>
<MenuIcon className="h-6 w-6" aria-hidden="true" />
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
</Popover.Button>
</div>
<Popover.Group as="nav" className="hidden space-x-6 md:flex lg:space-x-10">
@@ -268,6 +268,12 @@ export default function Header() {
</>
)}
</Popover>
{/* <Link
href="/community"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Community
</Link>
*/}
<Link
href="/pricing"
className="text-sm font-medium text-slate-400 hover:text-slate-700 lg:text-base dark:hover:text-slate-300">
@@ -288,6 +294,11 @@ export default function Header() {
className="text-sm font-medium text-slate-400 hover:text-slate-700 lg:text-base dark:hover:text-slate-300">
Blog {/* <p className="bg-brand inline rounded-full px-2 text-xs text-white">1</p> */}
</Link>
{/* <Link
href="/careers"
className="text-base font-medium text-slate-400 hover:text-slate-700 dark:hover:text-slate-300">
Careers <p className="bg-brand inline rounded-full px-2 text-xs text-white">1</p>
</Link> */}
</Popover.Group>
<div className="hidden flex-1 items-center justify-end md:flex">
<Button
@@ -308,6 +319,11 @@ export default function Header() {
className="hidden dark:block"
/>
</Button>
{/* <Button variant="secondary" className="ml-2 px-2" onClick={() => setVideoModal(true)}>
<VideoWalkThrough open={videoModal} setOpen={() => setVideoModal(false)} />
<PlayCircleIcon className="h-6 w-6" />
</Button> */}
<Button
variant="highlight"
className="ml-2 text-xs lg:text-sm"
@@ -340,7 +356,7 @@ export default function Header() {
<div className="-mr-2">
<Popover.Button className="inline-flex items-center justify-center rounded-md bg-white p-2 text-slate-400 hover:bg-slate-100 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-teal-500 dark:bg-slate-700 dark:text-slate-200">
<span className="sr-only">Close menu</span>
<XIcon className="h-6 w-6" aria-hidden="true" />
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</Popover.Button>
</div>
</div>

View File

@@ -1,12 +1,15 @@
import clsx from "clsx";
interface Props {
teaser?: string;
heading: React.ReactNode;
heading: string;
subheading?: string;
closer?: boolean;
}
export default function HeadingCentered({ teaser, heading, subheading }: Props) {
export default function HeadingCentered({ teaser, heading, subheading, closer }: Props) {
return (
<div className="mb-12 text-center">
<div className={clsx(closer ? "pt-16 lg:pt-24" : "pt-24 lg:pt-40", "px-2 pb-4 text-center md:pb-12")}>
<p className="text-md text-brand-dark dark:text-brand-light mx-auto mb-3 max-w-2xl font-semibold uppercase sm:mt-4">
{teaser}
</p>

View File

@@ -1,5 +1,5 @@
import HeaderLight from "../salespage/HeaderLight";
import Footer from "./Footer";
import Header from "./Header";
import MetaInformation from "./MetaInformation";
interface LayoutProps {
@@ -10,11 +10,11 @@ interface LayoutProps {
export default function Layout({ title, description, children }: LayoutProps) {
return (
<div className="mx-auto w-full">
<div className="flex h-screen flex-col justify-between">
<MetaInformation title={title} description={description} />
<HeaderLight />
<Header />
{
<main className="max-w-8xl relative mx-auto flex w-full flex-col justify-center space-y-32 px-6 py-24 lg:px-24 xl:px-36 ">
<main className="max-w-8xl relative mx-auto mb-auto flex w-full flex-col justify-center sm:px-2 lg:px-8 xl:px-12">
{children}
</main>
}

View File

@@ -1,8 +1,8 @@
import HeaderLight from "@/components/salespage/HeaderLight";
import SlideInBanner from "@/components/shared/SlideInBanner";
import { useEffect } from "react";
import Footer from "./Footer";
import Header from "./Header";
import MetaInformation from "./MetaInformation";
import { Prose } from "./Prose";
@@ -39,7 +39,7 @@ interface Props {
export default function LayoutMdx({ meta, children }: Props) {
useExternalLinks(".prose a");
return (
<div className="mx-auto w-full">
<div className="flex h-screen flex-col justify-between">
<MetaInformation
title={meta.title}
description={meta.description}
@@ -48,7 +48,7 @@ export default function LayoutMdx({ meta, children }: Props) {
section={meta.section}
tags={meta.tags}
/>
<HeaderLight />
<Header />
<main className="min-w-0 max-w-2xl flex-auto px-4 lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16">
<article className="mx-auto my-16 max-w-3xl px-2">
{meta.title && (

View File

@@ -1,4 +1,4 @@
import { CopyIcon } from "lucide-react";
import { DocumentDuplicateIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import { Button } from "@formbricks/ui/Button";
@@ -28,7 +28,7 @@ export default function HeadingCentered() {
<div className="flex h-20 w-full items-center justify-between rounded-lg bg-slate-300 px-8 text-slate-700 dark:bg-slate-800 dark:text-slate-200 ">
<p>npm install @formbricks/react</p>
<button onClick={() => navigator.clipboard.writeText("npm install @formbricks/react")}>
<CopyIcon className="h-8 w-8" />
<DocumentDuplicateIcon className="h-8 w-8" />
</button>
</div>
</div>

View File

@@ -22,8 +22,8 @@ export default function MetaInformation({
}: Props) {
const router = useRouter();
const pageTitle = `${title}`;
const BASE_URL = `formbricks.com`;
const canonicalLink = `https://${BASE_URL}${router.asPath}`;
const BASE_URL = `https://${process.env.VERCEL_URL}`;
const canonicalLink = `${BASE_URL}${router.asPath}`;
return (
<Head>
<title>{pageTitle}</title>

View File

@@ -1,4 +1,4 @@
import { CheckIcon, XIcon } from "lucide-react";
import { CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
@@ -54,9 +54,13 @@ export const PricingTable = ({ leadRow, pricing, endRow }) => {
</Tooltip>
</TooltipProvider>
) : feature.free ? (
<CheckIcon className=" rounded-full border border-green-300 bg-green-100 p-0.5 text-green-500 dark:border-green-600 dark:bg-green-900 dark:text-green-300" />
<div className="h-6 w-6 rounded-full border border-green-300 bg-green-100 p-0.5 dark:border-green-600 dark:bg-green-900">
<CheckIcon className=" text-green-500 dark:text-green-300" />
</div>
) : (
<XIcon className="rounded-full border border-red-300 bg-red-100 p-0.5 text-red-500 dark:border-red-500 dark:bg-red-300 dark:text-red-600" />
<div className="h-6 w-6 rounded-full border border-red-300 bg-red-100 p-0.5 dark:border-red-500 dark:bg-red-300">
<XMarkIcon className="text-red-500 dark:text-red-600" />
</div>
)}
</div>
<div className="flex w-1/3 items-center justify-center text-center text-sm text-slate-800 dark:text-slate-100">
@@ -74,9 +78,13 @@ export const PricingTable = ({ leadRow, pricing, endRow }) => {
</Tooltip>
</TooltipProvider>
) : feature.paid ? (
<CheckIcon className=" rounded-full border border-green-300 bg-green-100 p-0.5 text-green-500 dark:border-green-600 dark:bg-green-900 dark:text-green-300" />
<div className="h-6 w-6 rounded-full border border-green-300 bg-green-100 p-0.5 dark:border-green-600 dark:bg-green-900">
<CheckIcon className="text-green-500 dark:text-green-300" />
</div>
) : (
<XIcon className="rounded-full border border-red-300 bg-red-100 p-0.5 text-red-500 dark:border-red-500 dark:bg-red-300 dark:text-red-600" />
<div className="h-6 w-6 rounded-full border border-red-300 bg-red-100 p-0.5 dark:border-red-600 dark:bg-red-900">
<XMarkIcon className="text-red-500 dark:text-red-600" />
</div>
)}
</div>
</div>

View File

@@ -1,5 +1,5 @@
import LFGLuigi from "@/images/blog/lfg-luigi-200px.webp";
import { XIcon } from "lucide-react";
import { XMarkIcon } from "@heroicons/react/24/solid";
import Image from "next/image";
import React, { useEffect, useState } from "react";
@@ -70,7 +70,7 @@ const SlideInBanner: React.FC<Props> = ({ delay = 5000, scrollPercentage = 10, U
setTimeout(() => setIsDismissed(true), 500);
}}
className="rounded-full p-2 hover:bg-slate-600 hover:bg-opacity-30">
<XIcon className="h-6 w-6" />
<XMarkIcon className="h-6 w-6" />
</button>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { CopyIcon } from "lucide-react";
import { DocumentDuplicateIcon } from "@heroicons/react/24/outline";
import { useRouter } from "next/router";
import { Button } from "@formbricks/ui/Button";
@@ -30,7 +30,7 @@ export default function HeadingCentered() {
<div className="flex h-20 w-full items-center justify-between rounded-lg bg-slate-800 px-8 text-slate-100 ">
<p>npm install @formbricks/react</p>
<button onClick={() => navigator.clipboard.writeText("npm install @formbricks/react")}>
<CopyIcon className="h-8 w-8" />
<DocumentDuplicateIcon className="h-8 w-8" />
</button>
</div>
</div>

View File

@@ -1,35 +1,42 @@
import { BlocksIcon, BoxIcon, LockIcon, SwatchBookIcon, TerminalIcon, UsersIcon } from "lucide-react";
import {
CommandLineIcon,
CubeTransparentIcon,
SquaresPlusIcon,
SwatchIcon,
UserGroupIcon,
UsersIcon,
} from "@heroicons/react/24/outline";
const features = [
{
name: "Futureproof",
description: "Form needs change. With Formbricks youll avoid island solutions right from the start.",
icon: BoxIcon,
icon: CubeTransparentIcon,
},
{
name: "Privacy by design",
description: "Self-host the entire product and fly through privacy compliance reviews.",
icon: LockIcon,
icon: UsersIcon,
},
{
name: "Community driven",
description: "We're building for you. If you need something specific, were happy to build it!",
icon: UsersIcon,
icon: UserGroupIcon,
},
{
name: "Great DX",
description: "We love a solid developer experience. We felt your pain and do our best to avoid it.",
icon: TerminalIcon,
icon: CommandLineIcon,
},
{
name: "Customizable",
description: "We have to build opinionated. If it doesn't suit your need, just change it up.",
icon: SwatchBookIcon,
icon: SwatchIcon,
},
{
name: "Extendable",
description: "Even though we try, we cannot build every single integration. With Formbricks, you can.",
icon: BlocksIcon,
icon: SquaresPlusIcon,
},
];

View File

@@ -1,79 +0,0 @@
import Script from "next/script";
import { FAQPage, WithContext } from "schema-dts";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@formbricks/ui/Accordion";
interface Answer {
"@type": "Answer";
text: string;
}
interface Question {
"@type": "Question";
name: string;
acceptedAnswer: Answer;
}
interface FAQ {
question: string;
answer: string;
}
interface FAQSchemaProps {
faqs: FAQ[];
headline: string;
description: string;
datePublished: string;
dateModified: string;
}
const SeoFaq: React.FC<FAQSchemaProps> = ({ faqs, headline, description, datePublished, dateModified }) => {
const FAQMainEntity: Question[] = faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: {
"@type": "Answer",
text: faq.answer,
},
}));
const FAQjsonld: WithContext<FAQPage> = {
"@context": "https://schema.org",
"@type": "FAQPage",
name: `Frequently Asked Questions around ${headline}`,
mainEntity: FAQMainEntity,
headline,
description,
author: {
"@type": "Person",
name: "Johannes Dancker",
url: "https://formbricks.com",
},
image: "",
datePublished,
dateModified,
};
return (
<>
<Script
id="faq-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(FAQjsonld),
}}
/>
<Accordion type="single" collapsible className="px-4 sm:px-0">
{faqs.map((faq, index) => (
<AccordionItem key={`item-${index}`} value={`item-${index + 1}`}>
<AccordionTrigger>{faq.question}</AccordionTrigger>
<AccordionContent>{faq.answer}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</>
);
};
export default SeoFaq;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 443 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -1,51 +1,50 @@
import { slugifyWithCounter } from '@sindresorhus/slugify'
import glob from 'fast-glob'
import * as fs from 'fs'
import { toString } from 'mdast-util-to-string'
import * as path from 'path'
import { remark } from 'remark'
import remarkMdx from 'remark-mdx'
import { createLoader } from 'simple-functional-loader'
import { filter } from 'unist-util-filter'
import { SKIP, visit } from 'unist-util-visit'
import * as url from 'url'
import { slugifyWithCounter } from "@sindresorhus/slugify";
import glob from "fast-glob";
import * as fs from "fs";
import { toString } from "mdast-util-to-string";
import * as path from "path";
import { remark } from "remark";
import remarkMdx from "remark-mdx";
import { createLoader } from "simple-functional-loader";
import { filter } from "unist-util-filter";
import { SKIP, visit } from "unist-util-visit";
import * as url from "url";
const __filename = url.fileURLToPath(import.meta.url)
const processor = remark().use(remarkMdx).use(extractSections)
const slugify = slugifyWithCounter()
const __filename = url.fileURLToPath(import.meta.url);
const processor = remark().use(remarkMdx).use(extractSections);
const slugify = slugifyWithCounter();
function isObjectExpression(node) {
return (
node.type === 'mdxTextExpression' &&
node.data?.estree?.body?.[0]?.expression?.type === 'ObjectExpression'
)
node.type === "mdxTextExpression" && node.data?.estree?.body?.[0]?.expression?.type === "ObjectExpression"
);
}
function excludeObjectExpressions(tree) {
return filter(tree, (node) => !isObjectExpression(node))
return filter(tree, (node) => !isObjectExpression(node));
}
function extractSections() {
return (tree, { sections }) => {
slugify.reset()
slugify.reset();
visit(tree, (node) => {
if (node.type === 'heading' || node.type === 'paragraph') {
let content = toString(excludeObjectExpressions(node))
if (node.type === 'heading' && node.depth <= 2) {
let hash = node.depth === 1 ? null : slugify(content)
sections.push([content, hash, []])
if (node.type === "heading" || node.type === "paragraph") {
let content = toString(excludeObjectExpressions(node));
if (node.type === "heading" && node.depth <= 2) {
let hash = node.depth === 1 ? null : slugify(content);
sections.push([content, hash, []]);
} else {
sections.at(-1)?.[2].push(content)
sections.at(-1)?.[2].push(content);
}
return SKIP
return SKIP;
}
})
}
});
};
}
export default function Search(nextConfig = {}) {
let cache = new Map()
export default function (nextConfig = {}) {
let cache = new Map();
return Object.assign({}, nextConfig, {
webpack(config, options) {
@@ -53,26 +52,26 @@ export default function Search(nextConfig = {}) {
test: __filename,
use: [
createLoader(function () {
let appDir = path.resolve('./src/app')
this.addContextDependency(appDir)
let appDir = path.resolve("./app");
this.addContextDependency(appDir);
let files = glob.sync('**/*.mdx', { cwd: appDir })
let files = glob.sync("**/*.mdx", { cwd: appDir });
let data = files.map((file) => {
let url = '/' + file.replace(/(^|\/)page\.mdx$/, '')
let mdx = fs.readFileSync(path.join(appDir, file), 'utf8')
let url = "/" + file.replace(/(^|\/)page\.mdx$/, "");
let mdx = fs.readFileSync(path.join(appDir, file), "utf8");
let sections = []
let sections = [];
if (cache.get(file)?.[0] === mdx) {
sections = cache.get(file)[1]
sections = cache.get(file)[1];
} else {
let vfile = { value: mdx, sections }
processor.runSync(processor.parse(vfile), vfile)
cache.set(file, [mdx, sections])
let vfile = { value: mdx, sections };
processor.runSync(processor.parse(vfile), vfile);
cache.set(file, [mdx, sections]);
}
return { url, sections }
})
return { url, sections };
});
// When this file is imported within the application
// the following module is loaded:
@@ -120,16 +119,16 @@ export default function Search(nextConfig = {}) {
pageTitle: item.doc.pageTitle,
}))
}
`
`;
}),
],
})
});
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options)
if (typeof nextConfig.webpack === "function") {
return nextConfig.webpack(config, options);
}
return config
return config;
},
})
});
}

View File

@@ -34,11 +34,6 @@ const nextConfig = {
},
async redirects() {
return [
{
source: "/demo",
destination: "/",
permanent: false,
},
{
source: "/discord",
destination: "https://discord.gg/3YFcABF2Ts",

View File

@@ -12,62 +12,62 @@
},
"browserslist": "defaults, not ie <= 11",
"dependencies": {
"@algolia/autocomplete-core": "^1.17.0",
"@calcom/embed-react": "^1.3.2",
"@docsearch/react": "^3.6.0",
"@algolia/autocomplete-core": "^1.13.0",
"@calcom/embed-react": "^1.3.0",
"@docsearch/react": "^3.5.2",
"@formbricks/lib": "workspace:*",
"@formbricks/types": "workspace:*",
"@formbricks/ui": "workspace:*",
"@headlessui/react": "^1.7.18",
"@headlessui/react": "^1.7.17",
"@headlessui/tailwindcss": "^0.2.0",
"lucide-react": "^0.356.0",
"@heroicons/react": "^2.1.1",
"@mapbox/rehype-prism": "^0.9.0",
"@mdx-js/loader": "^3.0.1",
"@mdx-js/react": "^3.0.1",
"@next/mdx": "14.1.3",
"@mdx-js/loader": "^3.0.0",
"@mdx-js/react": "^3.0.0",
"@next/mdx": "14.0.4",
"@paralleldrive/cuid2": "^2.2.2",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-tooltip": "^1.0.6",
"@sindresorhus/slugify": "^2.2.1",
"@tailwindcss/typography": "^0.5.10",
"acorn": "^8.11.3",
"autoprefixer": "^10.4.18",
"clsx": "^2.1.0",
"fast-glob": "^3.3.2",
"flexsearch": "^0.7.43",
"framer-motion": "11.0.13",
"@types/dompurify": "^3.0.5",
"@types/react-highlight-words": "^0.16.5",
"acorn": "^8.10.0",
"autoprefixer": "^10.4.15",
"clsx": "^2.0.0",
"fast-glob": "^3.3.1",
"flexsearch": "^0.7.31",
"framer-motion": "10.17.8",
"lottie-web": "^5.12.2",
"lucide": "^0.350.0",
"mdast-util-to-string": "^4.0.0",
"mdx-annotations": "^0.1.4",
"next": "14.1.3",
"next": "13.4.19",
"next-plausible": "^3.12.0",
"next-seo": "^6.5.0",
"next-seo": "^6.4.0",
"next-sitemap": "^4.2.3",
"next-themes": "^0.3.0",
"next-themes": "^0.2.1",
"node-fetch": "^3.3.2",
"prism-react-renderer": "^2.3.1",
"prismjs": "^1.29.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-highlight-words": "^0.20.0",
"react-icons": "^5.0.1",
"react-icons": "^4.12.0",
"react-markdown": "^9.0.1",
"react-responsive-embed": "^2.1.0",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
"remark-mdx": "^3.0.0",
"schema-dts": "^1.1.2",
"sharp": "^0.33.1",
"shiki": "^0.14.7",
"simple-functional-loader": "^1.2.1",
"tailwindcss": "^3.4.1",
"tailwindcss": "^3.4.0",
"unist-util-filter": "^5.0.1",
"unist-util-visit": "^5.0.0",
"zustand": "^4.5.2"
"zustand": "^4.4.7"
},
"devDependencies": {
"@formbricks/tsconfig": "workspace:*",
"@types/dompurify": "^3.0.5",
"@types/react-highlight-words": "^0.16.7",
"eslint-config-formbricks": "workspace:*"
}
}

View File

@@ -9,6 +9,7 @@ export default function Document() {
<Html className="scroll-smooth antialiased [font-feature-settings:'ss01']" lang="en" dir="ltr">
<Head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />

View File

@@ -121,11 +121,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
"Open source, end-to-end encrypted platform that lets you securely manage secrets and configs across your team, devices, and infrastructure.",
href: "https://infisical.com",
},
{
name: "Keep",
description: "Open source alert management and AIOps platform.",
href: "https://keephq.dev",
},
{
name: "Langfuse",
description: "Open source LLM engineering platform. Debug, analyze and iterate together.",
@@ -174,7 +169,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
name: "Requestly",
description:
"Makes frontend development cycle 10x faster with API Client, Mock Server, Intercept & Modify HTTP Requests and Session Replays.",
href: "https://requestly.com",
href: "https://requestly.io",
},
{
name: "Revert",

View File

@@ -14,7 +14,7 @@ import Userpilot from "./userpilot-best-feedback-in-app-tool.png";
export const meta = {
title: "Feedback App Contest: 6 Candidates, 1 Winner (and how to use it)",
description:
"We looked at the best in-app feedback tools 2024 and found a clear winner. Gather feedback in your app for free with Formbricks.",
"We looked at the best in app feedback tools 2024 and found a clear winner. Gather feedback in your app for free with Formbricks.",
date: "2023-12-21",
publishedTime: "2023-12-21T12:00:00",
authors: ["Olasunkanmi Balogun"],
@@ -22,7 +22,7 @@ export const meta = {
tags: ["Feedback Apps", "Formbricks", "Userpilot", "Pendo", "Appcues", "Survicate", "Qualaroo"],
};
<Image src={Header} alt="Gather in-app feedback for free with these 6 tools." className="w-full rounded-lg" />
<Image src={Header} alt="Gather in app feedback for free with these 6 tools." className="w-full rounded-lg" />
<AuthorBox
name="Olasunkanmi Balogun"
@@ -87,7 +87,7 @@ Among the plethora of tools available in todays market, this section will gui
<Image
src={Formbricks}
alt="Formbricks is a free and open source survey software for in-app micro surveys. Ask any user segment at any point in the user journey."
alt="Formbricks is a free and open source survey software for in app micro surveys. Ask any user segment at any point in the user journey."
className="w-full rounded-lg"
/>

View File

@@ -64,7 +64,7 @@ Let's have a look at the best HotJar alternatives in 2024, including open source
<Image
src={Formbricks}
alt="Formbricks is a free and open source survey software for in-app micro surveys. Ask any user segment at any point in the user journey."
alt="Formbricks is a free and open source survey software for in app micro surveys. Ask any user segment at any point in the user journey."
className="w-full rounded-lg"
/>

View File

@@ -30,13 +30,13 @@ _Most open source projects get abandoned after a while. But these 5 open source
Looking for the perfect open source survey tool to help you gather valuable insights and improve your business? Look no further!
We've compiled a list of the top 5 open source form and survey tools that are still maintained in 2024. In-app surveys, conversational bots, AI-generated surveys: These open source tools offer various features that cater to different needs.
We've compiled a list of the top 5 open source form and survey tools that are still maintained in 2024. In app surveys, conversational bots, AI-generated surveys: These open source tools offer various features that cater to different needs.
## 1. Formbricks - In-app micro surveys
## 1. Formbricks - In app micro surveys
<Image
src={Formbricks}
alt="Formbricks is a free and open source survey software for in-app micro surveys. Ask any user segment at any point in the user journey."
alt="Formbricks is a free and open source survey software for in app micro surveys. Ask any user segment at any point in the user journey."
className="rounded-lg w-full"
/>
@@ -125,7 +125,7 @@ LimeSurvey has been around for at least a decade. It's a powerful survey tool ma
In this article, we've rounded up the top 5 open source form and survey tools that are still rocking it in 2024. Perfect for devs who are always on the lookout for the latest and greatest!
1. Formbricks: A game-changer for in-app micro surveys, letting you target specific customer segments at any point in their journey. It's still early days, but this bad boy is worth keeping an eye on.
1. Formbricks: A game-changer for in app micro surveys, letting you target specific customer segments at any point in their journey. It's still early days, but this bad boy is worth keeping an eye on.
2. SurveyJS: A must-have for DIY enthusiasts, this collection of JavaScript libraries makes building your own form management system a breeze. Just remember, the starting price is $499/year.

View File

@@ -2,17 +2,17 @@ import AuthorBox from "@/components/shared/AuthorBox";
import LayoutMdx from "@/components/shared/LayoutMdx";
import Image from "next/image";
import CoverImage from './cover-best-feedbackt-tools-2024-open-source-website-surveys-targeted.webp';
import CrazyEgg from './crazy-egg-website-optimization-heatmaps-recordings-surveys.png';
import Formbricks from './formbricks-privacy-first-experience-management.png';
import Hotjar from './hotjar-website-heatmaps-behavior-analytics-tools.png';
import IdeaScale from './idea-and-innovation-management-software-ideaScale.png';
import Mopinion from './mopinion-feedback-for-websites-apps-and-email.png';
import Sprinklr from './sprinklr-unified-customer-experience-management-platform-sprinklr.png';
import SurveyMonkey from './surveyMonkey-the-world-most-popular-free-online-survey-tool.png';
import Qualaroo from './user-research-customer-feedback-software-qualaroo.png';
import UserReport from './userReport-simple-user-engagement-tools-that-help-you-improve.png';
import UserSnap from './usersnap-your-number-one-user-feedback-platform.png';
import CrazyEgg from './crazy-egg-website-optimization-heatmaps-recordings-surveys.png'
import CoverImage from './cover-best-feedbackt-tools-2024-open-source-website-surveys-targeted.webp'
import Formbricks from './formbricks-privacy-first-experience-management.png'
import Hotjar from './hotjar-website-heatmaps-behavior-analytics-tools.png'
import IdeaScale from './idea-and-innovation-management-software-ideaScale.png'
import Mopinion from './mopinion-feedback-for-websites-apps-and-email.png'
import Sprinklr from './sprinklr-unified-customer-experience-management-platform-sprinklr.png'
import SurveyMonkey from './surveyMonkey-the-world-most-popular-free-online-survey-tool.png'
import Qualaroo from './user-research-customer-feedback-software-qualaroo.png'
import UserReport from './userReport-simple-user-engagement-tools-that-help-you-improve.png'
import UserSnap from './usersnap-your-number-one-user-feedback-platform.png'
export const meta = {
title: "Best Website Feedback Tools in 2024",
@@ -68,7 +68,7 @@ These tools, as well see, are tools that help you collect and analyze the opi
<Image
src={Formbricks}
alt="Formbricks is a free and open source survey software for in-app micro surveys. Ask any user segment at any point in the user journey."
alt="Formbricks is a free and open source survey software for in app micro surveys. Ask any user segment at any point in the user journey."
className="rounded-lg w-full"
/>

View File

@@ -19,7 +19,7 @@ export const meta = {
tags: ["Improve Newsletter Content"],
};
<Image src={Header} alt="Gather in-app feedback for free with these 6 tools." className="w-full rounded-lg" />
<Image src={Header} alt="Gather in app feedback for free with these 6 tools." className="w-full rounded-lg" />
<AuthorBox
name="Olasunkanmi Balogun"

View File

@@ -118,7 +118,7 @@ Enter Formbricks, an open-source survey solution designed to capture targeted us
<Image
src={Formbricks}
alt="Formbricks is a free and open source survey software for in-app micro surveys. Ask any user segment at any point in the user journey."
alt="Formbricks is a free and open source survey software for in app micro surveys. Ask any user segment at any point in the user journey."
className="w-full rounded-lg"
/>

View File

@@ -1,5 +1,5 @@
import footerLogoDark from "@/images/logo/footerlogo-dark.svg";
import { MenuIcon } from "lucide-react";
import { Bars3Icon } from "@heroicons/react/24/solid";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
@@ -50,7 +50,7 @@ export default function HeaderLight() {
<Popover open={mobileNavMenuOpen} onOpenChange={setMobileNavMenuOpen}>
<PopoverTrigger onClick={() => setMobileNavMenuOpen(!mobileNavMenuOpen)}>
<span>
<MenuIcon className="h-8 w-8 rounded-md bg-slate-700 p-1 text-slate-200" />
<Bars3Icon className="h-8 w-8 rounded-md bg-slate-700 p-1 text-slate-200" />
</span>
</PopoverTrigger>
<PopoverContent className="border-slate-600 bg-slate-700 shadow">

View File

@@ -1,4 +1,4 @@
import { ChevronDownIcon } from "lucide-react";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
import Image from "next/image";
import Link from "next/link";
import { FaGithub } from "react-icons/fa6";

View File

@@ -202,9 +202,9 @@ const FAQ = [
"The commercial plan is for features who break the OSS WIN-WIN Loop or incur additional cost. We charge 30$ if you want a custom domain, remove Formbricks branding, collect large files in surveys or collect payments. We think thats fair :)",
},
{
question: "Are your in-app surveys also free forever?",
question: "Are your in app surveys also free forever?",
answer:
"The in-app surveys you can run with Formbricks are not part of this Deal. We offer a generous free plan but keep full control over the pricing in the long run. In-app surveys are really powerful for products with thousands of users and something has to bring in the dollars.",
"The in app surveys you can run with Formbricks are not part of this Deal. We offer a generous free plan but keep full control over the pricing in the long run. In app surveys are really powerful for products with thousands of users and something has to bring in the dollars.",
},
{

View File

@@ -0,0 +1,12 @@
import Layout from "@/components/demo/LayoutLight";
import DemoView from "@/components/dummyUI/DemoView";
export default function DemoPage() {
return (
<Layout
title="Formbricks Demo"
description="Play around with our pre-defined 30+ templates and them to kick-start your survey & experience management.">
<DemoView />
</Layout>
);
}

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