Compare commits
67 Commits
ReviewBot/
...
ReviewBot/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b301482868 | ||
|
|
9123e3c866 | ||
|
|
92d0c6bce6 | ||
|
|
ae3f1885c2 | ||
|
|
3ca6ec8b56 | ||
|
|
83a46d7313 | ||
|
|
b55b37b874 | ||
|
|
c76bcecca0 | ||
|
|
3776397468 | ||
|
|
4704c4a077 | ||
|
|
034ca1d639 | ||
|
|
e5862a2064 | ||
|
|
0332a2efe3 | ||
|
|
be8e461f55 | ||
|
|
722ee68b4c | ||
|
|
e4078a3307 | ||
|
|
907a9dc563 | ||
|
|
f6df94081d | ||
|
|
2436192995 | ||
|
|
f54e2e032a | ||
|
|
1a28660dfd | ||
|
|
f98a57582a | ||
|
|
0cc365261e | ||
|
|
6f78049c1f | ||
|
|
2f11aa6c14 | ||
|
|
09cb61ae1e | ||
|
|
65a152e518 | ||
|
|
92d88271d7 | ||
|
|
a56c354e84 | ||
|
|
29a9b7e23e | ||
|
|
94a419249b | ||
|
|
12907c9061 | ||
|
|
0aa468f8f3 | ||
|
|
91447e1502 | ||
|
|
84ea14820a | ||
|
|
8fb472c37c | ||
|
|
6efb6d4e7b | ||
|
|
99da20f831 | ||
|
|
52d1dc9ed9 | ||
|
|
5633bb18ef | ||
|
|
b2cb0ecff3 | ||
|
|
2089b339b4 | ||
|
|
2e83adc846 | ||
|
|
189cbcecd7 | ||
|
|
5aebde79e7 | ||
|
|
5cce4a1db4 | ||
|
|
c6ff74f166 | ||
|
|
e8aad9f469 | ||
|
|
455a061f35 | ||
|
|
a9f35df278 | ||
|
|
82124a8b1c | ||
|
|
f3f93faf1d | ||
|
|
57d117eb98 | ||
|
|
d01b293a27 | ||
|
|
a9f5289672 | ||
|
|
1df1419827 | ||
|
|
f20a0d2ff7 | ||
|
|
b9e5a6f9b9 | ||
|
|
b2eaf1f6a3 | ||
|
|
a873974f0d | ||
|
|
09974e1a10 | ||
|
|
45d5980527 | ||
|
|
73a25a412c | ||
|
|
f0647ce240 | ||
|
|
7c09dd9d10 | ||
|
|
c5bdbc89ca | ||
|
|
673832a7e1 |
@@ -56,11 +56,14 @@ 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 uplaod in serverless environments like Vercel
|
||||
# S3 Storage is required for the file upload in serverless environments like Vercel
|
||||
S3_ACCESS_KEY=
|
||||
S3_SECRET_KEY=
|
||||
S3_REGION=
|
||||
@@ -162,3 +165,6 @@ 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,6 +6,8 @@ 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,11 +24,6 @@ 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
|
||||
|
||||
@@ -78,9 +73,6 @@ 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
|
||||
|
||||
120
.github/workflows/kamal.yml
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
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 }}
|
||||
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_target:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
- "!(**.md|.github/CODEOWNERS)"
|
||||
|
||||
test:
|
||||
name: Run Tests
|
||||
name: Run Unit Tests
|
||||
needs: [changes]
|
||||
if: ${{ needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/test.yml
|
||||
@@ -58,6 +58,7 @@ 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,16 +31,6 @@ 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
|
||||
|
||||
@@ -89,10 +79,6 @@ 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,16 +14,6 @@ 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
|
||||
|
||||
@@ -52,7 +42,3 @@ 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: Tests
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
|
||||
14
.kamal/hooks/post-deploy.sample
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/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"
|
||||
3
.kamal/hooks/post-traefik-reboot.sample
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Rebooted Traefik on $KAMAL_HOSTS"
|
||||
51
.kamal/hooks/pre-build.sample
Executable file
@@ -0,0 +1,51 @@
|
||||
#!/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
|
||||
47
.kamal/hooks/pre-connect.sample
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/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 ]
|
||||
109
.kamal/hooks/pre-deploy.sample
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/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
|
||||
3
.kamal/hooks/pre-traefik-reboot.sample
Executable file
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Rebooting Traefik on $KAMAL_HOSTS..."
|
||||
@@ -3,25 +3,25 @@ import {
|
||||
ClockIcon,
|
||||
CogIcon,
|
||||
CreditCardIcon,
|
||||
DocumentChartBarIcon,
|
||||
FileBarChartIcon,
|
||||
HelpCircleIcon,
|
||||
HomeIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
ScaleIcon,
|
||||
ShieldCheckIcon,
|
||||
UserGroupIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
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: UserGroupIcon, current: false },
|
||||
{ name: "Reports", href: "#", icon: DocumentChartBarIcon, current: false },
|
||||
{ name: "Recipients", href: "#", icon: UsersIcon, current: false },
|
||||
{ name: "Reports", href: "#", icon: FileBarChartIcon, current: false },
|
||||
];
|
||||
const secondaryNavigation = [
|
||||
{ name: "Settings", href: "#", icon: CogIcon },
|
||||
{ name: "Help", href: "#", icon: QuestionMarkCircleIcon },
|
||||
{ name: "Help", href: "#", icon: HelpCircleIcon },
|
||||
{ name: "Privacy", href: "#", icon: ShieldCheckIcon },
|
||||
];
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@formbricks/js": "workspace:*",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"next": "14.1.1",
|
||||
"lucide-react": "^0.356.0",
|
||||
"next": "14.1.3",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
|
||||
@@ -21,6 +21,17 @@ 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 attributes = isUserId ? { "Init Attribute 1": "eight", "Init Attribute 2": "two" } : undefined;
|
||||
@@ -69,7 +80,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 demo/.env
|
||||
Copy the environment ID of your Formbricks app to the env variable in /apps/demo/.env
|
||||
</p>
|
||||
<Image src={fbsetup} alt="fb setup" className="mt-4 rounded" priority />
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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)
|
||||
@@ -22,8 +23,7 @@ This set of API can be used to
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
|
||||
Retrieve all the surveys you have for the environment.
|
||||
Retrieve all the surveys you have for the environment with pagination.
|
||||
|
||||
### Mandatory Headers
|
||||
|
||||
@@ -33,14 +33,26 @@ 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' \
|
||||
'https://app.formbricks.com/api/v1/management/surveys?offset=20&limit=10' \
|
||||
--header \
|
||||
'x-api-key: <your-api-key>'
|
||||
```
|
||||
@@ -403,7 +415,6 @@ This set of API can be used to
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
@@ -453,7 +464,7 @@ This set of API can be used to
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
@@ -497,7 +508,6 @@ This set of API can be used to
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
@@ -568,7 +578,7 @@ This set of API can be used to
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```json {{ title: '401 Not Authenticated' }}
|
||||
{
|
||||
"code": "not_authenticated",
|
||||
@@ -585,7 +595,6 @@ This set of API can be used to
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Delete Survey by ID {{ tag: 'DELETE', label: '/api/v1/management/surveys/<survey-id>' }}
|
||||
|
||||
<Row>
|
||||
|
||||
@@ -99,7 +99,6 @@ if (typeof window !== "undefined") {
|
||||
formbricks.init({
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,7 +130,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"
|
||||
/>
|
||||
@@ -182,7 +181,6 @@ useEffect(() => {
|
||||
formbricks.init({
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -232,7 +230,6 @@ if (typeof window !== "undefined") {
|
||||
formbricks.init({
|
||||
environmentId: "<environment-id>",
|
||||
apiHost: "<api-host>",
|
||||
debug: true, // remove when in production
|
||||
});
|
||||
}
|
||||
|
||||
@@ -269,14 +266,6 @@ 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.
|
||||
@@ -358,14 +347,6 @@ 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
|
||||
@@ -396,10 +377,9 @@ Enabling Formbricks debug mode in your browser is a useful troubleshooting step
|
||||
|
||||
To activate Formbricks debug mode:
|
||||
|
||||
1. **In Your Integration Code:**
|
||||
1. **Via URL Parameter:**
|
||||
|
||||
- Locate the initialization code for Formbricks in your application (HTML, ReactJS, NextJS, VueJS).
|
||||
- Set the `debug` option to `true` when initializing Formbricks.
|
||||
- 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.
|
||||
|
||||
2. **View Debug Logs:**
|
||||
|
||||
@@ -413,29 +393,21 @@ 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 functionality.
|
||||
- Identifying integration issues.
|
||||
- Verifying Formbricks initialization.
|
||||
- Identifying survey trigger issues.
|
||||
- Troubleshooting unexpected behavior.
|
||||
|
||||
### Debug Log Messages
|
||||
|
||||
Specific debug log messages may provide insights into:
|
||||
Debug log messages provide insights into:
|
||||
|
||||
- API calls and responses.
|
||||
- Event tracking and form interactions.
|
||||
- Integration errors.
|
||||
|
||||
**Note:** Disable debugging in production to prevent unnecessary logs and improve performance.
|
||||
- Event tracking, survey triggers and form interactions.
|
||||
- Initialization errors.
|
||||
|
||||
## Overwrite CSS Styles for In-App Surveys
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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";
|
||||
@@ -8,7 +9,6 @@ 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"
|
||||
/>
|
||||
|
||||
@@ -76,6 +76,27 @@ 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.
|
||||
@@ -115,52 +136,54 @@ 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` |
|
||||
| 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` |
|
||||
| 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 | | |
|
||||
|
||||
## Build-time Variables
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PlusIcon, TrashIcon } from "@heroicons/react/24/solid";
|
||||
import { PlusIcon, TrashIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Button } from "@formbricks/ui/Button";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
import { MousePointerClickIcon } from "lucide-react";
|
||||
|
||||
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">
|
||||
<CursorArrowRaysIcon />
|
||||
<MousePointerClickIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-lg font-medium text-slate-700 dark:text-slate-300">Add Action</div>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
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: DOMPurify.sanitize(htmlString) }}></label>
|
||||
dangerouslySetInnerHTML={{ __html: htmlString }}></label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,87 +1,66 @@
|
||||
import HeadingCentered from "@/components/shared/HeadingCentered";
|
||||
import { FAQPageJsonLd } from "next-seo";
|
||||
import SeoFaq from "@/components/shared/seo/SeoFaq";
|
||||
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@formbricks/ui/Accordion";
|
||||
|
||||
const FAQ_DATA = [
|
||||
const FAQs = [
|
||||
{
|
||||
question: "What is Formbricks?",
|
||||
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.
|
||||
</>
|
||||
),
|
||||
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.",
|
||||
},
|
||||
{
|
||||
question: "How do I integrate Formbricks into my application?",
|
||||
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>
|
||||
.
|
||||
</>
|
||||
),
|
||||
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.",
|
||||
},
|
||||
{
|
||||
question: "Is Formbricks GDPR compliant?",
|
||||
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.
|
||||
</>
|
||||
),
|
||||
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.",
|
||||
},
|
||||
{
|
||||
question: "Can I self-host Formbricks?",
|
||||
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>
|
||||
.
|
||||
</>
|
||||
),
|
||||
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.",
|
||||
},
|
||||
{
|
||||
question: "How does Formbricks pricing work?",
|
||||
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.
|
||||
</>
|
||||
),
|
||||
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.",
|
||||
},
|
||||
];
|
||||
|
||||
const faqJsonLdData = FAQ_DATA.map((faq) => ({
|
||||
questionName: faq.question,
|
||||
acceptedAnswerText: faq.answer(),
|
||||
}));
|
||||
|
||||
export default function FAQ() {
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,42 +24,39 @@ const features = [
|
||||
];
|
||||
export const Features: React.FC = () => {
|
||||
return (
|
||||
<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."
|
||||
/>
|
||||
<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."
|
||||
/>
|
||||
|
||||
<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" />
|
||||
</div>
|
||||
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 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>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
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 AnimationFallback from "@/public/animations/opensource-xm-platform-formbricks-fallback.png";
|
||||
import { ShieldCheckIcon, StarIcon } from "@heroicons/react/24/outline";
|
||||
import { ShieldCheckIcon, StarIcon } from "lucide-react";
|
||||
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="px-4 pb-20 pt-16 text-center sm:px-6 lg:px-8 lg:pb-32 lg:pt-20">
|
||||
<div className="text-center">
|
||||
<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
|
||||
@@ -46,9 +40,8 @@ export const Hero: React.FC = ({}) => {
|
||||
know what your customers need.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<Image
|
||||
src={FlixbusLogo}
|
||||
alt="Flixbus Flix Flixtrain Logo"
|
||||
@@ -56,37 +49,18 @@ export const Hero: React.FC = ({}) => {
|
||||
width={200}
|
||||
/>
|
||||
<Image src={CalLogoLight} alt="Cal Logo" className="block rounded-lg dark:hidden" width={170} />
|
||||
<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={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={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={OptimoleLogo} alt="Optimole 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"
|
||||
@@ -95,7 +69,7 @@ export const Hero: React.FC = ({}) => {
|
||||
router.push("https://app.formbricks.com/auth/signup");
|
||||
plausible("Hero_CTA_GetStartedItsFree");
|
||||
}}>
|
||||
Get Started, it's Free
|
||||
Get Started
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
@@ -108,9 +82,6 @@ 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" {...props}>
|
||||
<div className="relative hidden md:block" {...props}>
|
||||
<div ref={ref} />
|
||||
{!loaded && (
|
||||
<div className="absolute inset-0">
|
||||
|
||||
@@ -6,62 +6,43 @@ import Image from "next/image";
|
||||
|
||||
export const Highlights: React.FC = ({}) => {
|
||||
return (
|
||||
<>
|
||||
<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 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>
|
||||
</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 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 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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,144 +1,123 @@
|
||||
import DemoPreview from "@/components/dummyUI/DemoPreview";
|
||||
import DashboardMockupDark from "@/images/dashboard-mockup-dark.png";
|
||||
import DashboardMockup from "@/images/dashboard-mockup.png";
|
||||
import { CursorArrowRaysIcon } from "@heroicons/react/24/solid";
|
||||
import { MousePointerClickIcon } from "lucide-react";
|
||||
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 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 className="rounded-lg bg-slate-100 dark:bg-slate-800">
|
||||
<SetupTabs />
|
||||
</div>
|
||||
<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>
|
||||
<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} />
|
||||
</>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
19
apps/formbricks-com/components/salespage/FeatureCard.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
101
apps/formbricks-com/components/salespage/HeaderLight.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -8,16 +8,14 @@ interface LayoutProps {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export default function Layout({ title, description, children }: LayoutProps) {
|
||||
export default function LayoutLight({ 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 px-2 lg:px-8 xl:px-12">
|
||||
{children}
|
||||
</main>
|
||||
}
|
||||
<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>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
37
apps/formbricks-com/components/salespage/LogoBar.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
25
apps/formbricks-com/components/salespage/SalesBreaker.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
20
apps/formbricks-com/components/salespage/SalesCTA.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
28
apps/formbricks-com/components/salespage/SalesPageHero.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
32
apps/formbricks-com/components/salespage/SalesSteps.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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,15 +79,6 @@ 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,20 +1,14 @@
|
||||
import HeadingCentered from "@/components/shared/HeadingCentered";
|
||||
|
||||
import BestPracticeNavigation from "./BestPracticeNavigation";
|
||||
|
||||
export default function InsightOppos() {
|
||||
return (
|
||||
<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>
|
||||
|
||||
<div id="best-practices">
|
||||
<HeadingCentered
|
||||
heading="Get started with Best Practices"
|
||||
subheading="Run battle-tested approaches for qualitative user research in minutes."
|
||||
/>
|
||||
<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",
|
||||
"xs:mx-auto xs:w-full mx-4 my-4 mt-28 max-w-6xl rounded-xl bg-gradient-to-br md:mb-0 "
|
||||
"mx-auto w-full max-w-6xl rounded-xl bg-gradient-to-br "
|
||||
)}>
|
||||
<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 closer teaser="Get started" heading="Ready for the last form tool you need?" />
|
||||
<HeadingCentered 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,13 +4,39 @@ import { FaDiscord, FaGithub, FaXTwitter } from "react-icons/fa6";
|
||||
import { FooterLogo } from "./Logo";
|
||||
|
||||
const navigation = {
|
||||
other: [
|
||||
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: [
|
||||
{ name: "Community", href: "/community", status: true },
|
||||
{ name: "Pricing", href: "/pricing", status: true },
|
||||
{ name: "Blog", href: "/blog", status: true },
|
||||
{ name: "OSS Friends", href: "/oss-friends", 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: "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",
|
||||
@@ -39,27 +65,84 @@ export default function Footer() {
|
||||
<h2 id="footer-heading" className="sr-only">
|
||||
Footer
|
||||
</h2>
|
||||
<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 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>
|
||||
<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 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>
|
||||
</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>
|
||||
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
||||
<MenuIcon 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,12 +268,6 @@ 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">
|
||||
@@ -294,11 +288,6 @@ 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
|
||||
@@ -319,11 +308,6 @@ 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"
|
||||
@@ -356,7 +340,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>
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
<XIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</Popover.Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
interface Props {
|
||||
teaser?: string;
|
||||
heading: string;
|
||||
heading: React.ReactNode;
|
||||
subheading?: string;
|
||||
closer?: boolean;
|
||||
}
|
||||
|
||||
export default function HeadingCentered({ teaser, heading, subheading, closer }: Props) {
|
||||
export default function HeadingCentered({ teaser, heading, subheading }: Props) {
|
||||
return (
|
||||
<div className={clsx(closer ? "pt-16 lg:pt-24" : "pt-24 lg:pt-40", "px-2 pb-4 text-center md:pb-12")}>
|
||||
<div className="mb-12 text-center">
|
||||
<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="flex h-screen flex-col justify-between">
|
||||
<div className="mx-auto w-full">
|
||||
<MetaInformation title={title} description={description} />
|
||||
<Header />
|
||||
<HeaderLight />
|
||||
{
|
||||
<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">
|
||||
<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 ">
|
||||
{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="flex h-screen flex-col justify-between">
|
||||
<div className="mx-auto w-full">
|
||||
<MetaInformation
|
||||
title={meta.title}
|
||||
description={meta.description}
|
||||
@@ -48,7 +48,7 @@ export default function LayoutMdx({ meta, children }: Props) {
|
||||
section={meta.section}
|
||||
tags={meta.tags}
|
||||
/>
|
||||
<Header />
|
||||
<HeaderLight />
|
||||
<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 { DocumentDuplicateIcon } from "@heroicons/react/24/outline";
|
||||
import { CopyIcon } from "lucide-react";
|
||||
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")}>
|
||||
<DocumentDuplicateIcon className="h-8 w-8" />
|
||||
<CopyIcon 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 = `https://${process.env.VERCEL_URL}`;
|
||||
const canonicalLink = `${BASE_URL}${router.asPath}`;
|
||||
const BASE_URL = `formbricks.com`;
|
||||
const canonicalLink = `https://${BASE_URL}${router.asPath}`;
|
||||
return (
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { CheckIcon, XIcon } from "lucide-react";
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
|
||||
|
||||
@@ -54,13 +54,9 @@ export const PricingTable = ({ leadRow, pricing, endRow }) => {
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : feature.free ? (
|
||||
<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>
|
||||
<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-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>
|
||||
<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>
|
||||
<div className="flex w-1/3 items-center justify-center text-center text-sm text-slate-800 dark:text-slate-100">
|
||||
@@ -78,13 +74,9 @@ export const PricingTable = ({ leadRow, pricing, endRow }) => {
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : feature.paid ? (
|
||||
<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>
|
||||
<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-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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import LFGLuigi from "@/images/blog/lfg-luigi-200px.webp";
|
||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { XIcon } from "lucide-react";
|
||||
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">
|
||||
<XMarkIcon className="h-6 w-6" />
|
||||
<XIcon className="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DocumentDuplicateIcon } from "@heroicons/react/24/outline";
|
||||
import { CopyIcon } from "lucide-react";
|
||||
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")}>
|
||||
<DocumentDuplicateIcon className="h-8 w-8" />
|
||||
<CopyIcon className="h-8 w-8" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,42 +1,35 @@
|
||||
import {
|
||||
CommandLineIcon,
|
||||
CubeTransparentIcon,
|
||||
SquaresPlusIcon,
|
||||
SwatchIcon,
|
||||
UserGroupIcon,
|
||||
UsersIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { BlocksIcon, BoxIcon, LockIcon, SwatchBookIcon, TerminalIcon, UsersIcon } from "lucide-react";
|
||||
|
||||
const features = [
|
||||
{
|
||||
name: "Futureproof",
|
||||
description: "Form needs change. With Formbricks you’ll avoid island solutions right from the start.",
|
||||
icon: CubeTransparentIcon,
|
||||
icon: BoxIcon,
|
||||
},
|
||||
{
|
||||
name: "Privacy by design",
|
||||
description: "Self-host the entire product and fly through privacy compliance reviews.",
|
||||
icon: UsersIcon,
|
||||
icon: LockIcon,
|
||||
},
|
||||
{
|
||||
name: "Community driven",
|
||||
description: "We're building for you. If you need something specific, we’re happy to build it!",
|
||||
icon: UserGroupIcon,
|
||||
icon: UsersIcon,
|
||||
},
|
||||
{
|
||||
name: "Great DX",
|
||||
description: "We love a solid developer experience. We felt your pain and do our best to avoid it.",
|
||||
icon: CommandLineIcon,
|
||||
icon: TerminalIcon,
|
||||
},
|
||||
{
|
||||
name: "Customizable",
|
||||
description: "We have to build opinionated. If it doesn't suit your need, just change it up.",
|
||||
icon: SwatchIcon,
|
||||
icon: SwatchBookIcon,
|
||||
},
|
||||
{
|
||||
name: "Extendable",
|
||||
description: "Even though we try, we cannot build every single integration. With Formbricks, you can.",
|
||||
icon: SquaresPlusIcon,
|
||||
icon: BlocksIcon,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
79
apps/formbricks-com/components/shared/seo/SeoFaq.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
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;
|
||||
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 60 KiB |
BIN
apps/formbricks-com/images/clients/headshots/bailey.jpeg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
apps/formbricks-com/images/clients/headshots/jonathan.png
Normal file
|
After Width: | Height: | Size: 443 KiB |
BIN
apps/formbricks-com/images/clients/headshots/marius.jpeg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
apps/formbricks-com/images/clients/headshots/peer.jpeg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
apps/formbricks-com/images/clients/headshots/ram.jpeg
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
apps/formbricks-com/images/clients/headshots/sachin.jpeg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
apps/formbricks-com/images/clients/headshots/vishnu.jpeg
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
apps/formbricks-com/images/placeholder.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
@@ -1,50 +1,51 @@
|
||||
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 (nextConfig = {}) {
|
||||
let cache = new Map();
|
||||
export default function Search(nextConfig = {}) {
|
||||
let cache = new Map()
|
||||
|
||||
return Object.assign({}, nextConfig, {
|
||||
webpack(config, options) {
|
||||
@@ -52,26 +53,26 @@ export default function (nextConfig = {}) {
|
||||
test: __filename,
|
||||
use: [
|
||||
createLoader(function () {
|
||||
let appDir = path.resolve("./app");
|
||||
this.addContextDependency(appDir);
|
||||
let appDir = path.resolve('./src/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:
|
||||
@@ -119,16 +120,16 @@ export default function (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,6 +34,11 @@ 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.13.0",
|
||||
"@calcom/embed-react": "^1.3.0",
|
||||
"@docsearch/react": "^3.5.2",
|
||||
"@algolia/autocomplete-core": "^1.17.0",
|
||||
"@calcom/embed-react": "^1.3.2",
|
||||
"@docsearch/react": "^3.6.0",
|
||||
"@formbricks/lib": "workspace:*",
|
||||
"@formbricks/types": "workspace:*",
|
||||
"@formbricks/ui": "workspace:*",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"lucide-react": "^0.356.0",
|
||||
"@mapbox/rehype-prism": "^0.9.0",
|
||||
"@mdx-js/loader": "^3.0.0",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@next/mdx": "14.0.4",
|
||||
"@mdx-js/loader": "^3.0.1",
|
||||
"@mdx-js/react": "^3.0.1",
|
||||
"@next/mdx": "14.1.3",
|
||||
"@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",
|
||||
"@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",
|
||||
"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",
|
||||
"lottie-web": "^5.12.2",
|
||||
"lucide": "^0.350.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"mdx-annotations": "^0.1.4",
|
||||
"next": "13.4.19",
|
||||
"next": "14.1.3",
|
||||
"next-plausible": "^3.12.0",
|
||||
"next-seo": "^6.4.0",
|
||||
"next-seo": "^6.5.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"next-themes": "^0.2.1",
|
||||
"next-themes": "^0.3.0",
|
||||
"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": "^4.12.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"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.0",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"unist-util-filter": "^5.0.1",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"zustand": "^4.4.7"
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formbricks/tsconfig": "workspace:*",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/react-highlight-words": "^0.16.7",
|
||||
"eslint-config-formbricks": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ 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,6 +121,11 @@ 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.",
|
||||
@@ -169,7 +174,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.io",
|
||||
href: "https://requestly.com",
|
||||
},
|
||||
{
|
||||
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 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'
|
||||
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';
|
||||
|
||||
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 { Bars3Icon } from "@heroicons/react/24/solid";
|
||||
import { MenuIcon } from "lucide-react";
|
||||
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>
|
||||
<Bars3Icon className="h-8 w-8 rounded-md bg-slate-700 p-1 text-slate-200" />
|
||||
<MenuIcon 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 "@heroicons/react/24/outline";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
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.",
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 512 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 571 KiB |
|
After Width: | Height: | Size: 974 KiB |
|
After Width: | Height: | Size: 855 KiB |
|
After Width: | Height: | Size: 650 KiB |
|
After Width: | Height: | Size: 930 KiB |
312
apps/formbricks-com/pages/in-app-survey/index.tsx
Normal file
@@ -0,0 +1,312 @@
|
||||
import FeatureCard from "@/components/salespage/FeatureCard";
|
||||
import LayoutLight from "@/components/salespage/LayoutLight";
|
||||
import LogoBar from "@/components/salespage/LogoBar";
|
||||
import SalesBreaker from "@/components/salespage/SalesBreaker";
|
||||
import SalesPageFeature from "@/components/salespage/SalesPageFeature";
|
||||
import SalesPageHero from "@/components/salespage/SalesPageHero";
|
||||
import SalesSteps from "@/components/salespage/SalesSteps";
|
||||
import SalesTestimonial from "@/components/salespage/SalesTestimonial";
|
||||
import HeadingCentered from "@/components/shared/HeadingCentered";
|
||||
import SeoFaq from "@/components/shared/seo/SeoFaq";
|
||||
import Bailey from "@/images/clients/headshots/bailey.jpeg";
|
||||
import Ram from "@/images/clients/headshots/ram.jpeg";
|
||||
import Sachin from "@/images/clients/headshots/sachin.jpeg";
|
||||
import {
|
||||
IoCalendarNumber,
|
||||
IoCaretDownCircle,
|
||||
IoFileTrayFull,
|
||||
IoFilter,
|
||||
IoPlayForward,
|
||||
IoStopwatch,
|
||||
} from "react-icons/io5";
|
||||
|
||||
import Img1 from "./1-in-app-survey-open-source-free-gdpr-compliant-for-in-product-research.png";
|
||||
import Img2 from "./2-in-app-survey-open-source-sprig-alternative.png";
|
||||
import Img3 from "./3-granular-targeting-for-in-app-surveys-open-source.png";
|
||||
import Img4 from "./4-multi-language-in-app-survey-translation-rtl-ltr.png";
|
||||
import Img5 from "./5-fast-loading-in-product-surveys-for-apps-and-web-apps.png";
|
||||
import Img6 from "./6-in-app-survey-native-look-and-feel-design-open-source.png";
|
||||
import Img7 from "./7-unlimited-in-product-surveys-seats-team-members-open-source-and-free.png";
|
||||
import Img8 from "./8-team-roles-micro-surveys-in-app-open-source-and-for-free.png";
|
||||
import Img9 from "./9-reusable-segments-open-source-in-product-survey-software.png";
|
||||
|
||||
const inAppSurveySteps = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Connect your app",
|
||||
description:
|
||||
"Install the Formbricks SDK with your favorite package manager in seconds. Run native inapp surveys within minutes.",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Pre-segment cohorts",
|
||||
description:
|
||||
"Send attributes and events to Formbricks to create usage-based cohorts. Send out highly targeted app surveys for better insights.",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "AI analysis",
|
||||
description:
|
||||
"Analyze insights in Formbricks in a breeze with our AI. Enable everyone in your team to get the most out of your in-product research.",
|
||||
},
|
||||
];
|
||||
|
||||
const inAppSurveyFeatures = [
|
||||
{
|
||||
headline: "Granular in-app targeting",
|
||||
subheadline:
|
||||
"Combine usage data with custom attributes and device information for fine-grained targeting. Targeted embedded surveys mean better insights for your research team and a better UX for your users.",
|
||||
imgSrc: Img3,
|
||||
imgAlt: "Screenshot of granular targeting feature in an in-app survey tool",
|
||||
imgLeft: false,
|
||||
},
|
||||
{
|
||||
headline: "Multi-language app surveys",
|
||||
subheadline:
|
||||
"For app surveys to fit in smoothly, they should feel like a part of your UI. Matching languages plays a big role for seamless product research. Formbricks lets you handle survey translations easily.",
|
||||
imgSrc: Img4,
|
||||
imgAlt: "Example of a multi-language survey embedded in a mobile app",
|
||||
imgLeft: true,
|
||||
},
|
||||
{
|
||||
headline: "Super fast loading",
|
||||
subheadline:
|
||||
"The Formbricks SDK is tiny (7KB). Keep your app lightning fast and your users engaged. The in-app survey SDK always loads deferred and never slows down your app.",
|
||||
imgSrc: Img5,
|
||||
imgAlt: "Demonstration of super fast loading times for an embedded survey in an app",
|
||||
imgLeft: false,
|
||||
},
|
||||
{
|
||||
headline: "On brand design",
|
||||
subheadline:
|
||||
"Customize your embedded surveys to fit in. Match the look & feel of your embedded surveys with your app. Leverage our no-code design editor or load a custom style sheet - all on the free plan!",
|
||||
imgSrc: Img6,
|
||||
imgAlt: "Preview of an on-brand design survey custom designed to fit within an app",
|
||||
imgLeft: true,
|
||||
},
|
||||
{
|
||||
headline: "Unlimited seats & products included",
|
||||
subheadline:
|
||||
"Embed Formbricks to run surveys in as many apps as you wish, without additional cost. Invite everyone who should work with user insights (hence, everyone). Product survey tools should never limit how far customer insights spread within a company.",
|
||||
imgSrc: Img7,
|
||||
imgAlt: "Illustration of embedding Formbricks surveys across multiple mobile apps",
|
||||
imgLeft: false,
|
||||
},
|
||||
];
|
||||
|
||||
const linkSurveyFeaturesPt2 = [
|
||||
{
|
||||
headline: "Team roles",
|
||||
subheadline:
|
||||
"Control who can set up app surveys, and who gets to work with the insights gathered by your reserach. Granular access control allows everyone to work with the insights gathered with in-product research.",
|
||||
imgSrc: Img8,
|
||||
imgAlt: "Interface showcasing team roles and access rights for in-app survey setup and insights",
|
||||
imgLeft: true,
|
||||
},
|
||||
{
|
||||
headline: "Reuse segments to target consistently",
|
||||
subheadline:
|
||||
"Compose segments of app users with advanced filters. Reuse these segments to survey the same cohorts consistently. Keeping your targeting consistent allows to measure how much your app experience improves over time.",
|
||||
imgSrc: Img9,
|
||||
imgAlt: "Visualization of creating and reusing segments for targeted surveys inapp",
|
||||
imgLeft: false,
|
||||
},
|
||||
];
|
||||
|
||||
const allFeaturesList = [
|
||||
{
|
||||
title: "Show survey to % of user",
|
||||
text: "Only show surveys to e.g. 50% of visitors.",
|
||||
icon: IoFilter,
|
||||
},
|
||||
{
|
||||
title: "Add delay before showing",
|
||||
text: "Wait a few seconds before showing the survey",
|
||||
icon: IoStopwatch,
|
||||
},
|
||||
{
|
||||
title: "Auto close in inactivity",
|
||||
text: "Auto close a survey if the visitors does not interact.",
|
||||
icon: IoCaretDownCircle,
|
||||
},
|
||||
{
|
||||
title: "Close survey on response limit",
|
||||
text: "Auto-close a survey after hitting e.g. 50 responses",
|
||||
icon: IoFileTrayFull,
|
||||
},
|
||||
{
|
||||
title: "Close survey on date",
|
||||
text: "Auto-close a survey on a specific date.",
|
||||
icon: IoCalendarNumber,
|
||||
},
|
||||
{
|
||||
title: "Redirect on completion",
|
||||
text: "Redirect visitors after they completed your survey.",
|
||||
icon: IoPlayForward,
|
||||
},
|
||||
];
|
||||
|
||||
const FAQs = [
|
||||
{
|
||||
question: "Is Formbricks really free for creating embedded or in-app surveys?",
|
||||
answer:
|
||||
"Yes, Formbricks offers both a free Cloud plan and an open source community edition. This makes it an accessible choice for deploying embedded surveys and in-app survey. Advanced features are available for those needing more specialized capabilities.",
|
||||
},
|
||||
{
|
||||
question: "Can I self-host Formbricks for more control over my product research tools?",
|
||||
answer:
|
||||
"Absolutely. Formbricks can be self-hosted with one click via our Docker image. This gives you full control over your product survey tools, while ensuring data privacy and compliance.",
|
||||
},
|
||||
{
|
||||
question:
|
||||
"How does Formbricks compare to other micro survey software in terms of features and flexibility?",
|
||||
answer:
|
||||
"Formbricks offers a comprehensive suite of features for embedded surveys, in-app feedback, and micro surveys. For see the speed development, have a look at the Formbricks repository on GitHub linked in the Footer. In case you're missing something, just let us know and we'll build it.",
|
||||
},
|
||||
{
|
||||
question: "Is Formbricks GDPR-compliant for use as an in-app survey tool and embedded survey platform?",
|
||||
answer:
|
||||
"Yes, Formbricks is fully GDPR and CCPA compliant. It's a reliable choice for businesses seeking an in-app survey tool which handles potentially personalized data. Hosted in Frankfurt, Germany, and developed by a German company, it ensures the highest standards of data protection.",
|
||||
},
|
||||
{
|
||||
question: "Can Formbricks help in conducting micro surveys within a mobile app?",
|
||||
answer: "Currently, we do not offer SDKs for mobile apps yet. However, this is on the roadmap for 2024.",
|
||||
},
|
||||
{
|
||||
question: "What are the best tools for creating an app survey?",
|
||||
answer:
|
||||
"For creating app surveys, Formbricks is among the top tools. As an open source product, we keep developer requirements at heart. However, the UX of Formbricks is designed also support marketers, researchers and sales reps in their work.",
|
||||
},
|
||||
{
|
||||
question: "How can I implement an in-app survey effectively?",
|
||||
answer:
|
||||
"To implement an in-app survey effectively, use Formbricks to embed surveys directly into your application. In-product research has higher conversion and completion rates than any other form of surveying.",
|
||||
},
|
||||
{
|
||||
question: "What is a micro survey and how can I use it with Formbricks?",
|
||||
answer:
|
||||
"A micro survey is a short, focused survey designed to capture quick insights. With Formbricks, you can easily create and embed these micro surveys into your website or app, enhancing the user experience and obtaining precise feedback.",
|
||||
},
|
||||
{
|
||||
question: "Are embedded surveys more effective for user engagement?",
|
||||
answer:
|
||||
"Yes, embedded surveys, like those created with Formbricks, are highly effective for user engagement. They blend naturally with the app or website, encouraging more users to participate and share their insights without leaving the platform.",
|
||||
},
|
||||
{
|
||||
question: "What advantages does Formbricks offer for in-app survey tools?",
|
||||
answer:
|
||||
"Formbricks offers numerous advantages for in-app surveys, including easy integration, real-time analytics, customizable survey templates, and micro survey capabilities. It's a powerful tool for enhancing user engagement and feedback collection.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function LinkSurveyPage() {
|
||||
return (
|
||||
<LayoutLight
|
||||
title="In-app Surveys, Open Source"
|
||||
description="Run targeted in-app surveys with full control over your data. Natively embed open source in-product reserach to understand what your users think. Get started in minutes.">
|
||||
<SalesPageHero
|
||||
headline={
|
||||
<span>
|
||||
In-app Surveys People <i>Want</i> to Fill Out
|
||||
</span>
|
||||
}
|
||||
subheadline="In-product user research with a native look and feel. Ask only the right cohort, ask gracefully."
|
||||
imgSrc={Img1}
|
||||
imgAlt="Targeted in-app surveys built on open source technology."
|
||||
/>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<SalesTestimonial
|
||||
quote="We self-host Formbricks for our app with 100.000+ users. It's remarkable how the surveys feel like a native part of our own app. Great product, built for everyone who cares about UX!"
|
||||
author="Ram Pasala, CEO @ NeverInstall"
|
||||
imgSrc={Ram}
|
||||
imgAlt="Ram Pasala, CEO @ NeverInstall"
|
||||
textSize="base"
|
||||
/>
|
||||
<SalesTestimonial
|
||||
quote="As a product-led growth company, we run surveys at key moments in our user journey. We spent a lot of time crafting our UX and I love how seamless Formbricks fits in! Should be a no-brainer for every product team."
|
||||
author="Bailey Pumfleet, Co-CEO @ Cal.com"
|
||||
imgSrc={Bailey}
|
||||
imgAlt="Cal.com co-founder Bailey Pumfleet speaks about how Formbricks in-app surveys feel like a native part of the UI of their product."
|
||||
textSize="large"
|
||||
/>
|
||||
</div>
|
||||
<SalesPageFeature
|
||||
headline="Native look and feel, powered by open source"
|
||||
subheadline="Formbricks is fully open source. Integrate it natively and keep engineers, designers and researchers happy. Formbricks is the most versatile open source in-product survey tool available."
|
||||
imgSrc={Img2}
|
||||
imgAlt="Indicator of GitHub stars for open source in-app survey product Formbricks which rund embedded surveys with a native look and feel."
|
||||
imgLeft
|
||||
/>
|
||||
|
||||
<SalesSteps steps={inAppSurveySteps} />
|
||||
{inAppSurveyFeatures.map((feature) => {
|
||||
return (
|
||||
<SalesPageFeature
|
||||
key={feature.headline}
|
||||
headline={feature.headline}
|
||||
subheadline={feature.subheadline}
|
||||
imgSrc={feature.imgSrc}
|
||||
imgAlt={feature.imgAlt}
|
||||
imgLeft={feature.imgLeft}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<LogoBar />
|
||||
{linkSurveyFeaturesPt2.map((feature) => {
|
||||
return (
|
||||
<SalesPageFeature
|
||||
key={feature.headline}
|
||||
headline={feature.headline}
|
||||
subheadline={feature.subheadline}
|
||||
imgSrc={feature.imgSrc}
|
||||
imgAlt={feature.imgAlt}
|
||||
imgLeft={feature.imgLeft}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<SalesTestimonial
|
||||
quote="We're using Formbricks with Amplitude. The surveys fit in perfectly with our UI and the event-based targeting is super useful. The team loves it!"
|
||||
author="Sachin Jain, CEO @ Requestly (YC W22)"
|
||||
imgSrc={Sachin}
|
||||
imgAlt="Sachin Jain, CEO @ Requestly"
|
||||
textSize="base"
|
||||
/>
|
||||
<div className="space-y-12">
|
||||
<HeadingCentered
|
||||
heading={
|
||||
<span>
|
||||
In-app surveys <i>exactly</i> how you want them
|
||||
</span>
|
||||
}
|
||||
teaser="All features backed in"
|
||||
/>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{allFeaturesList.map((feature) => {
|
||||
return (
|
||||
<FeatureCard
|
||||
key={feature.title}
|
||||
title={feature.title}
|
||||
text={feature.text}
|
||||
Icon={feature.icon}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<SalesBreaker
|
||||
headline="Embed surveys the right way - natively."
|
||||
subheadline="You spent months crafting your product, don’t ruin it with pop-ups."
|
||||
/>
|
||||
<div>
|
||||
<HeadingCentered heading="Frequently asked questions" teaser="FAQ" />
|
||||
<SeoFaq
|
||||
faqs={FAQs}
|
||||
headline="Targeted website surveys, open source. Like HotJar Ask but GDPR compliant."
|
||||
description="Make the most out of your website traffic by asking pointed quesitons in online surveys."
|
||||
datePublished="2024-03-12"
|
||||
dateModified="2024-03-12"
|
||||
/>
|
||||
</div>
|
||||
</LayoutLight>
|
||||
);
|
||||
}
|
||||
@@ -7,19 +7,19 @@ import Steps from "@/components/home/Steps";
|
||||
import BestPractices from "@/components/shared/BestPractices";
|
||||
import BreakerCTA from "@/components/shared/BreakerCTA";
|
||||
import Layout from "@/components/shared/Layout";
|
||||
import AnimationFallback from "@/public/animations/opensource-xm-platform-formbricks-fallback.png";
|
||||
|
||||
import HeroAnimation from "../components/home/HeroAnimation";
|
||||
|
||||
const IndexPage = () => (
|
||||
<Layout
|
||||
title="Formbricks | Privacy-first Experience Management"
|
||||
description="Build qualitative user research into your product. Leverage Best practices to increase Product-Market Fit.">
|
||||
<Hero />
|
||||
<BestPractices />
|
||||
<HeroAnimation fallbackImage={AnimationFallback} />
|
||||
<Features />
|
||||
<Highlights />
|
||||
<ScrollToTopButton />
|
||||
{/* <div className="block lg:hidden">
|
||||
<GitHubSponsorship />
|
||||
</div> */}
|
||||
<div className="hidden lg:block">
|
||||
<BreakerCTA
|
||||
teaser="READY?"
|
||||
@@ -29,9 +29,7 @@ const IndexPage = () => (
|
||||
href="https://app.formbricks.com/auth/signup"
|
||||
/>
|
||||
</div>
|
||||
<div className="pb-16"> </div>
|
||||
<Steps />
|
||||
|
||||
<BreakerCTA
|
||||
teaser="Curious?"
|
||||
headline="Give it a squeeze 🍋"
|
||||
@@ -40,8 +38,8 @@ const IndexPage = () => (
|
||||
href="https://app.formbricks.com/auth/signup"
|
||||
inverted
|
||||
/>
|
||||
|
||||
<Faq />
|
||||
<BestPractices />
|
||||
</Layout>
|
||||
);
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 3.8 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 492 KiB |
|
After Width: | Height: | Size: 953 KiB |