Compare commits
15 Commits
ReviewBot/
...
ReviewBot/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a642bfdd0 | ||
|
|
7a1af85141 | ||
|
|
2586a3ba3a | ||
|
|
77408bf0b0 | ||
|
|
d5b183155b | ||
|
|
d7fc7995bc | ||
|
|
a9d8239a25 | ||
|
|
71bdb5095a | ||
|
|
b84e322eee | ||
|
|
08ccb954f3 | ||
|
|
38c6cb01df | ||
|
|
2c13121487 | ||
|
|
73f1d09dc8 | ||
|
|
1f884a408c | ||
|
|
ed2253dcfc |
@@ -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
|
||||
|
||||
2
.github/actions/cache-build-web/action.yml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/ecs-deployment.yml
vendored
@@ -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
|
||||
|
||||
121
.github/workflows/kamal.yml
vendored
@@ -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
|
||||
5
.github/workflows/pr.yml
vendored
@@ -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
|
||||
|
||||
14
.github/workflows/release-docker-github.yml
vendored
@@ -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
|
||||
|
||||
14
.github/workflows/release-docker.yml
vendored
@@ -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 }}
|
||||
|
||||
2
.github/workflows/test.yml
vendored
@@ -3,7 +3,7 @@ on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
build:
|
||||
name: Unit Tests
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Rebooted Traefik on $KAMAL_HOSTS"
|
||||
@@ -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
|
||||
@@ -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 ]
|
||||
@@ -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
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Rebooting Traefik on $KAMAL_HOSTS..."
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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. Let’s 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. Let’s 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"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
**Can’t figure it out?**: [Join our Discord!](https://formbricks.com/discord)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
45
apps/formbricks-com/components/demo/HeaderLight.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'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'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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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. Here’s 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 <script> tag to your HTML head - that’s 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 <script> tag to your HTML head - that’s 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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'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's free!
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -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 world’s 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 © {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 © {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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 you’ll 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, we’re 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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
Before Width: | Height: | Size: 399 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 443 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 7.8 KiB |
@@ -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;
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,11 +34,6 @@ const nextConfig = {
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: "/demo",
|
||||
destination: "/",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/discord",
|
||||
destination: "https://discord.gg/3YFcABF2Ts",
|
||||
|
||||
@@ -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:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 today’s 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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 we’ll 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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 that’s 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.",
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
12
apps/formbricks-com/pages/demo/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 106 KiB |